@consilioweb/payload-support 5.0.0 → 6.0.1

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 (120) hide show
  1. package/README.md +249 -22
  2. package/dist/components/RichTextEditor/index.js +1 -1
  3. package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
  4. package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
  5. package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
  6. package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
  7. package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
  8. package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +10 -8
  10. package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
  11. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
  12. package/dist/components/TicketConversation/index.d.ts +10 -0
  13. package/dist/components/TicketConversation/index.js +25 -7
  14. package/dist/index.cjs +306 -60
  15. package/dist/index.d.cts +107 -38
  16. package/dist/index.d.ts +107 -38
  17. package/dist/index.js +300 -59
  18. package/dist/styles/BillingView.module.scss +11 -11
  19. package/dist/styles/ChatView.module.scss +7 -7
  20. package/dist/styles/CommandPalette.module.scss +1 -1
  21. package/dist/styles/CrmView.module.scss +9 -9
  22. package/dist/styles/EmailTracking.module.scss +6 -6
  23. package/dist/styles/ImportConversation.module.scss +5 -5
  24. package/dist/styles/Logs.module.scss +5 -5
  25. package/dist/styles/NewTicket.module.scss +2 -2
  26. package/dist/styles/PendingEmails.module.scss +13 -13
  27. package/dist/styles/SupportDashboard.module.scss +24 -8
  28. package/dist/styles/TicketDetail.module.scss +20 -20
  29. package/dist/styles/TicketInbox.module.scss +6 -6
  30. package/dist/styles/TicketingSettings.module.scss +10 -10
  31. package/dist/styles/TimeDashboard.module.scss +6 -6
  32. package/dist/styles/_tokens.scss +3 -1
  33. package/dist/utils/db.js +20 -0
  34. package/dist/utils/readSettings.js +150 -0
  35. package/dist/views/BillingView/client.js +15 -8
  36. package/dist/views/ChatView/client.js +2 -0
  37. package/dist/views/CrmView/client.js +2 -0
  38. package/dist/views/EmailTrackingView/client.js +23 -11
  39. package/dist/views/ImportConversationView/client.js +1 -0
  40. package/dist/views/LogsView/client.js +1 -1
  41. package/dist/views/NewTicketView/client.js +20 -13
  42. package/dist/views/PendingEmailsView/client.js +9 -3
  43. package/dist/views/SupportDashboardView/client.js +17 -19
  44. package/dist/views/TicketDetailView/client.js +23 -11
  45. package/dist/views/TicketInboxView/client.js +52 -26
  46. package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  47. package/dist/views/TicketingSettingsView/client.js +44 -26
  48. package/dist/views/TimeDashboardView/client.js +10 -7
  49. package/dist/views/shared/ErrorBoundary.d.ts +36 -1
  50. package/dist/views/shared/ErrorBoundary.js +59 -28
  51. package/dist/views/shared/adminTokens.d.ts +15 -5
  52. package/dist/views/shared/adminTokens.js +23 -7
  53. package/dist/views/shared/icons.d.ts +49 -0
  54. package/dist/views/shared/icons.js +92 -0
  55. package/dist/views/shared/locales/en.json +15 -1
  56. package/dist/views/shared/locales/fr.json +15 -1
  57. package/package.json +9 -4
  58. package/scripts/copy-subpath-types.mjs +103 -0
  59. package/scripts/uninstall-data.mjs +178 -0
  60. package/scripts/uninstall.mjs +184 -0
  61. package/scripts/verify-dist-imports.mjs +84 -0
  62. package/src/collections/Tickets.ts +7 -0
  63. package/src/collections/TimeEntries.ts +11 -3
  64. package/src/components/RichTextEditor/index.tsx +1 -1
  65. package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
  66. package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
  67. package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
  68. package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
  69. package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
  70. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
  71. package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
  72. package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
  73. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
  74. package/src/components/TicketConversation/index.tsx +41 -7
  75. package/src/endpoints/delete-account.ts +191 -47
  76. package/src/endpoints/export-data.ts +104 -4
  77. package/src/endpoints/purge-logs.ts +6 -15
  78. package/src/index.ts +2 -0
  79. package/src/plugin.ts +51 -1
  80. package/src/portal/LiveChat.tsx +1 -1
  81. package/src/portal/auth/profile/page.tsx +3 -2
  82. package/src/portal/icons.tsx +91 -0
  83. package/src/styles/BillingView.module.scss +11 -11
  84. package/src/styles/ChatView.module.scss +7 -7
  85. package/src/styles/CommandPalette.module.scss +1 -1
  86. package/src/styles/CrmView.module.scss +9 -9
  87. package/src/styles/EmailTracking.module.scss +6 -6
  88. package/src/styles/ImportConversation.module.scss +5 -5
  89. package/src/styles/Logs.module.scss +5 -5
  90. package/src/styles/NewTicket.module.scss +2 -2
  91. package/src/styles/PendingEmails.module.scss +13 -13
  92. package/src/styles/SupportDashboard.module.scss +24 -8
  93. package/src/styles/TicketDetail.module.scss +20 -20
  94. package/src/styles/TicketInbox.module.scss +6 -6
  95. package/src/styles/TicketingSettings.module.scss +10 -10
  96. package/src/styles/TimeDashboard.module.scss +6 -6
  97. package/src/styles/_tokens.scss +3 -1
  98. package/src/types.ts +20 -0
  99. package/src/utils/retention.ts +111 -0
  100. package/src/utils/slugs.ts +15 -0
  101. package/src/views/BillingView/client.tsx +17 -8
  102. package/src/views/ChatView/client.tsx +2 -0
  103. package/src/views/CrmView/client.tsx +2 -0
  104. package/src/views/EmailTrackingView/client.tsx +18 -7
  105. package/src/views/ImportConversationView/client.tsx +1 -0
  106. package/src/views/LogsView/client.tsx +1 -1
  107. package/src/views/NewTicketView/client.tsx +22 -13
  108. package/src/views/PendingEmailsView/client.tsx +9 -3
  109. package/src/views/SupportDashboardView/client.tsx +11 -5
  110. package/src/views/TicketDetailView/client.tsx +25 -11
  111. package/src/views/TicketInboxView/client.tsx +47 -7
  112. package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  113. package/src/views/TicketingSettingsView/client.tsx +201 -144
  114. package/src/views/TimeDashboardView/client.tsx +10 -7
  115. package/src/views/shared/ErrorBoundary.tsx +95 -31
  116. package/src/views/shared/adminTokens.ts +28 -11
  117. package/src/views/shared/icons.tsx +146 -0
  118. package/src/views/shared/locales/en.json +15 -1
  119. package/src/views/shared/locales/fr.json +15 -1
  120. package/src/types/lucide-react.d.ts +0 -42
