@adatechnology/conversations-ui 0.1.0-rc.24 → 0.1.0-rc.26

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.
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Tela completa de "Mensagens": mensagens do bot, tópicos, templates do WhatsApp e transcrição.
3
+ *
4
+ * Os formulários daqui já eram do pacote, mas cada produto remontava as abas, o estado e o
5
+ * salvamento por conta — e foi exatamente aí que divergiram (o financiamento tinha tópicos e
6
+ * criação de template, o QuickCart tinha transcrição, e nenhum dos dois tinha tudo). Esta tela
7
+ * é a composição: recebe funções assíncronas e devolve a tela inteira.
8
+ *
9
+ * Tudo além das mensagens de boas-vindas/despedida é **opcional por capacidade**: sem
10
+ * `getTopics`, a seção de tópicos não existe; sem `listTemplates`, a aba de templates não
11
+ * aparece; sem `createTemplate`, o submenu de criação some. Nada de aba que abre em branco.
12
+ */
13
+
14
+ import { useCallback, useEffect, useState, type FormEvent, type ReactNode } from 'react'
15
+ import { WelcomeFarewellForm, type WelcomeFarewellFormLabels } from './WelcomeFarewellForm'
16
+ import { TopicsForm, type TopicItem, type TopicsFormLabels } from './TopicsForm'
17
+ import {
18
+ WhatsAppTemplatesSettings,
19
+ type WhatsAppTemplatesSettingsLabels,
20
+ } from './WhatsAppTemplatesSettings'
21
+ import type {
22
+ WhatsAppTemplateSummary,
23
+ WhatsAppTemplateVariableSuggestion,
24
+ WhatsAppTemplateSettingsFormLabels,
25
+ } from './WhatsAppTemplateSettingsForm'
26
+ import type {
27
+ WhatsAppCreateTemplateFormLabels,
28
+ WhatsAppCreateTemplateResult,
29
+ WhatsAppCreateTemplateState,
30
+ } from './WhatsAppCreateTemplateForm'
31
+ import {
32
+ TranscriptionSettingsForm,
33
+ type TranscriptionSettingsFormLabels,
34
+ } from './TranscriptionSettingsForm'
35
+ import type { TranscriptionMode } from '../types'
36
+
37
+ export interface BotMessages {
38
+ welcomeMessage: string
39
+ farewellMessage: string
40
+ }
41
+
42
+ export interface TemplateSettings {
43
+ templateName: string
44
+ templateLanguage: string
45
+ variables: string[]
46
+ }
47
+
48
+ export interface TranscriptionSettings {
49
+ enabled: boolean
50
+ mode: TranscriptionMode
51
+ available: boolean
52
+ }
53
+
54
+ export interface MessagesWorkspaceApi {
55
+ getMessages(): Promise<BotMessages>
56
+ saveMessages(messages: BotMessages): Promise<void>
57
+ /** Sem este par, a seção de tópicos não é desenhada. */
58
+ getTopics?(): Promise<TopicItem[]>
59
+ saveTopics?(topics: TopicItem[]): Promise<void>
60
+ /** Sem este par, a aba de templates não é desenhada. */
61
+ getTemplateSettings?(): Promise<TemplateSettings>
62
+ saveTemplateSettings?(settings: TemplateSettings): Promise<void>
63
+ /**
64
+ * Lista de templates aprovados na Meta. Ausente, a aba ainda existe (dá para salvar o nome
65
+ * escolhido), mas nasce sem opções e sem botão de recarregar.
66
+ */
67
+ listTemplates?(): Promise<WhatsAppTemplateSummary[]>
68
+ /** Sem isto, o submenu "Criar template" some. */
69
+ createTemplate?(input: WhatsAppCreateTemplateState): Promise<WhatsAppCreateTemplateResult>
70
+ /** Sem este par, a aba de transcrição não é desenhada. */
71
+ getTranscription?(): Promise<TranscriptionSettings>
72
+ saveTranscription?(settings: Omit<TranscriptionSettings, 'available'>): Promise<void>
73
+ }
74
+
75
+ export interface MessagesWorkspaceLabels {
76
+ title: string
77
+ subtitle: string
78
+ loading: string
79
+ loadError: string
80
+ tabBot: string
81
+ tabTemplates: string
82
+ tabTranscription: string
83
+ welcomeFarewell: Partial<WelcomeFarewellFormLabels>
84
+ topics: Partial<TopicsFormLabels>
85
+ templates: Partial<WhatsAppTemplatesSettingsLabels>
86
+ templateSettings: Partial<WhatsAppTemplateSettingsFormLabels>
87
+ createTemplate: Partial<WhatsAppCreateTemplateFormLabels>
88
+ transcription: Partial<TranscriptionSettingsFormLabels>
89
+ }
90
+
91
+ const DEFAULT_LABELS: MessagesWorkspaceLabels = {
92
+ title: 'Mensagens',
93
+ subtitle: 'Mensagens do bot e templates do WhatsApp.',
94
+ loading: 'Carregando configurações…',
95
+ loadError: 'Não foi possível carregar as configurações de mensagens.',
96
+ tabBot: 'Mensagens do Bot',
97
+ tabTemplates: 'Templates WhatsApp',
98
+ tabTranscription: 'Transcrição de áudio',
99
+ welcomeFarewell: {},
100
+ topics: {},
101
+ templates: {},
102
+ templateSettings: {},
103
+ createTemplate: {},
104
+ transcription: {},
105
+ }
106
+
107
+ const MESSAGES_TAB = {
108
+ BOT: 'bot',
109
+ TEMPLATES: 'templates',
110
+ TRANSCRIPTION: 'transcription',
111
+ } as const
112
+ type MessagesTab = (typeof MESSAGES_TAB)[keyof typeof MESSAGES_TAB]
113
+
114
+ const EMPTY_MESSAGES: BotMessages = { welcomeMessage: '', farewellMessage: '' }
115
+ const EMPTY_TEMPLATE_SETTINGS: TemplateSettings = {
116
+ templateName: '',
117
+ templateLanguage: 'pt_BR',
118
+ variables: [],
119
+ }
120
+ const EMPTY_CREATE_TEMPLATE: WhatsAppCreateTemplateState = {
121
+ name: '',
122
+ category: 'UTILITY',
123
+ language: 'pt_BR',
124
+ headerType: 'NONE',
125
+ headerText: '',
126
+ bodyText: '',
127
+ footerText: '',
128
+ }
129
+
130
+ /**
131
+ * Padrão da instalação quando ninguém decidiu ainda.
132
+ *
133
+ * O servidor guarda `null` de propósito (herda o ambiente), mas o interruptor é booleano —
134
+ * `false` é a leitura honesta de "ninguém ligou aqui"; marcado sugeriria escolha que não houve.
135
+ */
136
+ const UNDECIDED_TRANSCRIPTION: TranscriptionSettings = {
137
+ enabled: false,
138
+ mode: 'onDemand',
139
+ available: false,
140
+ }
141
+
142
+ const SAVE_FEEDBACK_MS = 3000
143
+
144
+ export interface MessagesWorkspaceProps {
145
+ readonly api: MessagesWorkspaceApi
146
+ readonly labels?: Partial<MessagesWorkspaceLabels>
147
+ readonly welcomePlaceholders?: readonly string[]
148
+ readonly farewellPlaceholders?: readonly string[]
149
+ readonly availableVariables?: WhatsAppTemplateVariableSuggestion[]
150
+ /** Aviso do produto acima da aba de templates (ex.: rota da Graph API ainda não implementada). */
151
+ readonly renderTemplatesNotice?: () => ReactNode
152
+ readonly className?: string
153
+ }
154
+
155
+ export function MessagesWorkspace({
156
+ api,
157
+ labels: labelsOverride,
158
+ welcomePlaceholders,
159
+ farewellPlaceholders,
160
+ availableVariables,
161
+ renderTemplatesNotice,
162
+ className,
163
+ }: MessagesWorkspaceProps) {
164
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride }
165
+
166
+ const hasTopics = Boolean(api.getTopics && api.saveTopics)
167
+ const hasTemplates = Boolean(api.getTemplateSettings && api.saveTemplateSettings)
168
+ const hasTranscription = Boolean(api.getTranscription && api.saveTranscription)
169
+
170
+ const [tab, setTab] = useState<MessagesTab>(MESSAGES_TAB.BOT)
171
+ const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
172
+
173
+ const [messages, setMessages] = useState<BotMessages>(EMPTY_MESSAGES)
174
+ const [savingMessages, setSavingMessages] = useState(false)
175
+ const [messagesSaved, setMessagesSaved] = useState(false)
176
+
177
+ const [topics, setTopics] = useState<TopicItem[]>([])
178
+ const [savingTopics, setSavingTopics] = useState(false)
179
+ const [topicsSaved, setTopicsSaved] = useState(false)
180
+
181
+ const [templateSettings, setTemplateSettings] = useState<TemplateSettings>(EMPTY_TEMPLATE_SETTINGS)
182
+ const [templates, setTemplates] = useState<WhatsAppTemplateSummary[]>([])
183
+ const [loadingTemplates, setLoadingTemplates] = useState(false)
184
+ const [templatesError, setTemplatesError] = useState(false)
185
+ const [savingTemplate, setSavingTemplate] = useState(false)
186
+ const [templateSaved, setTemplateSaved] = useState(false)
187
+
188
+ const [createTemplate, setCreateTemplate] = useState<WhatsAppCreateTemplateState>(EMPTY_CREATE_TEMPLATE)
189
+ const [creatingTemplate, setCreatingTemplate] = useState(false)
190
+ const [createResult, setCreateResult] = useState<WhatsAppCreateTemplateResult | null>(null)
191
+
192
+ const [transcription, setTranscription] = useState<TranscriptionSettings>(UNDECIDED_TRANSCRIPTION)
193
+ const [savingTranscription, setSavingTranscription] = useState(false)
194
+ const [transcriptionSaved, setTranscriptionSaved] = useState(false)
195
+
196
+ const reloadTemplates = useCallback(async () => {
197
+ if (!api.listTemplates) return
198
+ setLoadingTemplates(true)
199
+ setTemplatesError(false)
200
+ try {
201
+ setTemplates(await api.listTemplates())
202
+ } catch {
203
+ setTemplatesError(true)
204
+ } finally {
205
+ setLoadingTemplates(false)
206
+ }
207
+ }, [api])
208
+
209
+ useEffect(() => {
210
+ let active = true
211
+ async function load(): Promise<void> {
212
+ try {
213
+ const [loadedMessages, loadedTopics, loadedTemplateSettings, loadedTranscription] = await Promise.all([
214
+ api.getMessages(),
215
+ api.getTopics?.(),
216
+ api.getTemplateSettings?.(),
217
+ api.getTranscription?.(),
218
+ ])
219
+ if (!active) return
220
+ setMessages(loadedMessages)
221
+ if (loadedTopics) setTopics(loadedTopics)
222
+ if (loadedTemplateSettings) setTemplateSettings(loadedTemplateSettings)
223
+ if (loadedTranscription) setTranscription(loadedTranscription)
224
+ setLoadState('ready')
225
+ } catch {
226
+ if (active) setLoadState('error')
227
+ }
228
+ }
229
+ void load()
230
+ return () => {
231
+ active = false
232
+ }
233
+ }, [api])
234
+
235
+ // A lista de templates é buscada à parte porque falha sozinha: a Graph API cai, ou a rota nem
236
+ // existe no produto, e nada disso pode impedir a tela de abrir para salvar o resto.
237
+ useEffect(() => {
238
+ void reloadTemplates()
239
+ }, [reloadTemplates])
240
+
241
+ function flash(setter: (value: boolean) => void): void {
242
+ setter(true)
243
+ setTimeout(() => setter(false), SAVE_FEEDBACK_MS)
244
+ }
245
+
246
+ async function handleSaveMessages(event: FormEvent): Promise<void> {
247
+ event.preventDefault()
248
+ setSavingMessages(true)
249
+ try {
250
+ await api.saveMessages(messages)
251
+ flash(setMessagesSaved)
252
+ } finally {
253
+ setSavingMessages(false)
254
+ }
255
+ }
256
+
257
+ async function handleSaveTopics(event: FormEvent): Promise<void> {
258
+ event.preventDefault()
259
+ if (!api.saveTopics) return
260
+ setSavingTopics(true)
261
+ try {
262
+ await api.saveTopics(topics)
263
+ flash(setTopicsSaved)
264
+ } finally {
265
+ setSavingTopics(false)
266
+ }
267
+ }
268
+
269
+ async function handleSaveTemplate(event: FormEvent): Promise<void> {
270
+ event.preventDefault()
271
+ if (!api.saveTemplateSettings) return
272
+ setSavingTemplate(true)
273
+ try {
274
+ await api.saveTemplateSettings(templateSettings)
275
+ flash(setTemplateSaved)
276
+ } finally {
277
+ setSavingTemplate(false)
278
+ }
279
+ }
280
+
281
+ async function handleCreateTemplate(event: FormEvent): Promise<void> {
282
+ event.preventDefault()
283
+ if (!api.createTemplate) return
284
+ setCreatingTemplate(true)
285
+ setCreateResult(null)
286
+ try {
287
+ const result = await api.createTemplate(createTemplate)
288
+ setCreateResult(result)
289
+ if (result.ok) {
290
+ setCreateTemplate(EMPTY_CREATE_TEMPLATE)
291
+ void reloadTemplates()
292
+ }
293
+ } catch (error) {
294
+ setCreateResult({ ok: false, message: error instanceof Error ? error.message : '' })
295
+ } finally {
296
+ setCreatingTemplate(false)
297
+ }
298
+ }
299
+
300
+ async function handleSaveTranscription(event: FormEvent): Promise<void> {
301
+ event.preventDefault()
302
+ if (!api.saveTranscription) return
303
+ setSavingTranscription(true)
304
+ try {
305
+ await api.saveTranscription({ enabled: transcription.enabled, mode: transcription.mode })
306
+ flash(setTranscriptionSaved)
307
+ } finally {
308
+ setSavingTranscription(false)
309
+ }
310
+ }
311
+
312
+ function handleSelectTemplate(name: string, template: WhatsAppTemplateSummary | undefined): void {
313
+ setTemplateSettings((previous) => {
314
+ if (!template) return { ...previous, templateName: name }
315
+ // Escolher um template com variáveis e deixar a lista vazia é o erro clássico: o envio
316
+ // vai à Meta sem os parâmetros e volta rejeitado. Pré-abrir os campos evita isso — mas só
317
+ // quando ninguém preencheu nada ainda, para não apagar mapeamento existente.
318
+ const shouldSeedVariables = template.variableCount > 0 && previous.variables.length === 0
319
+ return {
320
+ templateName: name,
321
+ templateLanguage: template.language,
322
+ variables: shouldSeedVariables
323
+ ? Array.from({ length: template.variableCount }, () => '')
324
+ : previous.variables,
325
+ }
326
+ })
327
+ }
328
+
329
+ if (loadState === 'loading') {
330
+ return <p className="p-6 text-sm text-gray-500 dark:text-gray-400">{labels.loading}</p>
331
+ }
332
+ if (loadState === 'error') {
333
+ return (
334
+ <p role="alert" className="p-6 text-sm text-red-600 dark:text-red-400">
335
+ {labels.loadError}
336
+ </p>
337
+ )
338
+ }
339
+
340
+ const tabs: { id: MessagesTab; label: string }[] = [
341
+ { id: MESSAGES_TAB.BOT, label: labels.tabBot },
342
+ ...(hasTemplates ? [{ id: MESSAGES_TAB.TEMPLATES, label: labels.tabTemplates }] : []),
343
+ ...(hasTranscription ? [{ id: MESSAGES_TAB.TRANSCRIPTION, label: labels.tabTranscription }] : []),
344
+ ]
345
+
346
+ return (
347
+ <div className={`space-y-6 p-4 lg:p-0 ${className ?? ''}`}>
348
+ <header>
349
+ <h1 className="text-xl font-semibold text-gray-900 dark:text-gray-100">{labels.title}</h1>
350
+ <p className="text-sm text-gray-500 dark:text-gray-400">{labels.subtitle}</p>
351
+ </header>
352
+
353
+ {/* Uma aba só não é navegação: vira ruído com aparência de escolha. */}
354
+ {tabs.length > 1 ? (
355
+ <nav className="flex gap-1 border-b border-gray-200 dark:border-gray-700" role="tablist">
356
+ {tabs.map((item) => (
357
+ <button
358
+ key={item.id}
359
+ type="button"
360
+ role="tab"
361
+ aria-selected={tab === item.id}
362
+ onClick={() => setTab(item.id)}
363
+ className={`-mb-px border-b-2 px-4 py-2.5 text-sm font-medium transition-colors ${
364
+ tab === item.id
365
+ ? 'border-blue-600 text-blue-700 dark:border-blue-400 dark:text-blue-400'
366
+ : 'border-transparent text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200'
367
+ }`}
368
+ >
369
+ {item.label}
370
+ </button>
371
+ ))}
372
+ </nav>
373
+ ) : null}
374
+
375
+ {tab === MESSAGES_TAB.BOT ? (
376
+ <div className="space-y-8">
377
+ <WelcomeFarewellForm
378
+ welcomeMessage={messages.welcomeMessage}
379
+ onWelcomeMessageChange={(welcomeMessage) =>
380
+ setMessages((previous) => ({ ...previous, welcomeMessage }))
381
+ }
382
+ farewellMessage={messages.farewellMessage}
383
+ onFarewellMessageChange={(farewellMessage) =>
384
+ setMessages((previous) => ({ ...previous, farewellMessage }))
385
+ }
386
+ onSave={handleSaveMessages}
387
+ saving={savingMessages}
388
+ saveSuccess={messagesSaved}
389
+ labels={labels.welcomeFarewell}
390
+ {...(welcomePlaceholders ? { welcomePlaceholders } : {})}
391
+ {...(farewellPlaceholders ? { farewellPlaceholders } : {})}
392
+ />
393
+
394
+ {hasTopics ? (
395
+ <TopicsForm
396
+ topics={topics}
397
+ onToggle={(key) =>
398
+ setTopics((previous) =>
399
+ previous.map((topic) => (topic.key === key ? { ...topic, enabled: !topic.enabled } : topic)),
400
+ )
401
+ }
402
+ onMessageChange={(key, message) =>
403
+ setTopics((previous) =>
404
+ previous.map((topic) => (topic.key === key ? { ...topic, message } : topic)),
405
+ )
406
+ }
407
+ onSave={handleSaveTopics}
408
+ saving={savingTopics}
409
+ saveSuccess={topicsSaved}
410
+ labels={labels.topics}
411
+ />
412
+ ) : null}
413
+ </div>
414
+ ) : null}
415
+
416
+ {tab === MESSAGES_TAB.TEMPLATES ? (
417
+ <div className="space-y-4">
418
+ {renderTemplatesNotice?.()}
419
+ <WhatsAppTemplatesSettings
420
+ templates={templates}
421
+ loadingTemplates={loadingTemplates}
422
+ templatesError={templatesError}
423
+ selectedTemplateName={templateSettings.templateName}
424
+ onSelectTemplate={handleSelectTemplate}
425
+ variables={templateSettings.variables}
426
+ onVariablesChange={(variables) => setTemplateSettings((previous) => ({ ...previous, variables }))}
427
+ onSave={handleSaveTemplate}
428
+ saving={savingTemplate}
429
+ saveSuccess={templateSaved}
430
+ labels={labels.templates}
431
+ settingsLabels={labels.templateSettings}
432
+ {...(api.listTemplates ? { onRefreshTemplates: () => void reloadTemplates() } : {})}
433
+ {...(availableVariables ? { availableVariables } : {})}
434
+ {...(api.createTemplate
435
+ ? {
436
+ create: {
437
+ value: createTemplate,
438
+ onChange: setCreateTemplate,
439
+ onSubmit: handleCreateTemplate,
440
+ submitting: creatingTemplate,
441
+ result: createResult,
442
+ labels: labels.createTemplate,
443
+ },
444
+ }
445
+ : {})}
446
+ />
447
+ </div>
448
+ ) : null}
449
+
450
+ {tab === MESSAGES_TAB.TRANSCRIPTION ? (
451
+ <TranscriptionSettingsForm
452
+ enabled={transcription.enabled}
453
+ onEnabledChange={(enabled) => setTranscription((previous) => ({ ...previous, enabled }))}
454
+ mode={transcription.mode}
455
+ onModeChange={(mode) => setTranscription((previous) => ({ ...previous, mode }))}
456
+ onSave={handleSaveTranscription}
457
+ isAvailable={transcription.available}
458
+ saving={savingTranscription}
459
+ saveSuccess={transcriptionSaved}
460
+ labels={labels.transcription}
461
+ />
462
+ ) : null}
463
+ </div>
464
+ )
465
+ }
@@ -13,9 +13,11 @@ import {
13
13
  WhatsAppTemplateSettingsForm,
14
14
  type WhatsAppTemplateSummary,
15
15
  type WhatsAppTemplateVariableSuggestion,
16
+ type WhatsAppTemplateSettingsFormLabels,
16
17
  } from './WhatsAppTemplateSettingsForm'
