@archbase/components 4.3.2 → 4.6.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.
@@ -0,0 +1,52 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ArchbaseReorderListProps<T> {
3
+ /** Itens na ordem corrente. Componente controlado. */
4
+ items: T[];
5
+ /** Identidade estavel do item. */
6
+ getItemId: (item: T) => string | number;
7
+ /** Nova ordem. Sem isto o componente nao teria como refletir o arrasto. */
8
+ onReorder: (items: T[]) => void;
9
+ /** Conteudo do item. Ausente, usa `getItemLabel`. */
10
+ renderItem?: (item: T, estado: {
11
+ arrastando: boolean;
12
+ indice: number;
13
+ }) => ReactNode;
14
+ /** Rotulo textual, usado no conteudo padrao e nos anuncios de acessibilidade. */
15
+ getItemLabel?: (item: T) => string;
16
+ /** Descricao secundaria no conteudo padrao. */
17
+ getItemDescription?: (item: T) => string | undefined;
18
+ onRemove?: (item: T) => void;
19
+ disabled?: boolean;
20
+ gap?: number;
21
+ className?: string;
22
+ labels?: {
23
+ dragHandle?: string;
24
+ remove?: string;
25
+ /** `{item}`, `{de}` e `{para}` sao substituidos. */
26
+ moved?: string;
27
+ instructions?: string;
28
+ };
29
+ }
30
+ /**
31
+ * Lista reordenavel por arrasto e por teclado.
32
+ *
33
+ * Unifica dois componentes do Lightswind UI (MIT, Muhilan / codewithMUHILAN) —
34
+ * `drag-order-list` e `draggable-reorder-list` — que resolviam o mesmo problema
35
+ * com implementacoes diferentes. Entregar os dois seria oferecer duas respostas
36
+ * para uma pergunta.
37
+ *
38
+ * Diferencas em relacao aos originais:
39
+ *
40
+ * - **Reordenacao por teclado.** Arrastar e, por natureza, inacessivel: quem
41
+ * navega por teclado nao tem gesto equivalente. Aqui `Alt+Setas` move o item
42
+ * focado, e cada movimento e anunciado por regiao viva. Sem isso, a
43
+ * funcionalidade simplesmente nao existe para parte dos usuarios.
44
+ * - **Controlado.** O original copiava `items` para estado interno e disparava
45
+ * `onReorder` num efeito com `[list]` na dependencia — o que notificava ja na
46
+ * montagem, sem ninguem ter reordenado nada, e ignorava mudancas vindas de
47
+ * fora depois disso.
48
+ * - **Generico.** O `drag-order-list` fixava a forma do item em `{ title,
49
+ * subtitle, date, link }`. Numa biblioteca isso obriga o consumidor a
50
+ * traduzir o dominio dele para um formato alheio.
51
+ */
52
+ export declare function ArchbaseReorderList<T>({ items, getItemId, onReorder, renderItem, getItemLabel, getItemDescription, onRemove, disabled, gap, className, labels, }: ArchbaseReorderListProps<T>): import("react/jsx-runtime").JSX.Element;
@@ -8,3 +8,5 @@ export { ArchbaseListContext };
8
8
  export * from './treeview';
9
9
  export { ArchbaseTreeList } from './ArchbaseTreeList';
10
10
  export type { ArchbaseTreeListProps, ArchbaseTreeListColumn } from './ArchbaseTreeList';
