@consilioweb/payload-support 1.1.0 → 2.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 (99) hide show
  1. package/README.md +4 -0
  2. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +22 -14
  3. package/dist/components/TicketConversation/components/AISummaryPanel.js +23 -16
  4. package/dist/components/TicketConversation/components/ActionPanels.cjs +5 -5
  5. package/dist/components/TicketConversation/components/ActionPanels.js +5 -5
  6. package/dist/components/TicketConversation/components/ClientBar.cjs +1 -1
  7. package/dist/components/TicketConversation/components/ClientBar.js +1 -1
  8. package/dist/components/TicketConversation/components/QuickActions.cjs +2 -2
  9. package/dist/components/TicketConversation/components/QuickActions.js +2 -2
  10. package/dist/components/TicketConversation/components/TicketHeader.cjs +1 -1
  11. package/dist/components/TicketConversation/components/TicketHeader.js +1 -1
  12. package/dist/components/TicketConversation/index.cjs +10 -10
  13. package/dist/components/TicketConversation/index.js +10 -10
  14. package/dist/index.cjs +710 -174
  15. package/dist/index.d.cts +165 -3
  16. package/dist/index.d.ts +165 -3
  17. package/dist/index.js +703 -176
  18. package/dist/styles/ChatView.module.scss +1 -1
  19. package/dist/styles/TicketDetail.module.scss +3 -3
  20. package/dist/styles/_tokens.scss +1 -1
  21. package/dist/styles/theme.css +6 -6
  22. package/dist/views/TicketingSettingsView/client.cjs +3 -3
  23. package/dist/views/TicketingSettingsView/client.js +3 -3
  24. package/package.json +9 -9
  25. package/src/__tests__/aiSummaryRendering.test.ts +21 -0
  26. package/src/__tests__/authResponses.test.ts +69 -0
  27. package/src/__tests__/capabilities.test.ts +71 -0
  28. package/src/__tests__/generateTrackingToken.test.ts +18 -4
  29. package/src/__tests__/integration/core-behaviors.test.ts +23 -0
  30. package/src/__tests__/rateLimiter.test.ts +60 -66
  31. package/src/__tests__/trackOpenEndpoint.test.ts +54 -0
  32. package/src/__tests__/webhookSecurity.test.ts +58 -0
  33. package/src/collections/PendingEmails.ts +2 -1
  34. package/src/collections/SupportClients.ts +43 -1
  35. package/src/collections/SupportCounters.ts +14 -0
  36. package/src/collections/SupportRateLimits.ts +20 -0
  37. package/src/collections/TicketMessages.ts +76 -0
  38. package/src/collections/Tickets.ts +183 -40
  39. package/src/collections/index.ts +2 -0
  40. package/src/components/TicketConversation/components/AISummaryPanel.tsx +26 -13
  41. package/src/components/TicketConversation/components/ActionPanels.tsx +5 -5
  42. package/src/components/TicketConversation/components/ClientBar.tsx +1 -1
  43. package/src/components/TicketConversation/components/QuickActions.tsx +2 -2
  44. package/src/components/TicketConversation/components/TicketHeader.tsx +1 -1
  45. package/src/components/TicketConversation/index.tsx +10 -10
  46. package/src/endpoints/admin-chat.ts +4 -4
  47. package/src/endpoints/ai-agent.ts +6 -1
  48. package/src/endpoints/ai.ts +10 -5
  49. package/src/endpoints/auth-2fa.ts +6 -7
  50. package/src/endpoints/auto-close.ts +2 -1
  51. package/src/endpoints/capabilities.ts +144 -0
  52. package/src/endpoints/channels.ts +2 -1
  53. package/src/endpoints/chat.ts +6 -6
  54. package/src/endpoints/chatbot.ts +4 -4
  55. package/src/endpoints/client-intelligence.ts +13 -4
  56. package/src/endpoints/import-conversation.ts +6 -5
  57. package/src/endpoints/index.ts +35 -14
  58. package/src/endpoints/invite-collaborator.ts +4 -4
  59. package/src/endpoints/login.ts +4 -5
  60. package/src/endpoints/oauth-google.ts +1 -1
  61. package/src/endpoints/process-digests.ts +2 -1
  62. package/src/endpoints/process-scheduled.ts +2 -1
  63. package/src/endpoints/process-snooze.ts +2 -1
  64. package/src/endpoints/resend-notification.ts +4 -4
  65. package/src/endpoints/send-reminder.ts +4 -4
  66. package/src/endpoints/ticket-synthesis.ts +17 -2
  67. package/src/endpoints/track-open.ts +44 -21
  68. package/src/endpoints/transfer-ticket.ts +4 -4
  69. package/src/index.ts +7 -0
  70. package/src/plugin.ts +16 -1
  71. package/src/portal/LiveChat.tsx +1 -1
  72. package/src/portal/auth/ChatWidget.tsx +4 -4
  73. package/src/portal/auth/ChatbotWidget.tsx +4 -4
  74. package/src/portal/auth/dashboard/DashboardClient.tsx +2 -2
  75. package/src/portal/auth/faq/FAQSearch.tsx +1 -1
  76. package/src/portal/auth/faq/page.tsx +2 -2
  77. package/src/portal/auth/profile/page.tsx +17 -17
  78. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +1 -1
  79. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +1 -1
  80. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +1 -1
  81. package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +3 -3
  82. package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +3 -3
  83. package/src/portal/auth/tickets/detail/page.tsx +5 -5
  84. package/src/portal/auth/tickets/new/page.tsx +8 -8
  85. package/src/portal/forgot-password/page.tsx +3 -3
  86. package/src/portal/layout.tsx +5 -2
  87. package/src/portal/login/page.tsx +7 -7
  88. package/src/portal/page.tsx +9 -9
  89. package/src/portal/register/page.tsx +4 -4
  90. package/src/portal/reset-password/page.tsx +3 -3
  91. package/src/styles/ChatView.module.scss +1 -1
  92. package/src/styles/TicketDetail.module.scss +3 -3
  93. package/src/styles/_tokens.scss +1 -1
  94. package/src/styles/theme.css +6 -6
  95. package/src/types.ts +71 -0
  96. package/src/utils/rateLimiter.ts +131 -38
  97. package/src/utils/slugs.ts +4 -0
  98. package/src/utils/webhookSecurity.ts +81 -0
  99. package/src/views/TicketingSettingsView/client.tsx +3 -3
