@consilioweb/payload-support 5.0.0 → 6.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.
- package/README.md +249 -22
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +306 -60
- package/dist/index.d.cts +107 -38
- package/dist/index.d.ts +107 -38
- package/dist/index.js +300 -59
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/package.json +6 -2
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/types/lucide-react.d.ts +0 -42
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
4
4
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
5
|
+
import { V } from '../shared/adminTokens'
|
|
5
6
|
import { DATE_LOCALE } from '../shared/dateLocale'
|
|
6
7
|
import s from '../../styles/EmailTracking.module.scss'
|
|
7
8
|
|
|
@@ -111,7 +112,7 @@ export const EmailTrackingClient: React.FC = () => {
|
|
|
111
112
|
<button key={v} className={`${s.dateRangeBtn} ${dateRange === v ? s.dateRangeActive : ''}`} onClick={() => setDateRange(v)}>{v}j</button>
|
|
112
113
|
))}
|
|
113
114
|
</div>
|
|
114
|
-
<input type="text" className={s.searchInput} placeholder={t('emailTracking.searchPlaceholder')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
115
|
+
<input type="text" className={s.searchInput} aria-label={t('emailTracking.searchLabel')} placeholder={t('emailTracking.searchPlaceholder')} value={search} onChange={(e) => setSearch(e.target.value)} />
|
|
115
116
|
</div>
|
|
116
117
|
|
|
117
118
|
{/* Table */}
|
|
@@ -125,7 +126,7 @@ export const EmailTrackingClient: React.FC = () => {
|
|
|
125
126
|
<tbody>
|
|
126
127
|
{logs.map((log) => (
|
|
127
128
|
<React.Fragment key={log.id}>
|
|
128
|
-
<tr
|
|
129
|
+
<tr>
|
|
129
130
|
<td style={{ fontSize: 12, whiteSpace: 'nowrap' }}>{fmtDate(log.createdAt)}</td>
|
|
130
131
|
<td style={{ maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.recipientEmail}>{log.recipientEmail || '—'}</td>
|
|
131
132
|
<td style={{ maxWidth: 250, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={log.subject}>{log.subject || '—'}</td>
|
|
@@ -137,14 +138,24 @@ export const EmailTrackingClient: React.FC = () => {
|
|
|
137
138
|
<span className={`${s.processingTime} ${log.processingTimeMs > 2000 ? s.timeSlow : log.processingTimeMs > 500 ? s.timeMedium : s.timeFast}`}>{log.processingTimeMs}ms</span>
|
|
138
139
|
) : '—'}
|
|
139
140
|
{log.status === 'error' && log.errorMessage && (
|
|
140
|
-
|
|
141
|
+
/* The row itself carries no handler: a `<tr onClick>` is
|
|
142
|
+
* unreachable by keyboard and role="button" on a row would
|
|
143
|
+
* destroy the table semantics. This button is the control. */
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className={s.expandBtn}
|
|
147
|
+
aria-expanded={expandedRow === log.id}
|
|
148
|
+
aria-controls={`email-error-${log.id}`}
|
|
149
|
+
aria-label={t('emailTracking.toggleError')}
|
|
150
|
+
onClick={() => setExpandedRow(expandedRow === log.id ? null : log.id)}
|
|
151
|
+
>
|
|
141
152
|
{expandedRow === log.id ? '▴' : '▾'}
|
|
142
153
|
</button>
|
|
143
154
|
)}
|
|
144
155
|
</td>
|
|
145
156
|
</tr>
|
|
146
157
|
{expandedRow === log.id && log.errorMessage && (
|
|
147
|
-
<tr><td colSpan={7}><div className={s.errorDetail}>
|
|
158
|
+
<tr><td colSpan={7}><div className={s.errorDetail} id={`email-error-${log.id}`}>{t('emailTracking.errorLabel')}{log.httpStatus ? ` (HTTP ${log.httpStatus})` : ''}: {log.errorMessage}</div></td></tr>
|
|
148
159
|
)}
|
|
149
160
|
</React.Fragment>
|
|
150
161
|
))}
|
|
@@ -155,9 +166,9 @@ export const EmailTrackingClient: React.FC = () => {
|
|
|
155
166
|
{/* Pagination */}
|
|
156
167
|
{totalDocs > 0 && (
|
|
157
168
|
<div className={s.pagination}>
|
|
158
|
-
<button className={s.pageBtn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
|
|
159
|
-
<span style={{ fontSize: 12, color:
|
|
160
|
-
<button className={s.pageBtn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
|
|
169
|
+
<button type="button" className={s.pageBtn} onClick={() => setPage((p) => Math.max(1, p - 1))} disabled={page <= 1}>{t('common.previous')}</button>
|
|
170
|
+
<span style={{ fontSize: 12, color: V.textSecondary, alignSelf: 'center' }}>{t('common.page')} {page} — {totalDocs} {t('common.results')}</span>
|
|
171
|
+
<button type="button" className={s.pageBtn} onClick={() => setPage((p) => p + 1)} disabled={!hasMore}>{t('common.next')}</button>
|
|
161
172
|
</div>
|
|
162
173
|
)}
|
|
163
174
|
</div>
|
|
@@ -131,7 +131,7 @@ export const LogsClient: React.FC = () => {
|
|
|
131
131
|
</td>
|
|
132
132
|
<td className={s.truncate} title={log.recipientEmail}>{log.recipientEmail || '—'}</td>
|
|
133
133
|
<td className={s.truncate} title={log.subject}>{log.subject || '—'}</td>
|
|
134
|
-
<td style={{ fontSize: 12, color: 'var(--theme-elevation-
|
|
134
|
+
<td style={{ fontSize: 12, color: 'var(--theme-elevation-650)' }}>{log.action || '—'}</td>
|
|
135
135
|
<td style={{ textAlign: 'right' }}>
|
|
136
136
|
{log.processingTimeMs != null ? (
|
|
137
137
|
<span className={s.mono} style={{ color: log.processingTimeMs > 2000 ? '#dc2626' : log.processingTimeMs > 500 ? '#d97706' : '#16a34a' }}>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect } from 'react'
|
|
3
|
+
import React, { useState, useEffect, useId } from 'react'
|
|
4
4
|
import Link from 'next/link'
|
|
5
5
|
import { useRouter } from 'next/navigation'
|
|
6
6
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
@@ -27,6 +27,14 @@ const PRIORITY_KEYS = [
|
|
|
27
27
|
|
|
28
28
|
export const NewTicketClient: React.FC = () => {
|
|
29
29
|
const { t } = useTranslation()
|
|
30
|
+
// Each `<label>` below already carries the right, already-translated text;
|
|
31
|
+
// it just was not associated with anything. useId, never a literal.
|
|
32
|
+
const clientSearchId = useId()
|
|
33
|
+
const subjectId = useId()
|
|
34
|
+
const categoryId = useId()
|
|
35
|
+
const priorityId = useId()
|
|
36
|
+
const projectId_ = useId()
|
|
37
|
+
const descriptionId = useId()
|
|
30
38
|
const router = useRouter()
|
|
31
39
|
const [subject, setSubject] = useState('')
|
|
32
40
|
const [description, setDescription] = useState('')
|
|
@@ -130,18 +138,19 @@ export const NewTicketClient: React.FC = () => {
|
|
|
130
138
|
<form className={s.form} onSubmit={handleSubmit}>
|
|
131
139
|
{/* Client search */}
|
|
132
140
|
<div className={s.fieldGroup}>
|
|
133
|
-
<label className={s.label}>{t('newTicket.clientLabel')} <span className={s.required}>*</span></label>
|
|
141
|
+
<label className={s.label} htmlFor={clientSearchId}>{t('newTicket.clientLabel')} <span className={s.required}>*</span></label>
|
|
134
142
|
{selectedClient ? (
|
|
135
143
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 14px', borderRadius: 10, border: '1px solid var(--theme-elevation-200)', background: 'var(--theme-elevation-50)' }}>
|
|
136
144
|
<span style={{ fontWeight: 600, fontSize: 13, color: 'var(--theme-text)' }}>
|
|
137
145
|
{selectedClient.firstName} {selectedClient.lastName} — {selectedClient.company}
|
|
138
146
|
</span>
|
|
139
|
-
<span style={{ fontSize: 12, color: 'var(--theme-elevation-
|
|
147
|
+
<span style={{ fontSize: 12, color: 'var(--theme-elevation-650)' }}>{selectedClient.email}</span>
|
|
140
148
|
<button type="button" onClick={() => { setSelectedClient(null); setClientId(null); setClientSearch('') }} style={{ marginLeft: 'auto', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--theme-elevation-400)', fontSize: 16 }}>×</button>
|
|
141
149
|
</div>
|
|
142
150
|
) : (
|
|
143
151
|
<div className={s.searchWrap}>
|
|
144
152
|
<input
|
|
153
|
+
id={clientSearchId}
|
|
145
154
|
type="text"
|
|
146
155
|
className={s.input}
|
|
147
156
|
placeholder={t('newTicket.clientSearchPlaceholder')}
|
|
@@ -169,27 +178,27 @@ export const NewTicketClient: React.FC = () => {
|
|
|
169
178
|
|
|
170
179
|
{/* Subject */}
|
|
171
180
|
<div className={s.fieldGroup}>
|
|
172
|
-
<label className={s.label}>{t('newTicket.subjectLabel')} <span className={s.required}>*</span></label>
|
|
173
|
-
<input type="text" className={s.input} placeholder={t('newTicket.subjectPlaceholder')} value={subject} onChange={(e) => setSubject(e.target.value)} />
|
|
181
|
+
<label className={s.label} htmlFor={subjectId}>{t('newTicket.subjectLabel')} <span className={s.required}>*</span></label>
|
|
182
|
+
<input id={subjectId} type="text" className={s.input} placeholder={t('newTicket.subjectPlaceholder')} value={subject} onChange={(e) => setSubject(e.target.value)} />
|
|
174
183
|
</div>
|
|
175
184
|
|
|
176
185
|
{/* Category + Priority + Project */}
|
|
177
186
|
<div className={s.row3}>
|
|
178
187
|
<div className={s.fieldGroup}>
|
|
179
|
-
<label className={s.label}>{t('newTicket.categoryLabel')}</label>
|
|
180
|
-
<select className={s.select} value={category} onChange={(e) => setCategory(e.target.value)}>
|
|
188
|
+
<label className={s.label} htmlFor={categoryId}>{t('newTicket.categoryLabel')}</label>
|
|
189
|
+
<select id={categoryId} className={s.select} value={category} onChange={(e) => setCategory(e.target.value)}>
|
|
181
190
|
{CATEGORY_KEYS.map((c) => <option key={c.value} value={c.value}>{t(c.key)}</option>)}
|
|
182
191
|
</select>
|
|
183
192
|
</div>
|
|
184
193
|
<div className={s.fieldGroup}>
|
|
185
|
-
<label className={s.label}>{t('newTicket.priorityLabel')}</label>
|
|
186
|
-
<select className={s.select} value={priority} onChange={(e) => setPriority(e.target.value)}>
|
|
194
|
+
<label className={s.label} htmlFor={priorityId}>{t('newTicket.priorityLabel')}</label>
|
|
195
|
+
<select id={priorityId} className={s.select} value={priority} onChange={(e) => setPriority(e.target.value)}>
|
|
187
196
|
{PRIORITY_KEYS.map((p) => <option key={p.value} value={p.value}>{t(p.key)}</option>)}
|
|
188
197
|
</select>
|
|
189
198
|
</div>
|
|
190
199
|
<div className={s.fieldGroup}>
|
|
191
|
-
<label className={s.label}>{t('newTicket.projectLabel')}</label>
|
|
192
|
-
<select className={s.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
|
|
200
|
+
<label className={s.label} htmlFor={projectId_}>{t('newTicket.projectLabel')}</label>
|
|
201
|
+
<select id={projectId_} className={s.select} value={projectId || ''} onChange={(e) => setProjectId(e.target.value ? Number(e.target.value) : null)}>
|
|
193
202
|
<option value="">{t('newTicket.noProject')}</option>
|
|
194
203
|
{projects.map((p) => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
195
204
|
</select>
|
|
@@ -198,8 +207,8 @@ export const NewTicketClient: React.FC = () => {
|
|
|
198
207
|
|
|
199
208
|
{/* Description */}
|
|
200
209
|
<div className={s.fieldGroup}>
|
|
201
|
-
<label className={s.label}>{t('newTicket.descriptionLabel')}</label>
|
|
202
|
-
<textarea className={s.textarea} placeholder={t('newTicket.descriptionPlaceholder')} value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
210
|
+
<label className={s.label} htmlFor={descriptionId}>{t('newTicket.descriptionLabel')}</label>
|
|
211
|
+
<textarea id={descriptionId} className={s.textarea} placeholder={t('newTicket.descriptionPlaceholder')} value={description} onChange={(e) => setDescription(e.target.value)} />
|
|
203
212
|
</div>
|
|
204
213
|
|
|
205
214
|
<button type="submit" className={s.submitBtn} disabled={submitting}>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import React, { useState, useEffect, useCallback } from 'react'
|
|
4
|
-
import { Inbox, Plus, Link2, X, Search, ChevronDown, ChevronUp, Paperclip } from '
|
|
4
|
+
import { Inbox, Plus, Link2, X, Search, ChevronDown, ChevronUp, Paperclip } from '../shared/icons'
|
|
5
5
|
import { SkeletonDashboard } from '../shared/Skeleton'
|
|
6
6
|
import { useTranslation } from '../../components/TicketConversation/hooks/useTranslation'
|
|
7
7
|
import styles from '../../styles/PendingEmails.module.scss'
|
|
@@ -86,7 +86,7 @@ function TicketSearchModal({
|
|
|
86
86
|
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
|
87
87
|
<div className={styles.modalHeader}>
|
|
88
88
|
<h3 className={styles.modalTitle}>Rattacher a un ticket</h3>
|
|
89
|
-
<button onClick={onClose} className={styles.modalClose}>
|
|
89
|
+
<button type="button" aria-label="Fermer" onClick={onClose} className={styles.modalClose}>
|
|
90
90
|
<X size={20} />
|
|
91
91
|
</button>
|
|
92
92
|
</div>
|
|
@@ -112,6 +112,7 @@ function TicketSearchModal({
|
|
|
112
112
|
<Search size={16} className={styles.searchIcon} />
|
|
113
113
|
<input
|
|
114
114
|
type="text"
|
|
115
|
+
aria-label="Rechercher un ticket"
|
|
115
116
|
placeholder="Rechercher un ticket (TK-0042, sujet...)..."
|
|
116
117
|
value={search}
|
|
117
118
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -225,7 +226,7 @@ function ClientPickerModal({
|
|
|
225
226
|
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
|
226
227
|
<div className={styles.modalHeader}>
|
|
227
228
|
<h3 className={styles.modalTitle}>Choisir un client</h3>
|
|
228
|
-
<button onClick={onClose} className={styles.modalClose}>
|
|
229
|
+
<button type="button" aria-label="Fermer" onClick={onClose} className={styles.modalClose}>
|
|
229
230
|
<X size={20} />
|
|
230
231
|
</button>
|
|
231
232
|
</div>
|
|
@@ -251,6 +252,7 @@ function ClientPickerModal({
|
|
|
251
252
|
<Search size={16} className={styles.searchIcon} />
|
|
252
253
|
<input
|
|
253
254
|
type="text"
|
|
255
|
+
aria-label="Rechercher un client"
|
|
254
256
|
placeholder="Rechercher un client (nom, email, entreprise)..."
|
|
255
257
|
value={search}
|
|
256
258
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -287,6 +289,7 @@ function ClientPickerModal({
|
|
|
287
289
|
<div className={styles.createFormRow}>
|
|
288
290
|
<input
|
|
289
291
|
type="text"
|
|
292
|
+
aria-label="Prenom"
|
|
290
293
|
placeholder="Prenom *"
|
|
291
294
|
value={newClient.firstName}
|
|
292
295
|
onChange={(e) => setNewClient((p) => ({ ...p, firstName: e.target.value }))}
|
|
@@ -294,6 +297,7 @@ function ClientPickerModal({
|
|
|
294
297
|
/>
|
|
295
298
|
<input
|
|
296
299
|
type="text"
|
|
300
|
+
aria-label="Nom"
|
|
297
301
|
placeholder="Nom"
|
|
298
302
|
value={newClient.lastName}
|
|
299
303
|
onChange={(e) => setNewClient((p) => ({ ...p, lastName: e.target.value }))}
|
|
@@ -302,6 +306,7 @@ function ClientPickerModal({
|
|
|
302
306
|
</div>
|
|
303
307
|
<input
|
|
304
308
|
type="email"
|
|
309
|
+
aria-label="Email"
|
|
305
310
|
placeholder="Email *"
|
|
306
311
|
value={newClient.email}
|
|
307
312
|
onChange={(e) => setNewClient((p) => ({ ...p, email: e.target.value }))}
|
|
@@ -309,6 +314,7 @@ function ClientPickerModal({
|
|
|
309
314
|
/>
|
|
310
315
|
<input
|
|
311
316
|
type="text"
|
|
317
|
+
aria-label="Entreprise"
|
|
312
318
|
placeholder="Entreprise *"
|
|
313
319
|
value={newClient.company}
|
|
314
320
|
onChange={(e) => setNewClient((p) => ({ ...p, company: e.target.value }))}
|
|
@@ -511,15 +511,21 @@ export const SupportDashboardClient: React.FC = () => {
|
|
|
511
511
|
</thead>
|
|
512
512
|
<tbody>
|
|
513
513
|
{tickets.map((tk) => (
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
514
|
+
/*
|
|
515
|
+
* The row is not the control: a `<tr onClick>` is invisible to
|
|
516
|
+
* the keyboard, and turning it into role="button" would cost
|
|
517
|
+
* the table semantics. The link lives in the subject cell.
|
|
518
|
+
*/
|
|
519
|
+
<tr key={tk.id}>
|
|
518
520
|
<td>
|
|
519
521
|
<span className={`${styles.statusDot} ${getStatusDotClass(tk.status)}`} />
|
|
520
522
|
</td>
|
|
521
523
|
<td className={styles.ticketNum}>#{tk.ticketNumber}</td>
|
|
522
|
-
<td className={styles.ticketSubject}>
|
|
524
|
+
<td className={styles.ticketSubject}>
|
|
525
|
+
<a className={styles.ticketLink} href={`/admin/support/ticket?id=${tk.id}`}>
|
|
526
|
+
{tk.subject}
|
|
527
|
+
</a>
|
|
528
|
+
</td>
|
|
523
529
|
<td className={styles.ticketClient}>{getClientName(tk)}</td>
|
|
524
530
|
<td className={styles.ticketTime}>{timeAgo(tk.updatedAt)}</td>
|
|
525
531
|
<td className={styles.ticketArrow}>→</td>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useCallback, useRef } from 'react'
|
|
3
|
+
import React, { useState, useEffect, useCallback, useId, useRef } from 'react'
|
|
4
4
|
import { useSearchParams } from 'next/navigation'
|
|
5
5
|
import Link from 'next/link'
|
|
6
6
|
import { RichTextEditor, type RichTextEditorHandle } from '../../components/RichTextEditor/index'
|
|
@@ -26,6 +26,13 @@ export const TicketDetailClient: React.FC = () => {
|
|
|
26
26
|
const threadEndRef = useRef<HTMLDivElement>(null)
|
|
27
27
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
28
28
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
29
|
+
// Generated ids so every control below carries the name of the text next to
|
|
30
|
+
// it. useId, never a literal: this view can be mounted more than once.
|
|
31
|
+
const manualTimeId = useId()
|
|
32
|
+
const billingTypeId = useId()
|
|
33
|
+
const flatAmountId = useId()
|
|
34
|
+
const billedAmountId = useId()
|
|
35
|
+
const paymentStatusId = useId()
|
|
29
36
|
|
|
30
37
|
const [ticket, setTicket] = useState<Record<string, unknown> | null>(null)
|
|
31
38
|
const [messages, setMessages] = useState<Message[]>([])
|
|
@@ -721,7 +728,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
721
728
|
const file = typeof att.file === 'object' ? att.file : null
|
|
722
729
|
if (!file) return null
|
|
723
730
|
return (file.mimeType || '').startsWith('image/')
|
|
724
|
-
? <a key={i} href={file.url || '#'} target="_blank" rel="noopener noreferrer"><img src={file.url || ''} alt=
|
|
731
|
+
? <a key={i} href={file.url || '#'} target="_blank" rel="noopener noreferrer"><img src={file.url || ''} alt={file.filename || t('detail.file')} className={s.attachmentImg} /></a>
|
|
725
732
|
: <a key={i} href={file.url || '#'} target="_blank" rel="noopener noreferrer" className={s.attachmentFile}>PJ {file.filename || 'Fichier'}</a>
|
|
726
733
|
})}
|
|
727
734
|
</div>
|
|
@@ -810,7 +817,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
810
817
|
/>
|
|
811
818
|
{/* #5 — File upload button */}
|
|
812
819
|
<button className={s.toolbarBtn} data-tooltip={t('detail.file')} aria-label={t('detail.file')} onClick={() => fileInputRef.current?.click()} style={{ fontSize: 11, fontWeight: 700, padding: '4px 10px', width: 'auto' }}>📎 {t('detail.file')}</button>
|
|
813
|
-
<input ref={fileInputRef} type="file" multiple className={s.hiddenFileInput} onChange={(e) => handleFileSelect(e.target.files)} />
|
|
820
|
+
<input ref={fileInputRef} type="file" multiple aria-label={t('detail.file')} className={s.hiddenFileInput} onChange={(e) => handleFileSelect(e.target.files)} />
|
|
814
821
|
{macros.length > 0 && (
|
|
815
822
|
<select
|
|
816
823
|
className={s.cannedSelect}
|
|
@@ -871,6 +878,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
871
878
|
<div className={s.composerFooter}>
|
|
872
879
|
<div className={s.composerOptions}>
|
|
873
880
|
<select
|
|
881
|
+
aria-label={t('detail.sendAs')}
|
|
874
882
|
value={sendAsClient ? 'client' : 'admin'}
|
|
875
883
|
onChange={(e) => {
|
|
876
884
|
const asClient = e.target.value === 'client'
|
|
@@ -1089,9 +1097,9 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1089
1097
|
</div>
|
|
1090
1098
|
{/* Manual time entry */}
|
|
1091
1099
|
<div style={{ display: 'flex', gap: 6, marginTop: 8, alignItems: 'center' }}>
|
|
1092
|
-
<input type="number" min="1" placeholder="min" style={{ width: 60, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} id=
|
|
1100
|
+
<input type="number" min="1" placeholder="min" aria-label={t('detail.manualMinutes')} style={{ width: 60, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--theme-elevation-200)', fontSize: 12, color: 'var(--theme-text)', background: 'var(--theme-elevation-0)' }} id={manualTimeId} />
|
|
1093
1101
|
<button className={`${s.timerBtn} ${s.timerBtnGhost}`} onClick={async () => {
|
|
1094
|
-
const input = document.getElementById(
|
|
1102
|
+
const input = document.getElementById(manualTimeId) as HTMLInputElement
|
|
1095
1103
|
const mins = Number(input?.value)
|
|
1096
1104
|
if (!mins || mins < 1 || !ticketId) return
|
|
1097
1105
|
try {
|
|
@@ -1102,7 +1110,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1102
1110
|
}} style={{ fontSize: 11 }}>{t('detail.addTime')}</button>
|
|
1103
1111
|
</div>
|
|
1104
1112
|
{/* Billing info */}
|
|
1105
|
-
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--theme-elevation-
|
|
1113
|
+
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--theme-elevation-650)' }}>
|
|
1106
1114
|
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '2px 0', alignItems: 'center' }}>
|
|
1107
1115
|
<span>{t('detail.billing.billable')}</span>
|
|
1108
1116
|
<button
|
|
@@ -1126,7 +1134,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1126
1134
|
{timeEntries.length > 0 && (
|
|
1127
1135
|
<div style={{ marginTop: 8, fontSize: 11 }}>
|
|
1128
1136
|
{timeEntries.slice(0, 6).map((e) => (
|
|
1129
|
-
<div key={e.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 0', color: 'var(--theme-elevation-
|
|
1137
|
+
<div key={e.id} style={{ display: 'flex', justifyContent: 'space-between', padding: '3px 0', color: 'var(--theme-elevation-650)' }}>
|
|
1130
1138
|
<span>{new Date(e.date).toLocaleDateString(DATE_LOCALE, { day: 'numeric', month: 'short' })}</span>
|
|
1131
1139
|
<span title={e.description} style={{ fontWeight: 600, cursor: e.description ? 'help' : 'default' }}>{e.duration}min</span>
|
|
1132
1140
|
</div>
|
|
@@ -1149,6 +1157,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1149
1157
|
{addingTag ? (
|
|
1150
1158
|
<input
|
|
1151
1159
|
className={s.tagInput}
|
|
1160
|
+
aria-label={t('detail.addTag')}
|
|
1152
1161
|
value={newTagValue}
|
|
1153
1162
|
onChange={(e) => setNewTagValue(e.target.value)}
|
|
1154
1163
|
onKeyDown={(e) => { if (e.key === 'Enter') handleAddTag(); if (e.key === 'Escape') { setAddingTag(false); setNewTagValue('') } }}
|
|
@@ -1167,8 +1176,9 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1167
1176
|
<div className={s.sideSectionTitle}>{t('detail.billing.title')}</div>
|
|
1168
1177
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
1169
1178
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
1170
|
-
<
|
|
1179
|
+
<label style={{ fontSize: 12 }} htmlFor={billingTypeId}>{t('detail.billing.type')}</label>
|
|
1171
1180
|
<select
|
|
1181
|
+
id={billingTypeId}
|
|
1172
1182
|
value={(ticket as any)?.billingType || 'hourly'}
|
|
1173
1183
|
onChange={async (e) => {
|
|
1174
1184
|
try {
|
|
@@ -1188,9 +1198,10 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1188
1198
|
</div>
|
|
1189
1199
|
{(ticket as any)?.billingType === 'flat' && (
|
|
1190
1200
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
1191
|
-
<
|
|
1201
|
+
<label style={{ fontSize: 12 }} htmlFor={flatAmountId}>{t('detail.billing.flatAmount')}</label>
|
|
1192
1202
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
1193
1203
|
<input
|
|
1204
|
+
id={flatAmountId}
|
|
1194
1205
|
type="number"
|
|
1195
1206
|
defaultValue={(ticket as any)?.flatRateAmount ?? ''}
|
|
1196
1207
|
placeholder="0"
|
|
@@ -1212,9 +1223,10 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1212
1223
|
</div>
|
|
1213
1224
|
)}
|
|
1214
1225
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
1215
|
-
<
|
|
1226
|
+
<label style={{ fontSize: 12 }} htmlFor={billedAmountId}>{t('detail.billing.billedAmount')}</label>
|
|
1216
1227
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
1217
1228
|
<input
|
|
1229
|
+
id={billedAmountId}
|
|
1218
1230
|
type="number"
|
|
1219
1231
|
defaultValue={(ticket as any)?.billedAmount ?? ''}
|
|
1220
1232
|
placeholder="0"
|
|
@@ -1235,8 +1247,9 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1235
1247
|
</div>
|
|
1236
1248
|
</div>
|
|
1237
1249
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
1238
|
-
<
|
|
1250
|
+
<label style={{ fontSize: 12 }} htmlFor={paymentStatusId}>{t('detail.billing.payment')}</label>
|
|
1239
1251
|
<select
|
|
1252
|
+
id={paymentStatusId}
|
|
1240
1253
|
value={(ticket as any)?.paymentStatus || 'unpaid'}
|
|
1241
1254
|
onChange={async (e) => {
|
|
1242
1255
|
try {
|
|
@@ -1358,6 +1371,7 @@ const [clientTyping, setClientTyping] = useState(false)
|
|
|
1358
1371
|
<div className={s.splitPreview}>{splitModal.preview}</div>
|
|
1359
1372
|
<input
|
|
1360
1373
|
className={s.splitInput}
|
|
1374
|
+
aria-label={t('detail.split.subjectLabel')}
|
|
1361
1375
|
value={splitSubject}
|
|
1362
1376
|
onChange={(e) => setSplitSubject(e.target.value)}
|
|
1363
1377
|
onKeyDown={(e) => { if (e.key === 'Enter') handleSplitConfirm() }}
|
|
@@ -31,6 +31,9 @@ interface Ticket {
|
|
|
31
31
|
|
|
32
32
|
type Tab = 'all' | 'open' | 'waiting_client' | 'resolved' | 'sla_breach'
|
|
33
33
|
|
|
34
|
+
// Single id: this view is a full admin route, mounted once.
|
|
35
|
+
const INBOX_TABPANEL_ID = 'inbox-tabpanel'
|
|
36
|
+
|
|
34
37
|
const PRIORITY_COLORS: Record<string, string> = {
|
|
35
38
|
urgent: 'var(--theme-error-500)',
|
|
36
39
|
high: 'var(--theme-warning-500)',
|
|
@@ -239,6 +242,7 @@ export const TicketInboxClient: React.FC = () => {
|
|
|
239
242
|
<input
|
|
240
243
|
type="text"
|
|
241
244
|
className={s.searchInput}
|
|
245
|
+
aria-label={t('inbox.searchLabel')}
|
|
242
246
|
placeholder={t('inbox.searchPlaceholder')}
|
|
243
247
|
value={search}
|
|
244
248
|
onChange={(e) => setSearch(e.target.value)}
|
|
@@ -250,14 +254,48 @@ export const TicketInboxClient: React.FC = () => {
|
|
|
250
254
|
</div>
|
|
251
255
|
|
|
252
256
|
{/* Tabs */}
|
|
253
|
-
|
|
257
|
+
{/*
|
|
258
|
+
A tablist is ONE tab stop: the inactive tabs carry `tabIndex={-1}` and
|
|
259
|
+
the arrow keys move between them. That is the ARIA pattern, but the
|
|
260
|
+
roving tabIndex is only half of it — without this handler the four
|
|
261
|
+
inactive tabs are simply unreachable from the keyboard, which is worse
|
|
262
|
+
than the plain tab order it replaced.
|
|
263
|
+
*/}
|
|
264
|
+
<div
|
|
265
|
+
className={s.tabs}
|
|
266
|
+
role="tablist"
|
|
267
|
+
aria-label={t('inbox.tabsLabel')}
|
|
268
|
+
onKeyDown={(e) => {
|
|
269
|
+
const keys = ['ArrowLeft', 'ArrowRight', 'Home', 'End']
|
|
270
|
+
if (!keys.includes(e.key)) return
|
|
271
|
+
e.preventDefault()
|
|
272
|
+
const i = tabs.findIndex((tk) => tk.key === tab)
|
|
273
|
+
const next =
|
|
274
|
+
e.key === 'Home' ? 0
|
|
275
|
+
: e.key === 'End' ? tabs.length - 1
|
|
276
|
+
: e.key === 'ArrowLeft' ? (i - 1 + tabs.length) % tabs.length
|
|
277
|
+
: (i + 1) % tabs.length
|
|
278
|
+
const target = tabs[next]
|
|
279
|
+
if (!target) return
|
|
280
|
+
setTab(target.key)
|
|
281
|
+
setSelectedIdx(-1)
|
|
282
|
+
// Focus follows selection, as the pattern expects for an
|
|
283
|
+
// automatically-activated tablist.
|
|
284
|
+
document.getElementById(`inbox-tab-${target.key}`)?.focus()
|
|
285
|
+
}}
|
|
286
|
+
>
|
|
254
287
|
{tabs.map((tk) => {
|
|
255
288
|
const isAlert = tk.alert && tk.count > 0
|
|
256
289
|
return (
|
|
257
290
|
<button
|
|
258
291
|
key={tk.key}
|
|
292
|
+
type="button"
|
|
259
293
|
role="tab"
|
|
294
|
+
id={`inbox-tab-${tk.key}`}
|
|
260
295
|
aria-selected={tab === tk.key}
|
|
296
|
+
aria-controls={INBOX_TABPANEL_ID}
|
|
297
|
+
// Roving tabindex: a tablist is one tab stop, arrows move inside.
|
|
298
|
+
tabIndex={tab === tk.key ? 0 : -1}
|
|
261
299
|
className={`${s.tab} ${tab === tk.key ? s.tabActive : ''} ${isAlert ? s.tabAlert : ''}`}
|
|
262
300
|
onClick={() => { setTab(tk.key); setSelectedIdx(-1) }}
|
|
263
301
|
>
|
|
@@ -273,17 +311,17 @@ export const TicketInboxClient: React.FC = () => {
|
|
|
273
311
|
{checkedIds.size > 0 ? (
|
|
274
312
|
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flex: 1 }}>
|
|
275
313
|
<span style={{ fontSize: 13, fontWeight: 600, color: 'var(--theme-text)' }}>{checkedIds.size > 1 ? t('inbox.selectedPlural', { count: String(checkedIds.size) }) : t('inbox.selected', { count: String(checkedIds.size) })}</span>
|
|
276
|
-
<button className={s.sortSelect} onClick={() => handleBulkAction('close')} disabled={bulkProcessing}>{t('inbox.closeAction')}</button>
|
|
277
|
-
<button className={s.sortSelect} onClick={() => handleBulkAction('reopen')} disabled={bulkProcessing}>{t('inbox.reopenAction')}</button>
|
|
278
|
-
<select className={s.sortSelect} value={bulkAction} onChange={(e) => { if (e.target.value) handleBulkAction(e.target.value); setBulkAction('') }}>
|
|
314
|
+
<button type="button" className={s.sortSelect} onClick={() => handleBulkAction('close')} disabled={bulkProcessing}>{t('inbox.closeAction')}</button>
|
|
315
|
+
<button type="button" className={s.sortSelect} onClick={() => handleBulkAction('reopen')} disabled={bulkProcessing}>{t('inbox.reopenAction')}</button>
|
|
316
|
+
<select className={s.sortSelect} aria-label={t('inbox.moreActions')} value={bulkAction} onChange={(e) => { if (e.target.value) handleBulkAction(e.target.value); setBulkAction('') }}>
|
|
279
317
|
<option value="">{t('inbox.moreActions')}</option>
|
|
280
318
|
<option value="set_priority">{t('inbox.changePriority')}</option>
|
|
281
319
|
<option value="delete">{t('inbox.deleteAction')}</option>
|
|
282
320
|
</select>
|
|
283
|
-
<button className={s.sortSelect} onClick={() => setCheckedIds(new Set())} style={{ marginLeft: 'auto' }}>{t('inbox.deselect')}</button>
|
|
321
|
+
<button type="button" className={s.sortSelect} onClick={() => setCheckedIds(new Set())} style={{ marginLeft: 'auto' }}>{t('inbox.deselect')}</button>
|
|
284
322
|
</div>
|
|
285
323
|
) : (
|
|
286
|
-
<select className={s.sortSelect} value={sort} onChange={(e) => setSort(e.target.value)}>
|
|
324
|
+
<select className={s.sortSelect} aria-label={t('inbox.sortLabel')} value={sort} onChange={(e) => setSort(e.target.value)}>
|
|
287
325
|
<option value="-updatedAt">{t('inbox.sort.newest')}</option>
|
|
288
326
|
<option value="updatedAt">{t('inbox.sort.oldest')}</option>
|
|
289
327
|
<option value="-createdAt">{t('inbox.sort.created')}</option>
|
|
@@ -292,7 +330,8 @@ export const TicketInboxClient: React.FC = () => {
|
|
|
292
330
|
)}
|
|
293
331
|
</div>
|
|
294
332
|
|
|
295
|
-
{/* List */}
|
|
333
|
+
{/* List — the panel the tablist above controls */}
|
|
334
|
+
<div role="tabpanel" id={INBOX_TABPANEL_ID} aria-labelledby={`inbox-tab-${tab}`}>
|
|
296
335
|
{loading ? (
|
|
297
336
|
<div className={s.loading}>{t('common.loading')}</div>
|
|
298
337
|
) : tickets.length === 0 ? (
|
|
@@ -361,6 +400,7 @@ export const TicketInboxClient: React.FC = () => {
|
|
|
361
400
|
})}
|
|
362
401
|
</div>
|
|
363
402
|
)}
|
|
403
|
+
</div>
|
|
364
404
|
|
|
365
405
|
{/* Keyboard hints */}
|
|
366
406
|
<div className={s.keyboardHints}>
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
|
|
71
71
|
.sectionDescription {
|
|
72
72
|
font-size: 0.8125rem;
|
|
73
|
-
color: var(--theme-elevation-
|
|
73
|
+
color: var(--theme-elevation-650);
|
|
74
74
|
margin: 0 0 1rem;
|
|
75
75
|
line-height: 1.5;
|
|
76
76
|
}
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
|
|
144
144
|
.featureDesc {
|
|
145
145
|
font-size: 0.75rem;
|
|
146
|
-
color: var(--theme-elevation-
|
|
146
|
+
color: var(--theme-elevation-650);
|
|
147
147
|
margin-top: 0.125rem;
|
|
148
148
|
}
|
|
149
149
|
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
|
|
167
167
|
.fieldDescription {
|
|
168
168
|
font-size: 0.75rem;
|
|
169
|
-
color: var(--theme-elevation-
|
|
169
|
+
color: var(--theme-elevation-650);
|
|
170
170
|
font-weight: 400;
|
|
171
171
|
margin-top: 0.125rem;
|
|
172
172
|
}
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
.inputReadonly {
|
|
195
195
|
composes: input;
|
|
196
196
|
background: var(--theme-elevation-50);
|
|
197
|
-
color: var(--theme-elevation-
|
|
197
|
+
color: var(--theme-elevation-650);
|
|
198
198
|
cursor: not-allowed;
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
|
|
231
231
|
.inlineDesc {
|
|
232
232
|
font-size: 0.75rem;
|
|
233
|
-
color: var(--theme-elevation-
|
|
233
|
+
color: var(--theme-elevation-650);
|
|
234
234
|
margin-top: 0.125rem;
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
|
|
245
245
|
.slaHint {
|
|
246
246
|
font-size: 0.75rem;
|
|
247
|
-
color: var(--theme-elevation-
|
|
247
|
+
color: var(--theme-elevation-650);
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// ── API Key ───────────────────────────────────
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
.aiToggleDesc {
|
|
291
291
|
display: block;
|
|
292
292
|
font-size: 0.6875rem;
|
|
293
|
-
color: var(--theme-elevation-
|
|
293
|
+
color: var(--theme-elevation-650);
|
|
294
294
|
}
|
|
295
295
|
|
|
296
296
|
// ── Signature ─────────────────────────────────
|