@@ -1,11 +1,21 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
+ import { FIXED_SLUGS } from '../utils/slugs'
3
4
  import { requireClient, handleAuthError } from '../utils/auth'
4
5
  import { dbFind, dbFindByID } from '../utils/db'
5
6
 
7
+ const EMPTY = { docs: [] as any[] }
8
+
6
9
  /**
7
10
  * GET /api/support/export-data
8
11
  * RGPD Data Export — allows support clients to download all their personal data.
12
+ *
13
+ * The payload is split in two. `providedByYou` is what article 20
14
+ * (portability) covers: what the person or their correspondents typed.
15
+ * `derivedData` is what the plugin computed ABOUT them — the AI client
16
+ * summary. Article 20 does not cover derived data; article 15 (access) does,
17
+ * and article 17 (erasure) does too, which is why `delete-account` removes
18
+ * `client-summaries` as well. Merging the two sections would blur that.
9
19
  */
10
20
  export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
11
21
  return {
@@ -17,7 +27,20 @@ export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
17
27
 
18
28
  requireClient(req, slugs)
19
29
 
20
- const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
30
+ // Feature-flagged collections are not registered when their flag is
31
+ // off; addressing one that does not exist throws.
32
+ const isRegistered = (slug: string): boolean =>
33
+ Boolean((payload.collections as Record<string, unknown> | undefined)?.[slug])
34
+
35
+ const [
36
+ clientData,
37
+ ticketsResult,
38
+ messagesResult,
39
+ surveysResult,
40
+ chatResult,
41
+ feedbackResult,
42
+ summariesResult,
43
+ ] = await Promise.all([
21
44
  dbFindByID(payload, slugs.supportClients, {
22
45
  id: req.user.id,
23
46
  depth: 0,
@@ -44,12 +67,46 @@ export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
44
67
  depth: 0,
45
68
  overrideAccess: true,
46
69
  }),
70
+ isRegistered(slugs.chatMessages)
71
+ ? dbFind(payload, slugs.chatMessages, {
72
+ where: { client: { equals: req.user.id } },
73
+ limit: 5000,
74
+ depth: 0,
75
+ overrideAccess: true,
76
+ })
77
+ : EMPTY,
78
+ isRegistered(FIXED_SLUGS.ticketFeedback)
79
+ ? dbFind(payload, FIXED_SLUGS.ticketFeedback, {
80
+ where: { client: { equals: req.user.id } },
81
+ limit: 500,
82
+ depth: 0,
83
+ overrideAccess: true,
84
+ })
85
+ : EMPTY,
86
+ isRegistered(FIXED_SLUGS.clientSummaries)
87
+ ? dbFind(payload, FIXED_SLUGS.clientSummaries, {
88
+ where: { client: { equals: req.user.id } },
89
+ limit: 100,
90
+ depth: 0,
91
+ overrideAccess: true,
92
+ })
93
+ : EMPTY,
47
94
  ])
48
95
 
96
+ const ticketIds = ticketsResult.docs.map((t: any) => t.id)
97
+
98
+ // Time entries hang off the tickets, not off the client.
99
+ const timeResult = ticketIds.length > 0 && isRegistered(slugs.timeEntries)
100
+ ? await dbFind(payload, slugs.timeEntries, {
101
+ where: { ticket: { in: ticketIds } },
102
+ limit: 5000,
103
+ depth: 0,
104
+ overrideAccess: true,
105
+ })
106
+ : EMPTY
107
+
49
108
  const c = clientData as any
50
- const exportData = {
51
- exportDate: new Date().toISOString(),
52
- exportType: 'RGPD - Export des données personnelles',
109
+ const providedByYou = {
53
110
  profile: {
54
111
  email: c.email,
55
112
  firstName: c.firstName,
@@ -79,6 +136,49 @@ export function createExportDataEndpoint(slugs: CollectionSlugs): Endpoint {
79
136
  comment: s.comment,
80
137
  createdAt: s.createdAt,
81
138
  })),
139
+ feedback: feedbackResult.docs.map((f: any) => ({
140
+ ticketId: f.ticket,
141
+ rating: f.rating,
142
+ comment: f.comment,
143
+ submittedFrom: f.submittedFrom,
144
+ createdAt: f.createdAt,
145
+ })),
146
+ chatMessages: chatResult.docs.map((m: any) => ({
147
+ session: m.session,
148
+ senderType: m.senderType,
149
+ message: m.message,
150
+ createdAt: m.createdAt,
151
+ })),
152
+ timeEntries: timeResult.docs.map((e: any) => ({
153
+ ticketId: e.ticket,
154
+ duration: e.duration,
155
+ date: e.date,
156
+ description: e.description,
157
+ billable: e.billable,
158
+ })),
159
+ }
160
+
161
+ const exportData = {
162
+ exportDate: new Date().toISOString(),
163
+ exportType: 'RGPD - Export des données personnelles',
164
+ providedByYou: {
165
+ legalBasis: 'Art. 20 RGPD - droit à la portabilité',
166
+ ...providedByYou,
167
+ },
168
+ derivedData: {
169
+ legalBasis: 'Art. 15 RGPD - droit d\'accès (données dérivées, hors portabilité)',
170
+ clientSummaries: summariesResult.docs.map((s: any) => ({
171
+ summary: s.summary,
172
+ recurringTopics: s.recurringTopics,
173
+ patterns: s.patterns,
174
+ keyFacts: s.keyFacts,
175
+ ticketCount: s.ticketCount,
176
+ messageCount: s.messageCount,
177
+ averageSatisfaction: s.averageSatisfaction,
178
+ generatedAt: s.generatedAt,
179
+ aiModel: s.aiModel,
180
+ })),
181
+ },
82
182
  }
83
183
 
84
184
  const json = JSON.stringify(exportData, null, 2)
@@ -1,7 +1,7 @@
1
1
  import type { Endpoint } from 'payload'
2
2
  import type { CollectionSlugs } from '../utils/slugs'
3
3
  import { requireAdmin, handleAuthError } from '../utils/auth'
4
- import { dbDelete } from '../utils/db'
4
+ import { purgeOlderThan, purgeableCollections } from '../utils/retention'
5
5
 
6
6
  /**
7
7
  * DELETE /api/support/purge-logs?collection=email-logs&days=30
@@ -22,25 +22,16 @@ export function createPurgeLogsEndpoint(slugs: CollectionSlugs): Endpoint {
22
22
  const days = Number(url.searchParams.get('days') || '0')
23
23
 
24
24
  // Map collection param to slug
25
- const allowedCollections: Record<string, string> = {
26
- 'email-logs': slugs.emailLogs,
27
- 'auth-logs': slugs.authLogs,
28
- }
25
+ const allowedCollections = purgeableCollections(slugs)
29
26
 
30
27
  if (!collection || !allowedCollections[collection]) {
31
28
  return Response.json({ error: 'Invalid collection. Use email-logs or auth-logs.' }, { status: 400 })
32
29
  }
33
30
 
34
- const cutoff = days > 0 ? new Date(Date.now() - days * 86400000).toISOString() : null
35
-
36
- const result = await dbDelete(payload, allowedCollections[collection], {
37
- where: cutoff
38
- ? { createdAt: { less_than: cutoff } }
39
- : { id: { exists: true } },
40
- overrideAccess: true,
41
- })
42
-
43
- const count = Array.isArray(result.docs) ? result.docs.length : 0
31
+ // `days=0` wipes the journal entirely. It stays available here an
32
+ // explicit, admin-only, one-off action — and is refused by the
33
+ // scheduled task, which must never be able to do it by accident.
34
+ const count = await purgeOlderThan(payload, allowedCollections[collection], days, req)
44
35
 
45
36
  return Response.json({
46
37
  purged: count,
package/src/index.ts CHANGED
@@ -28,6 +28,8 @@ export type { SupportSettings, SupportSettingsState, UserPrefs } from './utils/r
28
28
  export { DEFAULT_TICKETING_FEATURES, normalizeFeatures, projectAutoClose, stripProjectedFeatures } from './utils/features'
29
29
  export type { TicketingFeatures } from './utils/features'
30
30
  export { MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter } from './utils/rateLimiter'
31
+ export { DEFAULT_RETENTION, PURGE_LOGS_TASK_SLUG, purgeOlderThan, purgeableCollections, runScheduledPurge } from './utils/retention'
32
+ export type { RetentionConfig, ScheduledPurgeResult } from './utils/retention'
31
33
  export type { RateLimitEntry, RateLimitStore } from './utils/rateLimiter'
32
34
  export { DEFAULT_INBOUND_EMAIL_LIMITS, validateInboundEmailPayload, verifySecret } from './utils/webhookSecurity'
33
35
  export type { InboundEmailInput, InboundEmailLimits, InboundEmailValidationError } from './utils/webhookSecurity'
package/src/plugin.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Config, Plugin, AdminViewConfig } from 'payload'
1
+ import type { Config, Plugin, AdminViewConfig, PayloadRequest } from 'payload'
2
2
  import type { SupportPluginConfig, SupportFeatures } from './types'
3
3
  import { DEFAULT_FEATURES } from './types'
4
4
  import { resolveSlugs } from './utils/slugs'
@@ -31,6 +31,7 @@ import {
31
31
  createSupportCountersCollection,
32
32
  } from './collections'
33
33
  import { PayloadRateLimitStore } from './utils/rateLimiter'
34
+ import { DEFAULT_RETENTION, PURGE_LOGS_TASK_SLUG, runScheduledPurge } from './utils/retention'
34
35
  import { SUPPORT_STAFF_SLUG_CONFIG_KEY } from './utils/readSettings'
35
36
 
36
37
  function viewConfig(component: string, path: string): AdminViewConfig {
@@ -168,8 +169,57 @@ export function supportPlugin(config?: SupportPluginConfig): Plugin {
168
169
  capabilities: config?.capabilities,
169
170
  })
170
171
 
172
+ // ─── Retention job ───────────────────────────────────
173
+ //
174
+ // `DELETE /api/support/purge-logs` existed but nothing ever called it, so
175
+ // the two journals grew forever — a retention policy nobody applies is not
176
+ // a retention policy. This registers a daily Payload Jobs task instead of
177
+ // a setInterval: an interval does not survive a serverless deploy and runs
178
+ // N times in parallel behind N instances.
179
+ // The task is OPT-IN: nothing is registered unless `retention` is set.
180
+ //
181
+ // Registering it by default was tempting — a retention policy nobody opts
182
+ // into is how the manual purge endpoint ended up never being called. But
183
+ // declaring a task turns Payload's job queue on, which adds `payload-jobs`
184
+ // and `payload-jobs-stats` to an app that had none: two tables, i.e. a
185
+ // schema change imposed on every consumer of a *support* plugin, by a minor
186
+ // release. And it would buy them nothing, because a scheduled task only
187
+ // fires when the host also runs a job runner (`payload jobs:run`, a cron,
188
+ // or `jobs.autoRun`). Defaulting it on therefore costs two tables and
189
+ // delivers no purge until the integrator does the other half of the work
190
+ // anyway — at which point they can just as well pass `retention`.
191
+ //
192
+ // The README states the trade-off where the option is documented, so the
193
+ // choice is visible rather than silent. `admin-ui-pro` made the same call
194
+ // for the same reason.
195
+ const retention = config?.retention === false ? undefined : config?.retention
196
+ const retentionEnabled = Boolean(retention)
197
+ const purgeTask = {
198
+ slug: PURGE_LOGS_TASK_SLUG,
199
+ label: 'Support — purge des journaux',
200
+ schedule: [
201
+ {
202
+ cron: retention?.cron ?? DEFAULT_RETENTION.cron,
203
+ queue: retention?.queue ?? DEFAULT_RETENTION.queue,
204
+ },
205
+ ],
206
+ handler: async ({ req }: { req: PayloadRequest }) => {
207
+ const result = await runScheduledPurge(req.payload, slugs, retention, req)
208
+ return { output: result }
209
+ },
210
+ }
211
+
212
+ const existingJobs = incomingConfig.jobs
213
+ const jobs = retentionEnabled
214
+ ? ({
215
+ ...existingJobs,
216
+ tasks: [...(existingJobs?.tasks ?? []), purgeTask],
217
+ } as Config['jobs'])
218
+ : existingJobs
219
+
171
220
  return {
172
221
  ...incomingConfig,
222
+ jobs,
173
223
  // Publish the resolved staff collection so the server-side readers share
174
224
  // ONE source of truth with the writers. `requireAdmin` compares against
175
225
  // `slugs.users`; the `payload-preferences` reads used to scope themselves
@@ -1,7 +1,7 @@
1
1
  'use client'
2
2
 
3
3
  import React, { useState, useEffect, useRef, useCallback } from 'react'
4
- import { MessageCircle, Send, X, Minimize2, ArrowLeft } from 'lucide-react'
4
+ import { MessageCircle, Send, X, Minimize2, ArrowLeft } from './icons'
5
5
  import type { ChatMessage, LiveChatSession, Screen } from './types'
6
6
  import { loadSession, saveSession, clearSession } from './helpers'
7
7
 
@@ -561,8 +561,9 @@ export default function ProfilePage() {
561
561
 
562
562
  <div className="p-6 sm:p-8">
563
563
  <p className="mb-4 text-sm text-slate-500 dark:text-slate-400 leading-relaxed">
564
- Conformement au RGPD, vous pouvez télécharger l&apos;ensemble de vos données personnelles
565
- (profil, tickets, messages, enquetes de satisfaction).
564
+ Conformement au RGPD, vous pouvez télécharger l&apos;ensemble de vos données personnelles :
565
+ celles que vous avez fournies (profil, tickets, messages, temps passe, enquetes de
566
+ satisfaction, chat) et celles que nous avons deduites de votre historique (synthese IA).
566
567
  </p>
567
568
  <a
568
569
  href="/api/support/export-data"
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Inlined Lucide icons.
3
+ *
4
+ * The plugin used to require `lucide-react` as a peer dependency for exactly
5
+ * 5 icons across the portal live-chat widget. That made every integrator install an icon set,
6
+ * and a missing install a build-time failure, for 5 paths totalling a few
7
+ * hundred bytes. They are inlined here instead, and `lucide-react` is no longer
8
+ * a peer dependency.
9
+ *
10
+ * The geometry below is copied from lucide-react v1.41.0 and, like the rest of
11
+ * Lucide, is under the ISC licence:
12
+ *
13
+ * ISC License
14
+ * Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as
15
+ * part of Feather (MIT). All other copyright (c) for Lucide are held by
16
+ * Lucide Contributors 2022.
17
+ *
18
+ * Permission to use, copy, modify, and/or distribute this software for any
19
+ * purpose with or without fee is hereby granted, provided that the above
20
+ * copyright notice and this permission notice appear in all copies.
21
+ *
22
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
23
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
24
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
25
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
26
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
27
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
28
+ * IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
29
+ */
30
+
31
+ import React from 'react'
32
+
33
+ export interface IconProps extends Omit<React.SVGProps<SVGSVGElement>, 'width' | 'height'> {
34
+ /** Rendered width and height. Lucide's own default is 24. */
35
+ size?: number | string
36
+ }
37
+
38
+ type IconNode = ReadonlyArray<readonly [string, Record<string, string | number>]>
39
+
40
+ /**
41
+ * Icons are decorative by default (`aria-hidden`): every control that has
42
+ * nothing but an icon inside carries its own `aria-label`. Pass
43
+ * `aria-hidden={false}` plus a label to make one meaningful on its own.
44
+ */
45
+ function createIcon(displayName: string, nodes: IconNode): React.FC<IconProps> {
46
+ const Icon: React.FC<IconProps> = ({ size = 24, strokeWidth = 2, ...rest }) => (
47
+ <svg
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ width={size}
50
+ height={size}
51
+ viewBox="0 0 24 24"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ strokeWidth={strokeWidth}
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ aria-hidden="true"
58
+ {...rest}
59
+ >
60
+ {nodes.map(([tag, attrs], index) => React.createElement(tag, { key: index, ...attrs }))}
61
+ </svg>
62
+ )
63
+ Icon.displayName = displayName
64
+ return Icon
65
+ }
66
+
67
+ export const ArrowLeft = createIcon('ArrowLeft', [
68
+ ['path', { d: "m12 19-7-7 7-7" }],
69
+ ['path', { d: "M19 12H5" }],
70
+ ] as const)
71
+
72
+ export const MessageCircle = createIcon('MessageCircle', [
73
+ ['path', { d: "M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" }],
74
+ ] as const)
75
+
76
+ export const Minimize2 = createIcon('Minimize2', [
77
+ ['path', { d: "m14 10 7-7" }],
78
+ ['path', { d: "M20 10h-6V4" }],
79
+ ['path', { d: "m3 21 7-7" }],
80
+ ['path', { d: "M4 14h6v6" }],
81
+ ] as const)
82
+
83
+ export const Send = createIcon('Send', [
84
+ ['path', { d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" }],
85
+ ['path', { d: "m21.854 2.147-10.94 10.939" }],
86
+ ] as const)
87
+
88
+ export const X = createIcon('X', [
89
+ ['path', { d: "M18 6 6 18" }],
90
+ ['path', { d: "m6 6 12 12" }],
91
+ ] as const)
@@ -23,7 +23,7 @@
23
23
  }
24
24
 
25
25
  .subtitle {
26
- color: var(--theme-elevation-500);
26
+ color: var(--theme-elevation-650);
27
27
  margin: 4px 0 0;
28
28
  font-size: 14px;
29
29
  }
@@ -59,7 +59,7 @@
59
59
  .label {
60
60
  font-size: 12px;
61
61
  font-weight: 700;
62
- color: var(--theme-elevation-500);
62
+ color: var(--theme-elevation-650);
63
63
  margin-bottom: 4px;
64
64
  }
65
65
 
@@ -134,7 +134,7 @@
134
134
  border: 1px solid var(--theme-elevation-300);
135
135
  border-radius: 8px;
136
136
  background: var(--theme-elevation-100);
137
- color: var(--theme-elevation-500);
137
+ color: var(--theme-elevation-650);
138
138
  font-size: 14px;
139
139
  }
140
140
 
@@ -162,7 +162,7 @@
162
162
  }