package/src/plugin.ts CHANGED
@@ -27,7 +27,10 @@ import {
27
27
  createAutomationRulesCollection,
28
28
  createSupportTeamCollection,
29
29
  createPushSubscriptionCollection,
30
+ createSupportRateLimitsCollection,
31
+ createSupportCountersCollection,
30
32
  } from './collections'
33
+ import { PayloadRateLimitStore } from './utils/rateLimiter'
31
34
 
32
35
  function viewConfig(component: string, path: string): AdminViewConfig {
33
36
  return { Component: component, path: path as `/${string}` }
@@ -68,6 +71,9 @@ export function supportPlugin(config?: SupportPluginConfig): Plugin {
68
71
  ...config?.collectionSlugs,
69
72
  users: config?.userCollectionSlug || 'users',
70
73
  })
74
+ const rateLimitStore = config?.rateLimitStore === 'payload'
75
+ ? new PayloadRateLimitStore(slugs.rateLimits)
76
+ : config?.rateLimitStore
71
77
 
72
78
  return (incomingConfig: Config): Config => {
73
79
  const existingCollections = incomingConfig.collections || []
@@ -80,14 +86,17 @@ export function supportPlugin(config?: SupportPluginConfig): Plugin {
80
86
  projectCollectionSlug: config?.projectCollectionSlug,
81
87
  documentsCollectionSlug: config?.documentsCollectionSlug,
82
88
  notificationSlug: config?.notificationSlug,
89
+ ticketNumber: config?.ticketNumber,
90
+ capabilities: config?.capabilities,
83
91
  }
84
92
  const messageOptions = {
85
93
  notificationSlug: config?.notificationSlug,
94
+ capabilities: config?.capabilities,
86
95
  }
87
96
  const supportCollections = [
88
97
  createTicketsCollection(slugs, ticketOptions),
89
98
  createTicketMessagesCollection(slugs, messageOptions),
90
- createSupportClientsCollection(slugs),
99
+ createSupportClientsCollection(slugs, { capabilities: config?.capabilities }),
91
100
  createCannedResponsesCollection(slugs),
92
101
  createTicketActivityLogCollection(slugs),
93
102
  createSatisfactionSurveysCollection(slugs),
@@ -98,7 +107,11 @@ export function supportPlugin(config?: SupportPluginConfig): Plugin {
98
107
  createAutomationRulesCollection(slugs),
99
108
  createSupportTeamCollection(slugs),
100
109
  createPushSubscriptionCollection(slugs),
110
+ createSupportCountersCollection(slugs.counters),
101
111
  ]
112
+ if (config?.rateLimitStore === 'payload') {
113
+ supportCollections.push(createSupportRateLimitsCollection(slugs.rateLimits))
114
+ }
102
115
 
103
116
  // Auth logs (conditional)
104
117
  if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs))