17
18
  import {
18
19
  WhatsAppCreateTemplateForm,
20
+ type WhatsAppCreateTemplateFormLabels,
19
21
  type WhatsAppCreateTemplateResult,
20
22
  type WhatsAppCreateTemplateState,
21
23
  } from './WhatsAppCreateTemplateForm'
@@ -56,12 +58,16 @@ export interface WhatsAppTemplatesSettingsProps {
56
58
  onSubmit: (event: FormEvent) => void
57
59
  submitting?: boolean
58
60
  result?: WhatsAppCreateTemplateResult | null
61
+ labels?: Partial<WhatsAppCreateTemplateFormLabels>
59
62
  }
60
63
  labels?: Partial<WhatsAppTemplatesSettingsLabels>
64
+ /** Vocabulário do formulário de seleção — separado de `labels`, que nomeia só as abas daqui. */
65
+ settingsLabels?: Partial<WhatsAppTemplateSettingsFormLabels>
61
66
  }
62
67
 
63
68
  export function WhatsAppTemplatesSettings({
64
69
  labels: labelsOverride,
70
+ settingsLabels,
65
71
  create,
66
72
  ...settingsProps
67
73
  }: WhatsAppTemplatesSettingsProps) {
@@ -97,7 +103,7 @@ export function WhatsAppTemplatesSettings({
97
103
  </nav>
98
104
 
99
105
  {tab === TEMPLATE_SETTINGS_TAB.SELECT ? (
100
- <WhatsAppTemplateSettingsForm {...settingsProps} />
106
+ <WhatsAppTemplateSettingsForm {...settingsProps} {...(settingsLabels ? { labels: settingsLabels } : {})} />
101
107
  ) : create ? (
102
108
  <WhatsAppCreateTemplateForm {...create} />
103
109
  ) : null}
package/src/styles.css CHANGED
@@ -772,3 +772,73 @@
772
772
  .dark .cv-workspace-attachments li {
773
773
  border-color: rgb(51 65 85);
774
774
  }
775
+
776
+ /* Peças de listagem (documentos, mensagens): tabela com filtros, seleção em lote e paginação. */
777
+ .cv-filter-count {
778
+ display: inline-flex;
779
+ align-items: center;
780
+ justify-content: center;
781
+ min-width: 1rem;
782
+ height: 1rem;
783
+ padding: 0 0.25rem;
784
+ border-radius: 9999px;
785
+ background: rgb(37 99 235);
786
+ color: white;
787
+ font-size: 0.625rem;
788
+ font-weight: 600;
789
+ }
790
+ .cv-filter-menu {
791
+ position: absolute;
792
+ top: 100%;
793
+ left: 0;
794
+ z-index: 20;
795
+ margin-top: 0.25rem;
796
+ min-width: 12rem;
797
+ padding: 0.5rem;
798
+ border: 1px solid rgb(226 232 240);
799
+ border-radius: 0.5rem;
800
+ background: white;
801
+ box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
802
+ }
803
+ .dark .cv-filter-menu { background: rgb(30 41 59); border-color: rgb(71 85 105); }
804
+ .cv-filter-option {
805
+ display: flex;
806
+ align-items: center;
807
+ gap: 0.5rem;
808
+ padding: 0.375rem 0.5rem;
809
+ border-radius: 0.375rem;
810
+ font-size: 0.75rem;
811
+ cursor: pointer;
812
+ }
813
+ .cv-filter-option:hover { background: rgb(241 245 249); }
814
+ .dark .cv-filter-option:hover { background: rgb(51 65 85); }
815
+ .cv-bulk-bar {
816
+ display: flex;
817
+ flex-wrap: wrap;
818
+ align-items: center;
819
+ gap: 0.5rem;
820
+ padding: 0.5rem 0.75rem;
821
+ border: 1px solid rgb(191 219 254);
822
+ border-radius: 0.5rem;
823
+ background: rgb(239 246 255);
824
+ }
825
+ .dark .cv-bulk-bar { background: rgb(30 58 138 / 0.25); border-color: rgb(30 64 175); }
826
+ .cv-listing-pagination {
827
+ display: flex;
828
+ flex-wrap: wrap;
829
+ align-items: center;
830
+ justify-content: space-between;
831
+ gap: 0.5rem;
832
+ padding-top: 0.5rem;
833
+ border-top: 1px solid rgb(241 245 249);
834
+ }
835
+ .dark .cv-listing-pagination { border-color: rgb(30 41 59); }
836
+ .cv-listing-perpage {
837
+ height: 2rem;
838
+ padding: 0 0.5rem;
839
+ border: 1px solid rgb(226 232 240);
840
+ border-radius: 0.375rem;
841
+ background: transparent;
842
+ font-size: 0.75rem;
843
+ }
844
+ .dark .cv-listing-perpage { border-color: rgb(71 85 105); }