11
+ export { ArchbaseReorderList } from './ArchbaseReorderList';
12
+ export type { ArchbaseReorderListProps } from './ArchbaseReorderList';
@@ -0,0 +1,49 @@
1
+ import { ReactNode } from 'react';
2
+ export interface ArchbaseMagnifyDockItem {
3
+ id: string;
4
+ /** Icone do item. */
5
+ icon: ReactNode;
6
+ /** Nome acessivel e texto da dica. Obrigatorio: item so com icone e mudo. */
7
+ label: string;
8
+ onClick?: () => void;
9
+ /** Contador exibido no canto. Zero ou ausente nao mostra nada. */
10
+ badge?: number;
11
+ disabled?: boolean;
12
+ }
13
+ export interface ArchbaseMagnifyDockProps {
14
+ items: ArchbaseMagnifyDockItem[];
15
+ /** Tamanho do item em repouso, em pixels. */
16
+ baseSize?: number;
17
+ /** Tamanho maximo sob o ponteiro. */
18
+ magnification?: number;
19
+ /** Alcance da magnificacao, em pixels. */
20
+ distance?: number;
21
+ spring?: {
22
+ mass: number;
23
+ stiffness: number;
24
+ damping: number;
25
+ };
26
+ className?: string;
27
+ 'aria-label'?: string;
28
+ }
29
+ /**
30
+ * Barra de icones com magnificacao sob o ponteiro, no estilo do dock do macOS.
31
+ *
32
+ * Adaptado de Lightswind UI (MIT, Muhilan / codewithMUHILAN).
33
+ *
34
+ * Nao confundir com `ArchbaseDockLayout`, que e layout de paineis acoplaveis —
35
+ * conceitos distintos que a palavra "dock" une por acidente.
36
+ *
37
+ * Correcoes em relacao ao original:
38
+ *
39
+ * - Os itens eram `div` com `role="button"` e `tabIndex={0}`, mas **sem
40
+ * tratador de teclado**: recebiam foco e nao faziam nada com Enter ou Espaco.
41
+ * Aqui sao `<button>` de verdade, e o comportamento vem do navegador.
42
+ * - Item so com icone nao tinha nome acessivel — o leitor anunciava "botao" e
43
+ * nada mais. `label` agora e obrigatorio e vira o nome.
44
+ * - O original marcava `aria-haspopup="true"` em todos, o que promete um menu
45
+ * que nao existe.
46
+ * - A magnificacao e desligada sob `prefers-reduced-motion`; a barra continua
47
+ * inteiramente utilizavel, apenas sem o efeito.
48
+ */
49
+ export declare function ArchbaseMagnifyDock({ items, baseSize, magnification, distance, spring, className, 'aria-label': ariaLabel, }: ArchbaseMagnifyDockProps): import("react/jsx-runtime").JSX.Element;
@@ -13,3 +13,5 @@ export { ArchbaseSpotlight, useArchbaseSpotlight } from './ArchbaseSpotlight';
13
13
  export type { ArchbaseSpotlightProps, SpotlightAction, UseArchbaseSpotlightOptions, } from './ArchbaseSpotlight';
14
14
  export { ArchbaseKeyboardShortcutsProvider, ArchbaseKeyboardShortcutsModal, ArchbaseShortcutHint, useArchbaseKeyboardShortcuts, useArchbaseRegisterShortcut, useArchbaseShortcutScope, } from './ArchbaseKeyboardShortcuts';
15
15
  export type { KeyboardShortcut, ArchbaseKeyboardShortcutsContextValue, ArchbaseKeyboardShortcutsProviderProps, ArchbaseKeyboardShortcutsModalProps, UseArchbaseRegisterShortcutOptions, UseArchbaseShortcutScopeReturn, ArchbaseShortcutHintProps, } from './ArchbaseKeyboardShortcuts';
16
+ export { ArchbaseMagnifyDock } from './ArchbaseMagnifyDock';
17
+ export type { ArchbaseMagnifyDockProps, ArchbaseMagnifyDockItem } from './ArchbaseMagnifyDock';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@archbase/components",
3
- "version": "4.3.2",
3
+ "version": "4.6.0",
4
4
  "description": "UI Components for Archbase React v3 - Form editors, data visualization, and business components",
5
5
  "author": "Edson Martins <edsonmartins2005@gmail.com>",
6
6
  "license": "MIT",
@@ -118,9 +118,9 @@
118
118
  "vis-timeline": "^7.7.3",
119
119
  "xlsx": "^0.18.5",
120
120
  "yet-another-react-lightbox": "^3.21.0",
121
- "@archbase/core": "4.3.2",
122
- "@archbase/data": "4.3.2",
123
- "@archbase/layout": "4.3.2"
121
+ "@archbase/core": "4.6.0",
122
+ "@archbase/data": "4.6.0",
123
+ "@archbase/layout": "4.6.0"
124
124
  },
