@adatechnology/conversations-ui 0.1.0-rc.32 → 0.1.0-rc.34
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/dist/index.d.ts +44 -1
- package/dist/index.js +340 -292
- package/package.json +2 -2
- package/src/DarkModeToggle.test.tsx +76 -0
- package/src/DarkModeToggle.tsx +92 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/conversations-ui",
|
|
3
|
-
"version": "0.1.0-rc.
|
|
3
|
+
"version": "0.1.0-rc.34",
|
|
4
4
|
"description": "WhatsApp conversation UI components — parametrizável por endpoint, tema e feature flags",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"clsx": "^2.1.1",
|
|
32
32
|
"lucide-react": "^1.21.0",
|
|
33
33
|
"tailwind-merge": "^3.6.0",
|
|
34
|
-
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.
|
|
34
|
+
"@adatechnology/meta-whatsapp-contracts": "0.2.0-rc.12"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"react": "^18 || ^19",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
|
|
4
|
+
import { DarkModeToggle } from './DarkModeToggle'
|
|
5
|
+
|
|
6
|
+
const noop = () => undefined
|
|
7
|
+
|
|
8
|
+
describe('DarkModeToggle', () => {
|
|
9
|
+
it('nomeia o destino do clique, não o tema atual', () => {
|
|
10
|
+
const markup = renderToStaticMarkup(<DarkModeToggle isDark={false} onToggle={noop} />)
|
|
11
|
+
|
|
12
|
+
expect(markup).toContain('aria-label="Tema escuro"')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it('inverte o rótulo quando o tema escuro está no ar', () => {
|
|
16
|
+
const markup = renderToStaticMarkup(<DarkModeToggle isDark onToggle={noop} />)
|
|
17
|
+
|
|
18
|
+
expect(markup).toContain('aria-label="Tema claro"')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('aceita rótulo do host', () => {
|
|
22
|
+
const markup = renderToStaticMarkup(
|
|
23
|
+
<DarkModeToggle isDark={false} onToggle={noop} labels={{ toDark: 'Modo noturno' }} />,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
expect(markup).toContain('aria-label="Modo noturno"')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('só-ícone garante a área de toque mínima', () => {
|
|
30
|
+
const markup = renderToStaticMarkup(<DarkModeToggle isDark={false} onToggle={noop} />)
|
|
31
|
+
|
|
32
|
+
expect(markup).toContain('cv-touch')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('com rótulo visível dispensa a área de toque forçada e o tooltip', () => {
|
|
36
|
+
const markup = renderToStaticMarkup(<DarkModeToggle isDark={false} onToggle={noop} showLabel />)
|
|
37
|
+
|
|
38
|
+
expect(markup).not.toContain('cv-touch')
|
|
39
|
+
expect(markup).not.toContain('data-cv-tooltip')
|
|
40
|
+
expect(markup).toContain('Tema escuro')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('deixa o host trocar a aparência sem herdar o utilitário do pacote', () => {
|
|
44
|
+
const markup = renderToStaticMarkup(
|
|
45
|
+
<DarkModeToggle isDark={false} onToggle={noop} className="rounded-2xl px-4" />,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
expect(markup).toContain('rounded-2xl')
|
|
49
|
+
expect(markup).not.toContain('rounded-lg')
|
|
50
|
+
expect(markup).not.toContain('px-3')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
/** Trava o contorno da limitação documentada: token do host não é família conhecida do merge. */
|
|
54
|
+
it('resolve raio de token do host quando ele vem na forma arbitrária', () => {
|
|
55
|
+
const markup = renderToStaticMarkup(
|
|
56
|
+
<DarkModeToggle isDark={false} onToggle={noop} className="rounded-[var(--radius-panel)]" />,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
expect(markup).not.toContain('rounded-lg')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('cede o tamanho do ícone quando o host manda classe', () => {
|
|
63
|
+
const markup = renderToStaticMarkup(
|
|
64
|
+
<DarkModeToggle isDark={false} onToggle={noop} classNames={{ icon: 'size-4' }} />,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
expect(markup).toContain('size-4')
|
|
68
|
+
expect(markup).not.toContain('width="16"')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('marca o ícone como decorativo — o rótulo já é anunciado', () => {
|
|
72
|
+
const markup = renderToStaticMarkup(<DarkModeToggle isDark={false} onToggle={noop} />)
|
|
73
|
+
|
|
74
|
+
expect(markup).toContain('aria-hidden="true"')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { Moon, Sun } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import { cn } from './lib/cn'
|
|
4
|
+
import { ICON_SIZE_ACTION } from './icon.constant'
|
|
5
|
+
|
|
6
|
+
export interface DarkModeToggleLabels {
|
|
7
|
+
/** Ação de ir para o tema escuro — é o rótulo lido enquanto o tema claro está ativo. */
|
|
8
|
+
toDark: string
|
|
9
|
+
toLight: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DarkModeToggleClassNames {
|
|
13
|
+
/** O tamanho do ícone vem daqui quando o host usa a própria escala (`size-4`, `size-5`). */
|
|
14
|
+
icon?: string
|
|
15
|
+
label?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DarkModeToggleProps {
|
|
19
|
+
isDark: boolean
|
|
20
|
+
onToggle: () => void
|
|
21
|
+
/** Rótulo ao lado do ícone. Só-ícone exige a área de toque mínima, que a classe `cv-touch` dá. */
|
|
22
|
+
showLabel?: boolean
|
|
23
|
+
labels?: Partial<DarkModeToggleLabels>
|
|
24
|
+
/**
|
|
25
|
+
* Substitui a aparência do botão. As classes passam por `tailwind-merge`, então utilitário do
|
|
26
|
+
* host vence o do pacote em vez de somar: `px-4` descarta o `px-3` daqui.
|
|
27
|
+
*
|
|
28
|
+
* A exceção é utilitário de token do host (`rounded-panel`), que o merge não reconhece como
|
|
29
|
+
* família e deixa passar junto com o `rounded-lg` do pacote — aí quem decide é a ordem no CSS
|
|
30
|
+
* gerado, não a do atributo. Nesse caso use a forma arbitrária (`rounded-[var(--radius-panel)]`),
|
|
31
|
+
* que o merge entende e substitui.
|
|
32
|
+
*/
|
|
33
|
+
className?: string
|
|
34
|
+
classNames?: DarkModeToggleClassNames
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_DARK_MODE_TOGGLE_LABELS: DarkModeToggleLabels = {
|
|
38
|
+
toDark: 'Tema escuro',
|
|
39
|
+
toLight: 'Tema claro',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Interruptor de tema, apresentacional: o estado vem do host, junto com `useDarkMode`.
|
|
44
|
+
*
|
|
45
|
+
* Ele não monta o hook de propósito. `useDarkMode` é o controlador — escreve a classe `dark` no
|
|
46
|
+
* `<html>` e persiste a escolha —, e uma barra lateral que também existe como gaveta fica montada
|
|
47
|
+
* duas vezes na mesma tela. Dois controladores guardam estados iniciais separados, e o segundo
|
|
48
|
+
* botão passa a mostrar o ícone contrário ao tema que está no ar. Com o estado no host, o hook é
|
|
49
|
+
* chamado uma vez acima dos dois e as duas cópias concordam.
|
|
50
|
+
*
|
|
51
|
+
* O rótulo nomeia o destino, não o estado atual: no claro se lê "Tema escuro", que é o que o clique
|
|
52
|
+
* faz. `aria-pressed` diria o oposto na mesma frase, então fica de fora.
|
|
53
|
+
*/
|
|
54
|
+
export function DarkModeToggle({
|
|
55
|
+
isDark,
|
|
56
|
+
onToggle,
|
|
57
|
+
showLabel = false,
|
|
58
|
+
labels,
|
|
59
|
+
className,
|
|
60
|
+
classNames,
|
|
61
|
+
}: DarkModeToggleProps) {
|
|
62
|
+
const label = isDark
|
|
63
|
+
? (labels?.toLight ?? DEFAULT_DARK_MODE_TOGGLE_LABELS.toLight)
|
|
64
|
+
: (labels?.toDark ?? DEFAULT_DARK_MODE_TOGGLE_LABELS.toDark)
|
|
65
|
+
|
|
66
|
+
const Icon = isDark ? Sun : Moon
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
type="button"
|
|
71
|
+
onClick={onToggle}
|
|
72
|
+
data-cv-tooltip={showLabel ? undefined : label}
|
|
73
|
+
aria-label={label}
|
|
74
|
+
className={cn(
|
|
75
|
+
'inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 px-3 py-2 text-sm text-gray-600 transition-colors',
|
|
76
|
+
'hover:bg-gray-100 hover:text-gray-900',
|
|
77
|
+
'dark:border-gray-700 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-100',
|
|
78
|
+
!showLabel && 'cv-touch',
|
|
79
|
+
className,
|
|
80
|
+
)}
|
|
81
|
+
>
|
|
82
|
+
{/* Sem `size` quando o host manda classe: a prop vira `width`/`height` no SVG e atributo
|
|
83
|
+
ganha da folha, deixando o ícone fora da escala de tipografia do host. */}
|
|
84
|
+
<Icon
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
className={cn('shrink-0', classNames?.icon)}
|
|
87
|
+
size={classNames?.icon ? undefined : ICON_SIZE_ACTION}
|
|
88
|
+
/>
|
|
89
|
+
{showLabel ? <span className={classNames?.label}>{label}</span> : null}
|
|
90
|
+
</button>
|
|
91
|
+
)
|
|
92
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -114,6 +114,8 @@ export { buildTranscriptText, buildTranscriptFilename, downloadTextFile } from '
|
|
|
114
114
|
export type { BuildTranscriptTextParams } from './conversationTranscript'
|
|
115
115
|
|
|
116
116
|
export { useDarkMode, useIsDarkTheme } from './useDarkMode'
|
|
117
|
+
export { DarkModeToggle, DEFAULT_DARK_MODE_TOGGLE_LABELS } from './DarkModeToggle'
|
|
118
|
+
export type { DarkModeToggleProps, DarkModeToggleLabels } from './DarkModeToggle'
|
|
117
119
|
export { useIsNarrow, NARROW_MAX_WIDTH_PX } from './useIsNarrow'
|
|
118
120
|
export { useWaitingNotifications } from './useWaitingNotifications'
|
|
119
121
|
export type {
|