163
163
 
164
164
  .groupClient {
165
- color: var(--theme-elevation-500);
165
+ color: var(--theme-elevation-650);
166
166
  margin-left: 10px;
167
167
  font-size: 13px;
168
168
  }
@@ -190,7 +190,7 @@
190
190
 
191
191
  .groupAmountStrike {
192
192
  font-size: 11px;
193
- color: var(--theme-elevation-500);
193
+ color: var(--theme-elevation-650);
194
194
  text-decoration: line-through;
195
195
  }
196
196
 
@@ -208,7 +208,7 @@
208
208
  text-transform: uppercase;
209
209
  letter-spacing: 0.5px;
210
210
  text-align: center;
211
- color: var(--theme-elevation-500);
211
+ color: var(--theme-elevation-650);
212
212
  background: var(--theme-elevation-50);
213
213
  border-bottom: 1px solid var(--theme-elevation-300);
214
214
  }
@@ -268,7 +268,7 @@
268
268
  }
269
269
 
270
270
  .secondary {
271
- color: var(--theme-elevation-500);
271
+ color: var(--theme-elevation-650);
272
272
  }
273
273
 
274
274
  .billedAmount {
@@ -326,7 +326,7 @@
326
326
  border-radius: 4px;
327
327
  border: 1px solid var(--theme-elevation-300);
328
328
  background: var(--theme-elevation-100);
329
- color: var(--theme-elevation-500);
329
+ color: var(--theme-elevation-650);
330
330
  font-size: 10px;
331
331
  font-weight: 600;
332
332
  cursor: pointer;
@@ -367,7 +367,7 @@
367
367
 
368
368
  .summaryMeta {
369
369
  font-size: 11px;
370
- color: var(--theme-elevation-500);
370
+ color: var(--theme-elevation-650);
371
371
  }
372
372
 
373
373
  .summaryAction {
@@ -403,7 +403,7 @@
403
403
  background: var(--theme-elevation-100);
404
404
  border: 1px dashed var(--theme-elevation-300);
405
405
  border-radius: 6px;
406
- color: var(--theme-elevation-500);
406
+ color: var(--theme-elevation-650);
407
407
  font-size: 12px;
408
408
  font-style: italic;
409
409
  }
@@ -423,7 +423,7 @@
423
423
 
424
424
  .totalMeta {
425
425
  font-size: 13px;
426
- color: var(--theme-elevation-500);
426
+ color: var(--theme-elevation-650);
427
427
  }
428
428
 
429
429
  .totalChecked {
@@ -29,7 +29,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
29
29
 
30
30
  .subtitle {
31
31
  font-size: 13px;
32
- color: var(--theme-elevation-500);
32
+ color: var(--theme-elevation-650);
33
33
  margin: 4px 0 0;
34
34
  }
35
35
 
@@ -67,7 +67,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
67
67
  letter-spacing: 0.04em;
68
68
  border: none;
69
69
  cursor: pointer;
70
- color: var(--theme-elevation-500);
70
+ color: var(--theme-elevation-650);
71
71
  background: none;
72
72
  position: relative;
73
73
  transition: color 100ms;
@@ -175,7 +175,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
175
175
 
176
176
  .sessionPreview {
177
177
  font-size: 12px;
178
- color: var(--theme-elevation-500);
178
+ color: var(--theme-elevation-650);
179
179
  white-space: nowrap;
180
180
  overflow: hidden;
181
181
  text-overflow: ellipsis;
@@ -233,7 +233,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
233
233
  border-radius: 6px;
234
234
  border: 1px solid var(--theme-elevation-200);
235
235
  background: none;
236
- color: var(--theme-elevation-500);
236
+ color: var(--theme-elevation-650);
237
237
  font-weight: 600;
238
238
  font-size: 12px;
239
239
  cursor: pointer;
@@ -301,7 +301,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
301
301
  border-color: var(--theme-elevation-200);
302
302
  padding: 6px 14px;
303
303
  font-size: 12px;
304
- color: var(--theme-elevation-500);
304
+ color: var(--theme-elevation-650);
305
305
  border-radius: 8px;
306
306
  max-width: 80%;
307
307
  }
@@ -339,7 +339,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
339
339
  font-weight: 700;
340
340
  margin-bottom: 4px;
341
341
  opacity: 0.7;
342
- color: var(--theme-elevation-500);
342
+ color: var(--theme-elevation-650);
343
343
  }
344
344
 
345
345
  .bubbleBody {
@@ -373,7 +373,7 @@ $font-stack: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', system-ui,
373
373
  font-size: 12px;
374
374
  font-weight: 600;
375
375
  background: none;
376
- color: var(--theme-elevation-500);
376
+ color: var(--theme-elevation-650);
377
377
  cursor: pointer;
378
378
  transition: all 80ms;
379
379
 
@@ -121,7 +121,7 @@
121
121
 
122
122
  .itemSub {
123
123
  font-size: 11px;
124
- color: var(--theme-elevation-500);
124
+ color: var(--theme-elevation-650);
125
125
  margin-top: 1px;
126
126
  }
127
127
 
@@ -34,7 +34,7 @@ $red: #dc2626;
34
34
 
35
35
  .subtitle {
36
36
  font-size: 13px;
37
- color: var(--theme-elevation-500);
37
+ color: var(--theme-elevation-650);
38
38
  font-weight: 400;
39
39
  }
40
40
 
@@ -125,7 +125,7 @@ $red: #dc2626;
125
125
 
126
126
  .clientName {
127
127
  font-size: 12px;
128
- color: var(--theme-elevation-500);
128
+ color: var(--theme-elevation-650);
129
129
  }
130
130
 
131
131
  .clientEmail {
@@ -181,7 +181,7 @@ $red: #dc2626;
181
181
 
182
182
  .clientHeaderContact {
183
183
  font-size: 14px;
184
- color: var(--theme-elevation-500);
184
+ color: var(--theme-elevation-650);
185
185
  margin-top: 4px;
186
186
  }
187
187
 
@@ -199,7 +199,7 @@ $red: #dc2626;
199
199
  background: var(--theme-elevation-50);
200
200
  border-radius: 6px;
201
201
  font-size: 12px;
202
- color: var(--theme-elevation-500);
202
+ color: var(--theme-elevation-650);
203
203
  font-style: italic;
204
204
  }
205
205
 
@@ -281,7 +281,7 @@ $red: #dc2626;
281
281
 
282
282
  .mergePanelDesc {
283
283
  font-size: 12px;
284
- color: var(--theme-elevation-500);
284
+ color: var(--theme-elevation-650);
285
285
  margin: 0 0 10px;
286
286
  line-height: 1.5;
287
287
  }
@@ -328,13 +328,13 @@ $red: #dc2626;
328
328
  }
329
329
 
330
330
  .mergeResultName {
331
- color: var(--theme-elevation-500);
331
+ color: var(--theme-elevation-650);
332
332
  margin-left: 8px;
333
333
  }
334
334
 
335
335
  .mergeEmpty {
336
336
  font-size: 12px;
337
- color: var(--theme-elevation-500);
337
+ color: var(--theme-elevation-650);
338
338
  font-style: italic;
339
339
  margin: 0;
340
340
  }
@@ -355,7 +355,7 @@ $red: #dc2626;
355
355
  font-size: 10px;
356
356
  font-weight: 700;
357
357
  text-transform: uppercase;
358
- color: var(--theme-elevation-500);
358
+ color: var(--theme-elevation-650);
359
359
  letter-spacing: 0.03em;
360
360
  }