125
125
  "devDependencies": {
126
126
  "@types/d3": "^7.4.3",
@@ -0,0 +1,250 @@
1
+ import { useCallback, useEffect, useRef, useState, type KeyboardEvent } from 'react';
2
+ import { Box, Loader, Text, useMantineTheme } from '@mantine/core';
3
+ import { IconArrowRight, IconCheck } from '@tabler/icons-react';
4
+ import { motion, useMotionValue, useTransform, animate } from 'framer-motion';
5
+
6
+ export type ArchbaseSlideToConfirmStatus = 'idle' | 'loading' | 'success' | 'error';
7
+
8
+ export interface ArchbaseSlideToConfirmProps {
9
+ /** Texto exibido antes da confirmacao. */
10
+ label?: string;
11
+ /** Texto exibido apos confirmar. */
12
+ successLabel?: string;
13
+ /** Executada ao completar o gesto. Rejeitar volta o controle ao inicio. */
14
+ onConfirm: () => Promise<void> | void;
15
+ /** Chamada quando `onConfirm` rejeita. */
16
+ onError?: (erro: unknown) => void;
17
+ width?: number;
18
+ height?: number;
19
+ disabled?: boolean;
20
+ /** Volta ao estado inicial apos confirmar, em milissegundos. Zero mantem. */
21
+ resetAfter?: number;
22
+ /** Fracao do curso necessaria para confirmar, entre 0 e 1. */
23
+ threshold?: number;
24
+ className?: string;
25
+ 'aria-label'?: string;
26
+ }
27
+
28
+ /**
29
+ * Confirmacao por deslize, para acoes destrutivas ou irreversiveis.
30
+ *
31
+ * Adaptado de Lightswind UI (MIT, Muhilan / codewithMUHILAN).
32
+ *
33
+ * A diferenca principal nao e visual. O original era um `div` arrastavel: quem
34
+ * navega por teclado ou usa leitor de tela **nao conseguia confirmar** — num
35
+ * controle cuja unica funcao e confirmar. Aqui ele e um `slider` de verdade:
36
+ *
37
+ * - `role="slider"` com `aria-valuenow`, entao o leitor anuncia o progresso;
38
+ * - setas movem o polegar, `Home`/`End` vao aos extremos, `End` confirma;
39
+ * - `Enter` e `Espaco` confirmam direto, para quem so quer executar a acao.
40
+ *
41
+ * O gesto continua sendo a defesa contra o clique acidental — o teclado apenas
42
+ * deixa de ser excluido dela.
43
+ */
44
+ export function ArchbaseSlideToConfirm({
45
+ label = 'Deslize para confirmar',
46
+ successLabel = 'Confirmado',
47
+ onConfirm,
48
+ onError,
49
+ width = 320,
50
+ height = 56,
51
+ disabled = false,
52
+ resetAfter = 0,
53
+ threshold = 0.9,
54
+ className,
55
+ 'aria-label': ariaLabel,
56
+ }: ArchbaseSlideToConfirmProps) {
57
+ const theme = useMantineTheme();
58
+ const [status, setStatus] = useState<ArchbaseSlideToConfirmStatus>('idle');
59
+
60
+ const curso = width - height;
61
+ const tamanhoPolegar = height - 8;
62
+
63
+ const x = useMotionValue(0);
64
+ const opacidadeTexto = useTransform(x, [0, curso * 0.5], [1, 0]);
65
+ const larguraPreenchida = useTransform(x, [0, curso], [height, width]);
66
+ const montado = useRef(true);
67
+
68
+ useEffect(() => {
69
+ montado.current = true;
70
+ return () => {
71
+ montado.current = false;
72
+ };
73
+ }, []);
74
+
75
+ const voltarAoInicio = useCallback(() => {
76
+ animate(x, 0, { type: 'spring', stiffness: 400, damping: 30 });
77
+ }, [x]);
78
+
79
+ const confirmar = useCallback(async () => {
80
+ if (status !== 'idle' || disabled) return;
81
+
82
+ animate(x, curso, { type: 'spring', stiffness: 400, damping: 30 });
83
+ setStatus('loading');
84
+
85
+ try {
86
+ await onConfirm();
87
+ // A acao pode terminar depois que o componente saiu da tela; atualizar
88
+ // estado ai dispara aviso do React e nao serve a ninguem.
89
+ if (!montado.current) return;
90
+ setStatus('success');
91
+
92
+ if (resetAfter > 0) {
93
+ window.setTimeout(() => {
94
+ if (!montado.current) return;
95
+ setStatus('idle');
96
+ voltarAoInicio();
97
+ }, resetAfter);
98
+ }
99
+ } catch (erro) {
100
+ if (!montado.current) return;
101
+ setStatus('error');
102
+ voltarAoInicio();
103
+ // O original engolia a falha e voltava ao inicio sem sinal. Quem confirmou
104
+ // ficava sem saber se a acao aconteceu.
105
+ onError?.(erro);
106
+ window.setTimeout(() => {
107
+ if (montado.current) setStatus('idle');
108
+ }, 1200);
109
+ }
110
+ }, [status, disabled, curso, x, onConfirm, onError, resetAfter, voltarAoInicio]);
111
+
112
+ const aoSoltar = useCallback(() => {
113
+ if (status !== 'idle') return;
114
+ if (x.get() >= curso * threshold) void confirmar();
115
+ else voltarAoInicio();
116
+ }, [status, x, curso, threshold, confirmar, voltarAoInicio]);
117
+
118
+ const aoTeclar = useCallback(
119
+ (evento: KeyboardEvent<HTMLDivElement>) => {
120
+ if (status !== 'idle' || disabled) return;
121
+ const passo = curso / 10;
122
+
123
+ switch (evento.key) {
124
+ case 'ArrowRight':
125
+ case 'ArrowUp':
126
+ evento.preventDefault();
127
+ animate(x, Math.min(curso, x.get() + passo), { duration: 0.12 });
128
+ break;
129
+ case 'ArrowLeft':
130
+ case 'ArrowDown':
131
+ evento.preventDefault();
132
+ animate(x, Math.max(0, x.get() - passo), { duration: 0.12 });
133
+ break;
134
+ case 'Home':
135
+ evento.preventDefault();
136
+ voltarAoInicio();
137
+ break;
138
+ case 'End':
139
+ case 'Enter':
140
+ case ' ':
141
+ evento.preventDefault();
142
+ void confirmar();
143
+ break;
144
+ default:
145
+ break;
146
+ }
147
+ },
148
+ [status, disabled, curso, x, confirmar, voltarAoInicio],
149
+ );
150
+
151
+ const corSucesso = theme.colors.green?.[6] ?? '#22c55e';
152
+ const corPrimaria = theme.colors[theme.primaryColor]?.[6] ?? '#228be6';
153
+ const concluido = status === 'success';
154
+
155
+ return (
156
+ <Box
157
+ className={className}
158
+ role="slider"
159
+ tabIndex={disabled ? -1 : 0}
160
+ aria-label={ariaLabel ?? label}
161
+ aria-valuemin={0}
162
+ aria-valuemax={100}
163
+ aria-valuenow={concluido ? 100 : 0}
164
+ aria-valuetext={concluido ? successLabel : label}
165
+ aria-disabled={disabled}
166
+ aria-busy={status === 'loading'}
167
+ onKeyDown={aoTeclar}
168
+ style={{
169
+ position: 'relative',
170
+ width,
171
+ height,
172
+ display: 'flex',
173
+ alignItems: 'center',
174
+ justifyContent: 'center',
175
+ overflow: 'hidden',
176
+ userSelect: 'none',
177
+ borderRadius: height / 2,
178
+ border: `1px solid ${concluido ? corSucesso : 'var(--mantine-color-default-border)'}`,
179
+ background: 'var(--mantine-color-default)',
180
+ opacity: disabled ? 0.6 : 1,
181
+ cursor: disabled ? 'not-allowed' : undefined,
182
+ }}
183
+ >
184
+ <motion.div
185
+ aria-hidden
186
+ style={{
187
+ position: 'absolute',
188
+ left: 0,
189
+ top: 0,
190
+ height: '100%',
191
+ borderRadius: height / 2,
192
+ width: concluido ? width : larguraPreenchida,
193
+ backgroundColor: concluido ? corSucesso : corPrimaria,
194
+ opacity: concluido ? 0.16 : 0.08,
195
+ }}
196
+ />
197
+
198
+ <motion.span
199
+ style={{
200
+ position: 'absolute',
201
+ opacity: status === 'idle' ? opacidadeTexto : 0,
202
+ pointerEvents: 'none',
203
+ }}
204
+ >
205
+ <Text size="sm" fw={500} c="dimmed">
206
+ {label}
207
+ </Text>
208
+ </motion.span>
209
+
210
+ {concluido && (
211
+ <Text size="sm" fw={500} style={{ position: 'absolute' }} c={corSucesso}>
212
+ {successLabel}
213
+ </Text>
214
+ )}
215
+
216
+ <motion.div
217
+ drag={status === 'idle' && !disabled ? 'x' : false}
218
+ dragConstraints={{ left: 0, right: curso }}
219
+ dragElastic={0.05}
220
+ dragMomentum={false}
221
+ onDragEnd={aoSoltar}
222
+ whileTap={{ scale: status === 'idle' && !disabled ? 0.95 : 1 }}
223
+ style={{
224
+ position: 'absolute',
225
+ left: 4,
226
+ zIndex: 1,
227
+ x,
228
+ width: tamanhoPolegar,
229
+ height: tamanhoPolegar,
230
+ borderRadius: '50%',
231
+ display: 'flex',
232
+ alignItems: 'center',
233
+ justifyContent: 'center',
234
+ background: concluido ? corSucesso : 'var(--mantine-color-body)',
235
+ color: concluido ? '#fff' : undefined,
236
+ boxShadow: 'var(--mantine-shadow-sm)',
237
+ cursor: disabled ? 'not-allowed' : status === 'idle' ? 'grab' : 'default',
238
+ }}
239
+ >
240
+ {status === 'loading' ? (
241
+ <Loader size="xs" />
242
+ ) : concluido ? (
243
+ <IconCheck size={20} />
244
+ ) : (
245
+ <IconArrowRight size={20} opacity={0.7} />
246
+ )}
247
+ </motion.div>
248
+ </Box>
249
+ );
250
+ }
@@ -8,3 +8,9 @@ export type {
8
8
 
9
9
  export { ArchbaseSpeedDial } from './ArchbaseSpeedDial';
10
10
  export type { ArchbaseSpeedDialProps, ArchbaseSpeedDialAction } from './ArchbaseSpeedDial';
11
+
12
+ export { ArchbaseSlideToConfirm } from './ArchbaseSlideToConfirm';
13
+ export type {
14
+ ArchbaseSlideToConfirmProps,
15
+ ArchbaseSlideToConfirmStatus,
16
+ } from './ArchbaseSlideToConfirm';
@@ -0,0 +1,172 @@
1
+ import { type CSSProperties, type ReactNode } from 'react';
2
+ import { useMantineColorScheme } from '@mantine/core';
3
+
4
+ export type ArchbaseDeviceVariant = 'phone' | 'tablet' | 'desktop';
5
+
6
+ export interface ArchbaseDeviceFrameProps {
7
+ /** Conteudo exibido na tela. Aceita imagem, video ou aplicacao viva. */
8
+ children?: ReactNode;
9
+ variant?: ArchbaseDeviceVariant;
10
+ /** Largura da moldura em pixels. A altura segue a proporcao do aparelho. */
11
+ width?: number;
12
+ /** Cor do corpo. Ausente, acompanha o esquema de cor. */
13
+ frameColor?: string;
14
+ /** Recorte superior (a "ilha"), so em telefone. */
15
+ showNotch?: boolean;
16
+ /** Botoes laterais desenhados no corpo. */
17
+ showButtons?: boolean;
18
+ shadow?: boolean;
19
+ /** Descricao para leitores de tela. A moldura em si e decorativa. */
20
+ 'aria-label'?: string;
21
+ className?: string;
22
+ style?: CSSProperties;
23
+ }
24
+
25
+ interface Perfil {
26
+ /** Altura dividida pela largura. */
27
+ proporcao: number;
28
+ /** Espessura do corpo em torno da tela, em pixels. */
29
+ moldura: number;
30
+ /** Arredondamento externo. */
31
+ raio: number;
32
+ notch: boolean;
33
+ }
34
+
35
+ const PERFIS: Record<ArchbaseDeviceVariant, Perfil> = {
36
+ phone: { proporcao: 2.16, moldura: 12, raio: 44, notch: true },
37
+ tablet: { proporcao: 1.4, moldura: 16, raio: 28, notch: false },
38
+ desktop: { proporcao: 0.62, moldura: 14, raio: 12, notch: false },
39
+ };
40
+
41
+ /**
42
+ * Moldura de aparelho para apresentar uma tela.
43
+ *
44
+ * Inspirado no `iphone16-pro` de Lightswind UI (MIT, Muhilan /
45
+ * codewithMUHILAN). Tres diferencas deliberadas:
46
+ *
47
+ * - **Nome generico.** Componente de biblioteca batizado de um modelo especifico
48
+ * envelhece junto com o modelo; `variant` cobre telefone, tablet e monitor.
49
+ * - **HTML e CSS, nao SVG.** O original recortava a tela com `clipPath` e so
50
+ * aceitava `src` de imagem ou video. Aqui a tela e um elemento comum, entao
51
+ * cabe qualquer conteudo — inclusive a aplicacao rodando de verdade, que e o
52
+ * uso mais interessante numa demonstracao.
53
+ * - **Cores por prop, nao por classe montada em runtime.** O original fazia
54
+ * `className={`fill-${frameColor}`}`; o compilador do Tailwind nao enxerga
55
+ * classe formada em execucao, entao aquelas regras nunca chegavam ao CSS e a
56
+ * prop de cor nao surtia efeito em build de producao.
57
+ *
58
+ * O `clipPath` do original tambem usava `id="screen"` fixo — duas molduras na
59
+ * mesma pagina colidiam. Sem SVG, o problema deixa de existir.
60
+ */
61
+ export function ArchbaseDeviceFrame({
62
+ children,
63
+ variant = 'phone',
64
+ width = 320,
65
+ frameColor,
66
+ showNotch,
67
+ showButtons = true,
68
+ shadow = true,
69
+ 'aria-label': ariaLabel,
70
+ className,
71
+ style,
72
+ }: ArchbaseDeviceFrameProps) {
73
+ const { colorScheme } = useMantineColorScheme();
74
+ const perfil = PERFIS[variant];
75
+
76
+ const altura = Math.round(width * perfil.proporcao);
77
+ const corDoCorpo =
78
+ frameColor ?? (colorScheme === 'dark' ? 'var(--mantine-color-dark-4)' : '#1f2937');
79
+ const exibirNotch = showNotch ?? perfil.notch;
80
+
81
+ return (
82
+ <div
83
+ className={className}
84
+ // A moldura e decoracao; quem carrega significado e o conteudo. Com
85
+ // rotulo, vira uma figura descritivel.
86
+ role={ariaLabel ? 'figure' : undefined}
87
+ aria-label={ariaLabel}
88
+ style={{
89
+ position: 'relative',
90
+ width,
91
+ height: altura,
92
+ borderRadius: perfil.raio,
93
+ background: corDoCorpo,
94
+ padding: perfil.moldura,
95
+ boxShadow: shadow ? 'var(--mantine-shadow-xl)' : undefined,
96
+ boxSizing: 'border-box',
97
+ ...style,
98
+ }}
99
+ >
100
+ <div
101
+ style={{
102
+ position: 'relative',
103
+ width: '100%',
104
+ height: '100%',
105
+ borderRadius: Math.max(4, perfil.raio - perfil.moldura),
106
+ overflow: 'hidden',
107
+ background: 'var(--mantine-color-body)',
108
+ }}
109
+ >
110
+ {children}
111
+
112
+ {exibirNotch && (
113
+ <div
114
+ aria-hidden
115
+ style={{
116
+ position: 'absolute',
117
+ top: 8,
118
+ left: '50%',
119
+ transform: 'translateX(-50%)',
120
+ width: Math.round(width * 0.3),
121
+ height: Math.round(width * 0.085),
122
+ borderRadius: 999,
123
+ background: corDoCorpo,
124
+ zIndex: 2,
125
+ }}
126
+ />
127
+ )}
128
+ </div>
129
+
130
+ {showButtons && variant !== 'desktop' && (
131
+ <>
132
+ <div aria-hidden style={botaoLateral(corDoCorpo, altura * 0.14, altura * 0.22, 'left')} />
133
+ <div aria-hidden style={botaoLateral(corDoCorpo, altura * 0.09, altura * 0.38, 'left')} />
134
+ <div aria-hidden style={botaoLateral(corDoCorpo, altura * 0.13, altura * 0.3, 'right')} />
135
+ </>
136
+ )}
137
+
138
+ {variant === 'desktop' && (
139
+ <div
140
+ aria-hidden
141
+ style={{
142
+ position: 'absolute',
143
+ bottom: -14,
144
+ left: '50%',
145
+ transform: 'translateX(-50%)',
146
+ width: width * 0.28,
147
+ height: 14,
148
+ borderRadius: '0 0 6px 6px',
149
+ background: corDoCorpo,
150
+ }}
151
+ />
152
+ )}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ function botaoLateral(
158
+ cor: string,
159
+ comprimento: number,
160
+ deslocamento: number,
161
+ lado: 'left' | 'right',
162
+ ): CSSProperties {
163
+ return {
164
+ position: 'absolute',
165
+ [lado]: -3,
166
+ top: deslocamento,
167
+ width: 3,
168
+ height: comprimento,
169
+ borderRadius: 2,
170
+ background: cor,
171
+ };
172
+ }