@@ -150,6 +163,8 @@ export function supportPlugin(config?: SupportPluginConfig): Plugin {
150
163
  const supportEndpoints = createSupportEndpoints(slugs, {
151
164
  oauth: { allowedEmailDomains: config?.allowedEmailDomains },
152
165
  features,
166
+ rateLimitStore,
167
+ capabilities: config?.capabilities,
153
168
  })
154
169
 
155
170
  return {
@@ -441,7 +441,7 @@ export function LiveChat() {
441
441
  width: 380,
442
442
  maxWidth: 'calc(100vw - 32px)',
443
443
  height: screen === 'identify' || screen === 'rating' ? 'auto' : 520,
444
- maxHeight: 'calc(100vh - 32px)',
444
+ maxHeight: 'calc(100dvh - 32px)',
445
445
  borderRadius: 16,
446
446
  border: '3px solid #000',
447
447
  backgroundColor: '#fff',
@@ -215,7 +215,7 @@ function ChatWidgetInner() {
215
215
  {!isOpen && (
216
216
  <button
217
217
  onClick={() => setIsOpen(true)}
218
- className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full border-3 border-black bg-[#00E5FF] shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] dark:border-gray-600 dark:shadow-[4px_4px_0px_#333]"
218
+ className="fixed bottom-6 right-6 z-50 flex h-14 w-14 items-center justify-center rounded-full border-3 border-black bg-[#00E5FF] shadow-[4px_4px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] dark:border-gray-600 dark:shadow-[4px_4px_0px_#333]"
219
219
  title="Chat en direct"
220
220
  >
221
221
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-6 w-6">
@@ -274,7 +274,7 @@ function ChatWidgetInner() {
274
274
  </p>
275
275
  <button
276
276
  onClick={startChat}
277
- className="rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-sm font-black text-black shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
277
+ className="rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-sm font-black text-black shadow-[4px_4px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
278
278
  >
279
279
  Démarrer le chat
280
280
  </button>
@@ -320,7 +320,7 @@ function ChatWidgetInner() {
320
320
  <p className="mb-3 text-sm text-gray-500 dark:text-gray-400">Le chat est terminé.</p>
321
321
  <button
322
322
  onClick={resetChat}
323
- className="rounded-xl border-2 border-black bg-[#FFD600] px-4 py-2 text-xs font-bold text-black shadow-[2px_2px_0px_#000] transition-all hover:translate-x-[1px] hover:translate-y-[1px] hover:shadow-[1px_1px_0px_#000]"
323
+ className="rounded-xl border-2 border-black bg-[#FFD600] px-4 py-2 text-xs font-bold text-black shadow-[2px_2px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[1px] hover:translate-y-[1px] hover:shadow-[1px_1px_0px_#000]"
324
324
  >
325
325
  Nouveau chat
326
326
  </button>
@@ -344,7 +344,7 @@ function ChatWidgetInner() {
344
344
  <button
345
345
  type="submit"
346
346
  disabled={!input.trim() || sending}
347
- className="rounded-xl border-2 border-black bg-[#00E5FF] px-3 py-2 font-bold text-black transition-all hover:shadow-[2px_2px_0px_#000] disabled:opacity-50 dark:border-gray-600"
347
+ className="rounded-xl border-2 border-black bg-[#00E5FF] px-3 py-2 font-bold text-black transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:shadow-[2px_2px_0px_#000] disabled:opacity-50 dark:border-gray-600"
348
348
  >
349
349
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
350
350
  <line x1="22" y1="2" x2="11" y2="13" />
@@ -106,7 +106,7 @@ export function ChatbotWidget() {
106
106
  {!isOpen && (
107
107
  <button
108
108
  onClick={() => setIsOpen(true)}
109
- className="fixed bottom-24 right-6 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition-all hover:bg-blue-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
109
+ className="fixed bottom-24 right-6 z-40 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-xl focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600"
110
110
  aria-label="Ouvrir l'assistant IA"
111
111
  title="Assistant IA"
112
112
  >
@@ -129,7 +129,7 @@ export function ChatbotWidget() {
129
129
  {/* Chat panel */}
130
130
  {isOpen && (
131
131
  <div
132
- className="fixed bottom-24 right-6 z-40 flex w-[360px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl transition-all dark:border-gray-700 dark:bg-gray-900"
132
+ className="fixed bottom-24 right-6 z-40 flex w-[360px] max-w-[calc(100vw-2rem)] flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none dark:border-gray-700 dark:bg-gray-900"
133
133
  style={{ height: '500px' }}
134
134
  role="dialog"
135
135
  aria-label="Assistant Support"
@@ -254,12 +254,12 @@ export function ChatbotWidget() {
254
254
  placeholder="Posez votre question..."
255
255
  maxLength={500}
256
256
  disabled={loading}
257
- className="flex-1 rounded-xl border border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 outline-none transition-all placeholder:text-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500 dark:focus:border-blue-400"
257
+ className="flex-1 rounded-xl border border-gray-300 bg-white px-3.5 py-2.5 text-sm text-gray-900 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none placeholder:text-gray-400 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 disabled:opacity-50 dark:border-gray-600 dark:bg-gray-800 dark:text-white dark:placeholder:text-gray-500 dark:focus:border-blue-400"
258
258
  />
259
259
  <button
260
260
  type="submit"
261
261
  disabled={!input.trim() || loading}
262
- className="flex h-[42px] w-[42px] flex-shrink-0 items-center justify-center rounded-xl bg-blue-600 text-white transition-all hover:bg-blue-700 disabled:opacity-40 disabled:hover:bg-blue-600 dark:bg-blue-500 dark:hover:bg-blue-600"
262
+ className="flex h-[42px] w-[42px] flex-shrink-0 items-center justify-center rounded-xl bg-blue-600 text-white transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 disabled:opacity-40 disabled:hover:bg-blue-600 dark:bg-blue-500 dark:hover:bg-blue-600"
263
263
  aria-label="Envoyer"
264
264
  >
265
265
  <svg
@@ -224,7 +224,7 @@ export function DashboardClient({ tickets }: { tickets: TicketData[] }) {
224
224
  </div>
225
225
  <Link
226
226
  href="/support/tickets/new"
227
- className="inline-flex flex-shrink-0 items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md active:scale-[0.98]"
227
+ className="inline-flex flex-shrink-0 items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-md active:scale-[0.98]"
228
228
  >
229
229
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
230
230
  <line x1="12" y1="5" x2="12" y2="19" />
@@ -447,7 +447,7 @@ export function DashboardClient({ tickets }: { tickets: TicketData[] }) {
447
447
  {!searchQuery && tab === 'active' && (
448
448
  <Link
449
449
  href="/support/tickets/new"
450
- className="mt-5 inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700"
450
+ className="mt-5 inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700"
451
451
  >
452
452
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
453
453
  <line x1="12" y1="5" x2="12" y2="19" />
@@ -24,7 +24,7 @@ export function FAQSearch({ articles }: { articles: Article[] }) {
24
24
  return (
25
25
  <div className="relative mb-10">
26
26
  {/* Search input */}
27
- <div className={`relative rounded-2xl border bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm transition-all duration-200 ${
27
+ <div className={`relative rounded-2xl border bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 ${
28
28
  isFocused
29
29
  ? 'border-blue-500 ring-4 ring-blue-500/10 shadow-md'
30
30
  : 'border-slate-200 dark:border-slate-700/50 hover:border-slate-300 dark:hover:border-slate-600'
@@ -28,7 +28,7 @@ const categoryConfig: Record<string, { label: string; icon: React.ReactNode; col
28
28
  },
29
29
  account: {
30
30
  label: 'Compte & Profil',
31
- color: 'bg-violet-50 dark:bg-violet-900/20 text-violet-600 dark:text-violet-400',
31
+ color: 'bg-teal-50 dark:bg-teal-900/20 text-teal-700 dark:text-teal-300',
32
32
  icon: (
33
33
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-5 w-5">
34
34
  <path d="M10 8a3 3 0 100-6 3 3 0 000 6zM3.465 14.493a1.23 1.23 0 00.41 1.412A9.957 9.957 0 0010 18c2.31 0 4.438-.784 6.131-2.1.43-.333.604-.903.408-1.41a7.002 7.002 0 00-13.074.003z" />
@@ -153,7 +153,7 @@ export default async function FAQPage() {
153
153
  {items.map((article) => (
154
154
  <details
155
155
  key={article.id}
156
- className="group rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50 shadow-sm transition-all duration-200 hover:border-slate-300 dark:hover:border-slate-600 open:shadow-md"
156
+ className="group rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:border-slate-300 dark:hover:border-slate-600 open:shadow-md"
157
157
  >
158
158
  <summary className="flex cursor-pointer items-center justify-between gap-4 px-5 py-4 text-sm font-semibold text-slate-900 dark:text-white list-none [&::-webkit-details-marker]:hidden">
159
159
  <span className="leading-relaxed">{article.title}</span>
@@ -254,7 +254,7 @@ export default function ProfilePage() {
254
254
  required
255
255
  value={profile.firstName}
256
256
  onChange={(e) => setProfile({ ...profile, firstName: e.target.value })}
257
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
257
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
258
258
  />
259
259
  </div>
260
260
  <div>
@@ -267,7 +267,7 @@ export default function ProfilePage() {
267
267
  required
268
268
  value={profile.lastName}
269
269
  onChange={(e) => setProfile({ ...profile, lastName: e.target.value })}
270
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
270
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
271
271
  />
272
272
  </div>
273
273
  </div>
@@ -283,7 +283,7 @@ export default function ProfilePage() {
283
283
  required
284
284
  value={profile.company}
285
285
  onChange={(e) => setProfile({ ...profile, company: e.target.value })}
286
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
286
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
287
287
  />
288
288
  </div>
289
289
 
@@ -298,7 +298,7 @@ export default function ProfilePage() {
298
298
  value={profile.phone}
299
299
  onChange={(e) => setProfile({ ...profile, phone: e.target.value })}
300
300
  placeholder="+33 6 00 00 00 00"
301
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
301
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
302
302
  />
303
303
  </div>
304
304
 
@@ -317,7 +317,7 @@ export default function ProfilePage() {
317
317
  <button
318
318
  type="submit"
319
319
  disabled={saving}
320
- className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
320
+ className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
321
321
  >
322
322
  {saving ? (
323
323
  <>
@@ -384,8 +384,8 @@ export default function ProfilePage() {
384
384
  <div className="rounded-2xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm">
385
385
  <div className="border-b border-slate-100 dark:border-slate-700/50 px-6 py-4 sm:px-8">
386
386
  <div className="flex items-center gap-3">
387
- <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-violet-50 dark:bg-violet-900/30">
388
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-violet-600 dark:text-violet-400">
387
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-teal-50 dark:bg-teal-900/30">
388
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4 text-teal-700 dark:text-teal-300">
389
389
  <path fillRule="evenodd" d="M10 2a6 6 0 00-6 6c0 1.887-.454 3.665-1.257 5.234a.75.75 0 00.515 1.076 32.91 32.91 0 003.256.508 3.5 3.5 0 006.972 0 32.903 32.903 0 003.256-.508.75.75 0 00.515-1.076A11.448 11.448 0 0116 8a6 6 0 00-6-6zM8.05 14.943a33.54 33.54 0 003.9 0 2 2 0 01-3.9 0z" clipRule="evenodd" />
390
390
  </svg>
391
391
  </div>
@@ -443,7 +443,7 @@ export default function ProfilePage() {
443
443
  type="button"
444
444
  onClick={handleSaveProfile}
445
445
  disabled={saving}
446
- className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-2.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
446
+ className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-6 py-2.5 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
447
447
  >
448
448
  {saving ? 'Enregistrement...' : 'Sauvegarder les preferences'}
449
449
  </button>
@@ -491,7 +491,7 @@ export default function ProfilePage() {
491
491
  required
492
492
  value={currentPassword}
493
493
  onChange={(e) => setCurrentPassword(e.target.value)}
494
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
494
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
495
495
  />
496
496
  </div>
497
497
 
@@ -506,7 +506,7 @@ export default function ProfilePage() {
506
506
  minLength={8}
507
507
  value={newPassword}
508
508
  onChange={(e) => setNewPassword(e.target.value)}
509
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
509
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
510
510
  />
511
511
  <p className="mt-1.5 text-xs text-slate-400 dark:text-slate-500">Minimum 8 caracteres</p>
512
512
  </div>
@@ -521,14 +521,14 @@ export default function ProfilePage() {
521
521
  required
522
522
  value={confirmPassword}
523
523
  onChange={(e) => setConfirmPassword(e.target.value)}
524
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
524
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
525
525
  />
526
526
  </div>
527
527
 
528
528
  <button
529
529
  type="submit"
530
530
  disabled={changingPassword}
531
- className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 dark:bg-slate-100 px-6 py-3 text-sm font-semibold text-white dark:text-slate-900 shadow-sm transition-all duration-200 hover:bg-slate-800 dark:hover:bg-white hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
531
+ className="inline-flex items-center justify-center gap-2 rounded-xl bg-slate-900 dark:bg-slate-100 px-6 py-3 text-sm font-semibold text-white dark:text-slate-900 shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-slate-800 dark:hover:bg-white hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
532
532
  >
533
533
  {changingPassword ? (
534
534
  <>
@@ -567,7 +567,7 @@ export default function ProfilePage() {
567
567
  <a
568
568
  href="/api/support/export-data"
569
569
  download
570
- className="inline-flex items-center gap-2 rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-5 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-300 transition-all duration-200 hover:border-emerald-300 dark:hover:border-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 hover:text-emerald-700 dark:hover:text-emerald-400"
570
+ className="inline-flex items-center gap-2 rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-5 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:border-emerald-300 dark:hover:border-emerald-600 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 hover:text-emerald-700 dark:hover:text-emerald-400"
571
571
  >
572
572
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
573
573
  <path d="M10.75 2.75a.75.75 0 00-1.5 0v8.614L6.295 8.235a.75.75 0 10-1.09 1.03l4.25 4.5a.75.75 0 001.09 0l4.25-4.5a.75.75 0 00-1.09-1.03l-2.955 3.129V2.75z" />
@@ -603,7 +603,7 @@ export default function ProfilePage() {
603
603
  {!showDeleteConfirm ? (
604
604
  <button
605
605
  onClick={() => setShowDeleteConfirm(true)}
606
- className="inline-flex items-center gap-2 rounded-xl border border-red-200 dark:border-red-800/50 bg-white dark:bg-slate-800 px-5 py-2.5 text-sm font-semibold text-red-600 dark:text-red-400 transition-all duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 hover:border-red-300"
606
+ className="inline-flex items-center gap-2 rounded-xl border border-red-200 dark:border-red-800/50 bg-white dark:bg-slate-800 px-5 py-2.5 text-sm font-semibold text-red-600 dark:text-red-400 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-red-50 dark:hover:bg-red-900/20 hover:border-red-300"
607
607
  >
608
608
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
609
609
  <path fillRule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clipRule="evenodd" />
@@ -651,7 +651,7 @@ export default function ProfilePage() {
651
651
  value={deletePassword}
652
652
  onChange={(e) => setDeletePassword(e.target.value)}
653
653
  placeholder="Votre mot de passe"
654
- className="w-full rounded-xl border border-red-200 dark:border-red-700 bg-white dark:bg-slate-900 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 outline-none transition-all focus:border-red-500 focus:ring-4 focus:ring-red-500/10"
654
+ className="w-full rounded-xl border border-red-200 dark:border-red-700 bg-white dark:bg-slate-900 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none focus:border-red-500 focus:ring-4 focus:ring-red-500/10"
655
655
  />
656
656
  </div>
657
657
 
@@ -684,13 +684,13 @@ export default function ProfilePage() {
684
684
  }
685
685
  }}
686
686
  disabled={deleting || !deletePassword}
687
- className="rounded-xl bg-red-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-red-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
687
+ className="rounded-xl bg-red-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-red-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
688
688
  >
689
689
  {deleting ? 'Suppression...' : 'Confirmer la suppression'}
690
690
  </button>
691
691
  <button
692
692
  onClick={() => { setShowDeleteConfirm(false); setDeletePassword(''); setDeleteError('') }}
693
- className="rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-5 py-2.5 text-sm font-semibold text-slate-600 dark:text-slate-300 transition-all duration-200 hover:bg-slate-50 dark:hover:bg-slate-700"
693
+ className="rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-5 py-2.5 text-sm font-semibold text-slate-600 dark:text-slate-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-slate-50 dark:hover:bg-slate-700"
694
694
  >
695
695
  Annuler
696
696
  </button>
@@ -47,7 +47,7 @@ export function CloseTicketButton({ ticketId }: { ticketId: number | string }) {
47
47
  <button
48
48
  onClick={handleClose}
49
49
  disabled={loading}
50
- className={`inline-flex items-center gap-1.5 rounded-xl border px-3.5 py-2 text-sm font-semibold transition-all disabled:opacity-50 ${
50
+ className={`inline-flex items-center gap-1.5 rounded-xl border px-3.5 py-2 text-sm font-semibold transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none disabled:opacity-50 ${
51
51
  confirming
52
52
  ? 'border-red-200 dark:border-red-800/50 bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30'
53
53
  : 'border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/30'
@@ -42,7 +42,7 @@ export function MarkSolutionButton({
42
42
  <button
43
43
  onClick={handleToggle}
44
44
  disabled={loading}
45
- className={`mt-2 flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-bold transition-all ${
45
+ className={`mt-2 flex items-center gap-1 rounded-lg px-2 py-1 text-xs font-bold transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none ${
46
46
  isSolution
47
47
  ? 'border-2 border-green-500 bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400'
48
48
  : 'border-2 border-gray-200 text-gray-400 hover:border-green-500 hover:text-green-600 dark:border-gray-600 dark:hover:border-green-500'
@@ -47,7 +47,7 @@ export function ReopenTicketButton({ ticketId }: { ticketId: number | string })
47
47
  <button
48
48
  onClick={handleReopen}
49
49
  disabled={loading}
50
- className={`inline-flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-semibold transition-all disabled:opacity-50 ${
50
+ className={`inline-flex items-center gap-2 rounded-xl border px-4 py-2.5 text-sm font-semibold transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none disabled:opacity-50 ${
51
51
  confirming
52
52
  ? 'border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900/30'
53
53
  : 'border-blue-200 dark:border-blue-800/50 bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900/30'
@@ -97,7 +97,7 @@ export function SatisfactionForm({ ticketId }: { ticketId: number | string }) {
97
97
  onClick={() => setRating(star)}
98
98
  onMouseEnter={() => setHoveredRating(star)}
99
99
  onMouseLeave={() => setHoveredRating(0)}
100
- className="rounded-lg p-1 transition-all hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400"
100
+ className="rounded-lg p-1 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400"
101
101
  >
102
102
  <svg
103
103
  className={`h-8 w-8 transition-colors ${
@@ -131,13 +131,13 @@ export function SatisfactionForm({ ticketId }: { ticketId: number | string }) {
131
131
  value={comment}
132
132
  onChange={(e) => setComment(e.target.value)}
133
133
  placeholder="Un commentaire ? (optionnel)"
134
- className="mb-4 w-full resize-y rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-3 text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 outline-none transition-all focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-400/20 dark:focus:ring-blue-500/20"
134
+ className="mb-4 w-full resize-y rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-3 text-sm text-slate-900 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none focus:border-blue-400 dark:focus:border-blue-500 focus:ring-2 focus:ring-blue-400/20 dark:focus:ring-blue-500/20"
135
135
  />
136
136
 
137
137
  <button
138
138
  type="submit"
139
139
  disabled={loading || rating === 0}
140
- className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed"
140
+ className="inline-flex items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed"
141
141
  >
142
142
  {loading ? (
143
143
  <>
@@ -154,7 +154,7 @@ export function TicketReplyForm({ ticketId }: { ticketId: number | string }) {
154
154
  onDragOver={handleDragOver}
155
155
  onDragLeave={handleDragLeave}
156
156
  onDrop={handleDrop}
157
- className={`rounded-xl transition-all ${
157
+ className={`rounded-xl transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none ${
158
158
  isDragging
159
159
  ? 'bg-blue-50/50 dark:bg-blue-900/20 ring-2 ring-blue-400/20'
160
160
  : ''
@@ -193,7 +193,7 @@ export function TicketReplyForm({ ticketId }: { ticketId: number | string }) {
193
193
  onChange={(e) => { setBody(e.target.value); setBodyHtml(''); sendTyping() }}
194
194
  placeholder="Écrivez votre message..."
195
195
  rows={5}
196
- className="w-full resize-y rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-4 py-3 text-sm leading-relaxed text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
196
+ className="w-full resize-y rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 px-4 py-3 text-sm leading-relaxed text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
197
197
  />
198
198
  </div>
199
199
 
@@ -222,7 +222,7 @@ export function TicketReplyForm({ ticketId }: { ticketId: number | string }) {
222
222
  <button
223
223
  type="submit"
224
224
  disabled={loading || (!body.trim() && !bodyHtml)}
225
- className="ml-auto inline-flex items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-all hover:bg-blue-700 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:shadow-sm"
225
+ className="ml-auto inline-flex items-center gap-2 rounded-xl bg-blue-600 px-5 py-2.5 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-blue-700 hover:shadow-md disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:shadow-sm"
226
226
  >
227
227
  {loading ? (
228
228
  <>
@@ -257,7 +257,7 @@ export default async function TicketDetailPage({ params }: { params: Promise<{ i
257
257
  href={`${process.env.NEXT_PUBLIC_SERVER_URL || ''}/api/documents/file/${quoteDoc.filename}`}
258
258
  target="_blank"
259
259
  rel="noopener noreferrer"
260
- className="flex items-center gap-2 rounded-xl border border-amber-200 dark:border-amber-800/50 bg-amber-50 dark:bg-amber-900/20 px-3 py-2 text-sm font-medium text-amber-800 dark:text-amber-300 transition-all hover:bg-amber-100 dark:hover:bg-amber-900/30"
260
+ className="flex items-center gap-2 rounded-xl border border-amber-200 dark:border-amber-800/50 bg-amber-50 dark:bg-amber-900/20 px-3 py-2 text-sm font-medium text-amber-800 dark:text-amber-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-amber-100 dark:hover:bg-amber-900/30"
261
261
  >
262
262
  <svg className="h-4 w-4 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
263
263
  <path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -270,7 +270,7 @@ export default async function TicketDetailPage({ params }: { params: Promise<{ i
270
270
  href={`${process.env.NEXT_PUBLIC_SERVER_URL || ''}/api/documents/file/${invoiceDoc.filename}`}
271
271
  target="_blank"
272
272
  rel="noopener noreferrer"
273
- className="flex items-center gap-2 rounded-xl border border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 px-3 py-2 text-sm font-medium text-emerald-800 dark:text-emerald-300 transition-all hover:bg-emerald-100 dark:hover:bg-emerald-900/30"
273
+ className="flex items-center gap-2 rounded-xl border border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 px-3 py-2 text-sm font-medium text-emerald-800 dark:text-emerald-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-emerald-100 dark:hover:bg-emerald-900/30"
274
274
  >
275
275
  <svg className="h-4 w-4 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
276
276
  <path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -499,7 +499,7 @@ export default async function TicketDetailPage({ params }: { params: Promise<{ i
499
499
  href={`${process.env.NEXT_PUBLIC_SERVER_URL || ''}/api/documents/file/${quoteDoc.filename}`}
500
500
  target="_blank"
501
501
  rel="noopener noreferrer"
502
- className="flex items-center gap-3 rounded-xl border border-amber-200 dark:border-amber-800/50 bg-amber-50 dark:bg-amber-900/20 px-4 py-3 text-sm font-medium text-amber-800 dark:text-amber-300 transition-all hover:bg-amber-100 dark:hover:bg-amber-900/30 hover:shadow-sm"
502
+ className="flex items-center gap-3 rounded-xl border border-amber-200 dark:border-amber-800/50 bg-amber-50 dark:bg-amber-900/20 px-4 py-3 text-sm font-medium text-amber-800 dark:text-amber-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-amber-100 dark:hover:bg-amber-900/30 hover:shadow-sm"
503
503
  >
504
504
  <svg className="h-5 w-5 text-amber-600 dark:text-amber-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
505
505
  <path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -518,7 +518,7 @@ export default async function TicketDetailPage({ params }: { params: Promise<{ i
518
518
  href={`${process.env.NEXT_PUBLIC_SERVER_URL || ''}/api/documents/file/${invoiceDoc.filename}`}
519
519
  target="_blank"
520
520
  rel="noopener noreferrer"
521
- className="flex items-center gap-3 rounded-xl border border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 px-4 py-3 text-sm font-medium text-emerald-800 dark:text-emerald-300 transition-all hover:bg-emerald-100 dark:hover:bg-emerald-900/30 hover:shadow-sm"
521
+ className="flex items-center gap-3 rounded-xl border border-emerald-200 dark:border-emerald-800/50 bg-emerald-50 dark:bg-emerald-900/20 px-4 py-3 text-sm font-medium text-emerald-800 dark:text-emerald-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-emerald-100 dark:hover:bg-emerald-900/30 hover:shadow-sm"
522
522
  >
523
523
  <svg className="h-5 w-5 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
524
524
  <path strokeLinecap="round" strokeLinejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
@@ -811,7 +811,7 @@ export default async function TicketDetailPage({ params }: { params: Promise<{ i
811
811
  <ReopenTicketButton ticketId={ticket.id} />
812
812
  <Link
813
813
  href="/support/tickets/new"
814
- className="inline-flex items-center gap-2 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-300 transition-all hover:bg-slate-50 dark:hover:bg-slate-700 hover:shadow-sm"
814
+ className="inline-flex items-center gap-2 rounded-xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 px-4 py-2.5 text-sm font-semibold text-slate-700 dark:text-slate-300 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:bg-slate-50 dark:hover:bg-slate-700 hover:shadow-sm"
815
815
  >
816
816
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-4 w-4">
817
817
  <line x1="12" y1="5" x2="12" y2="19" />
@@ -317,7 +317,7 @@ export default function NewTicketPage() {
317
317
  )}
318
318
 
319
319
  {/* Main card */}
320
- <div className={`rounded-2xl border bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm transition-all duration-200 ${
320
+ <div className={`rounded-2xl border bg-white dark:bg-slate-800/50 shadow-sm backdrop-blur-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 ${
321
321
  isDragging
322
322
  ? 'border-blue-400 dark:border-blue-500 ring-4 ring-blue-100 dark:ring-blue-900/30 shadow-blue-100/50'
323
323
  : 'border-slate-200 dark:border-slate-700/50'
@@ -335,7 +335,7 @@ export default function NewTicketPage() {
335
335
  value={subject}
336
336
  onChange={(e) => setSubject(e.target.value)}
337
337
  placeholder="Ex: Erreur 404 sur la page contact"
338
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
338
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
339
339
  />
340
340
  {/* KB deflection — suggest existing articles before ticket creation */}
341
341
  <KbDeflection query={subject} limit={3} />
@@ -353,7 +353,7 @@ export default function NewTicketPage() {
353
353
  value={message}
354
354
  onChange={(e) => setMessage(e.target.value)}
355
355
  placeholder="Decrivez votre probleme en detail. Plus vous donnez d'informations, plus nous pourrons vous aider rapidement."
356
- className="w-full resize-y rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm leading-relaxed text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-all duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
356
+ className="w-full resize-y rounded-xl border border-slate-200 dark:border-slate-600 bg-slate-50 dark:bg-slate-900/50 px-4 py-3 text-sm leading-relaxed text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500 outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 focus:border-blue-500 focus:bg-white dark:focus:bg-slate-900 focus:ring-4 focus:ring-blue-500/10"
357
357
  />
358
358
  </div>
359
359
 
@@ -385,7 +385,7 @@ export default function NewTicketPage() {
385
385
  <button
386
386
  type="button"
387
387
  onClick={() => fileInputRef.current?.click()}
388
- className="group flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-600 bg-slate-50/50 dark:bg-slate-900/30 px-6 py-4 transition-all duration-200 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50/50 dark:hover:bg-blue-900/10"
388
+ className="group flex w-full items-center justify-center gap-3 rounded-xl border-2 border-dashed border-slate-200 dark:border-slate-600 bg-slate-50/50 dark:bg-slate-900/30 px-6 py-4 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:border-blue-300 dark:hover:border-blue-600 hover:bg-blue-50/50 dark:hover:bg-blue-900/10"
389
389
  >
390
390
  <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-slate-100 dark:bg-slate-700 transition-colors group-hover:bg-blue-100 dark:group-hover:bg-blue-900/40">
391
391
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="h-5 w-5 text-slate-400 group-hover:text-blue-500 transition-colors">
@@ -461,7 +461,7 @@ export default function NewTicketPage() {
461
461
  return (
462
462
  <label
463
463
  key={cat.value}
464
- className={`group relative flex cursor-pointer flex-col items-start gap-1 rounded-xl border p-3 text-left transition-all ${
464
+ className={`group relative flex cursor-pointer flex-col items-start gap-1 rounded-xl border p-3 text-left transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none ${
465
465
  isActive
466
466
  ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500/20 dark:border-blue-400 dark:bg-blue-950/40'
467
467
  : 'border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:border-slate-600 dark:hover:bg-slate-800'
@@ -509,7 +509,7 @@ export default function NewTicketPage() {
509
509
  return (
510
510
  <label
511
511
  key={p.value}
512
- className={`flex cursor-pointer items-start gap-3 rounded-xl border p-3 transition-all ${
512
+ className={`flex cursor-pointer items-start gap-3 rounded-xl border p-3 transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none ${
513
513
  isActive
514
514
  ? 'border-blue-500 bg-blue-50 ring-2 ring-blue-500/20 dark:border-blue-400 dark:bg-blue-950/40'
515
515
  : 'border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:border-slate-600 dark:hover:bg-slate-800'
@@ -568,7 +568,7 @@ export default function NewTicketPage() {
568
568
  id="project"
569
569
  value={project}
570
570
  onChange={(e) => setProject(e.target.value)}
571
- className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-all focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
571
+ className="w-full rounded-xl border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2.5 text-sm text-slate-900 dark:text-white outline-none transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none focus:border-blue-500 focus:ring-4 focus:ring-blue-500/10"
572
572
  >
573
573
  <option value="">Aucun projet specifique</option>
574
574
  {projects.map((p) => (
@@ -590,7 +590,7 @@ export default function NewTicketPage() {
590
590
  <button
591
591
  type="submit"
592
592
  disabled={loading}
593
- className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-8 py-3 text-sm font-semibold text-white shadow-sm transition-all duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
593
+ className="inline-flex items-center justify-center gap-2 rounded-xl bg-blue-600 px-8 py-3 text-sm font-semibold text-white shadow-sm transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none duration-200 hover:bg-blue-700 hover:shadow-md active:scale-[0.98] disabled:opacity-50 disabled:cursor-not-allowed"
594
594
  >
595
595
  {loading ? (
596
596
  <>
@@ -35,7 +35,7 @@ export default function ForgotPasswordPage() {
35
35
  }
36
36
 
37
37
  return (
38
- <div className="flex min-h-screen items-center justify-center px-4 py-12">
38
+ <div className="flex min-h-[100dvh] items-center justify-center px-4 py-12">
39
39
  <div className="w-full max-w-md">
40
40
  <div className="mb-8 text-center">
41
41
  <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-2xl border-4 border-black bg-[#FFD600] shadow-[4px_4px_0px_#000]">
@@ -58,7 +58,7 @@ export default function ForgotPasswordPage() {
58
58
  </p>
59
59
  <Link
60
60
  href="/support/login"
61
- className="block w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-center text-base font-black text-black shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
61
+ className="block w-full rounded-xl border-3 border-black bg-[#00E5FF] px-6 py-3 text-center text-base font-black text-black shadow-[4px_4px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000]"
62
62
  >
63
63
  Retour à la connexion
64
64
  </Link>
@@ -96,7 +96,7 @@ export default function ForgotPasswordPage() {
96
96
  <button
97
97
  type="submit"
98
98
  disabled={loading}
99
- className="w-full rounded-xl border-3 border-black bg-[#FFD600] px-6 py-3 text-base font-black text-black shadow-[4px_4px_0px_#000] transition-all hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] disabled:opacity-50"
99
+ className="w-full rounded-xl border-3 border-black bg-[#FFD600] px-6 py-3 text-base font-black text-black shadow-[4px_4px_0px_#000] transition-[color,background-color,border-color,box-shadow,transform] motion-reduce:transition-none hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_#000] disabled:opacity-50"
100
100
  >
101
101
  {loading ? 'Envoi...' : 'Envoyer le lien'}
102
102
  </button>