361
361
 
@@ -477,7 +477,7 @@ $red: #dc2626;
477
477
  font-size: 10px;
478
478
  font-weight: 600;
479
479
  text-transform: uppercase;
480
- color: var(--theme-elevation-500);
480
+ color: var(--theme-elevation-650);
481
481
  border-bottom: 1px solid var(--theme-elevation-200);
482
482
  letter-spacing: 0.03em;
483
483
  }
@@ -42,7 +42,7 @@
42
42
  font-weight: 700;
43
43
  text-transform: uppercase;
44
44
  letter-spacing: 0.06em;
45
- color: var(--theme-elevation-500);
45
+ color: var(--theme-elevation-650);
46
46
  margin-bottom: 6px;
47
47
  }
48
48
 
@@ -55,7 +55,7 @@
55
55
  .statUnit {
56
56
  font-size: 14px;
57
57
  font-weight: 500;
58
- color: var(--theme-elevation-500);
58
+ color: var(--theme-elevation-650);
59
59
  margin-left: 2px;
60
60
  }
61
61
 
@@ -78,7 +78,7 @@
78
78
  padding: 8px 14px;
79
79
  font-size: 13px;
80
80
  font-weight: 600;
81
- color: var(--theme-elevation-500);
81
+ color: var(--theme-elevation-650);
82
82
  background: none;
83
83
  border: none;
84
84
  border-bottom: 2px solid transparent;
@@ -104,7 +104,7 @@
104
104
  background: none;
105
105
  font-size: 12px;
106
106
  font-weight: 600;
107
- color: var(--theme-elevation-500);
107
+ color: var(--theme-elevation-650);
108
108
  cursor: pointer;
109
109
  transition: all 80ms;
110
110
  &:hover { background: var(--theme-elevation-100); }
@@ -141,7 +141,7 @@
141
141
  font-weight: 700;
142
142
  text-transform: uppercase;
143
143
  letter-spacing: 0.04em;
144
- color: var(--theme-elevation-500);
144
+ color: var(--theme-elevation-650);
145
145
  border-bottom: 1px solid var(--theme-elevation-200);
146
146
  }
147
147
 
@@ -217,7 +217,7 @@
217
217
  background: none;
218
218
  font-size: 12px;
219
219
  font-weight: 600;
220
- color: var(--theme-elevation-500);
220
+ color: var(--theme-elevation-650);
221
221
  cursor: pointer;
222
222
  &:hover { background: var(--theme-elevation-100); }
223
223
  &:disabled { opacity: 0.4; cursor: not-allowed; }