@archbase/components 4.4.0 → 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,198 @@
1
+ import { useCallback, useRef, type ReactNode } from 'react';
2
+ import { Badge, Box, Tooltip, useMantineColorScheme } from '@mantine/core';
3
+ import { useReducedMotion } from '@mantine/hooks';
4
+ import { motion, useMotionValue, useSpring, useTransform, type MotionValue } from 'framer-motion';
5
+
6
+ export interface ArchbaseMagnifyDockItem {
7
+ id: string;
8
+ /** Icone do item. */
9
+ icon: ReactNode;
10
+ /** Nome acessivel e texto da dica. Obrigatorio: item so com icone e mudo. */
11
+ label: string;
12
+ onClick?: () => void;
13
+ /** Contador exibido no canto. Zero ou ausente nao mostra nada. */
14
+ badge?: number;
15
+ disabled?: boolean;
16
+ }
17
+
18
+ export interface ArchbaseMagnifyDockProps {
19
+ items: ArchbaseMagnifyDockItem[];
20
+ /** Tamanho do item em repouso, em pixels. */
21
+ baseSize?: number;
22
+ /** Tamanho maximo sob o ponteiro. */
23
+ magnification?: number;
24
+ /** Alcance da magnificacao, em pixels. */
25
+ distance?: number;
26
+ spring?: { mass: number; stiffness: number; damping: number };
27
+ className?: string;
28
+ 'aria-label'?: string;
29
+ }
30
+
31
+ const MOLA_PADRAO = { mass: 0.1, stiffness: 150, damping: 12 };
32
+
33
+ /**
34
+ * Barra de icones com magnificacao sob o ponteiro, no estilo do dock do macOS.
35
+ *
36
+ * Adaptado de Lightswind UI (MIT, Muhilan / codewithMUHILAN).
37
+ *
38
+ * Nao confundir com `ArchbaseDockLayout`, que e layout de paineis acoplaveis —
39
+ * conceitos distintos que a palavra "dock" une por acidente.
40
+ *
41
+ * Correcoes em relacao ao original:
42
+ *
43
+ * - Os itens eram `div` com `role="button"` e `tabIndex={0}`, mas **sem
44
+ * tratador de teclado**: recebiam foco e nao faziam nada com Enter ou Espaco.
45
+ * Aqui sao `<button>` de verdade, e o comportamento vem do navegador.
46
+ * - Item so com icone nao tinha nome acessivel — o leitor anunciava "botao" e
47
+ * nada mais. `label` agora e obrigatorio e vira o nome.
48
+ * - O original marcava `aria-haspopup="true"` em todos, o que promete um menu
49
+ * que nao existe.
50
+ * - A magnificacao e desligada sob `prefers-reduced-motion`; a barra continua
51
+ * inteiramente utilizavel, apenas sem o efeito.
52
+ */
53
+ export function ArchbaseMagnifyDock({
54
+ items,
55
+ baseSize = 44,
56
+ magnification = 68,
57
+ distance = 140,
58
+ spring = MOLA_PADRAO,
59
+ className,
60
+ 'aria-label': ariaLabel = 'Atalhos',
61
+ }: ArchbaseMagnifyDockProps) {
62
+ const movimentoReduzido = useReducedMotion();
63
+ // Fora do alcance: em repouso nenhum item cresce.
64
+ const ponteiroX = useMotionValue(Number.POSITIVE_INFINITY);
65
+
66
+ const aoMover = useCallback(
67
+ (evento: React.PointerEvent<HTMLDivElement>) => {
68
+ if (movimentoReduzido) return;
69
+ ponteiroX.set(evento.clientX);
70
+ },
71
+ [ponteiroX, movimentoReduzido],
72
+ );
73
+
74
+ const aoSair = useCallback(() => {
75
+ ponteiroX.set(Number.POSITIVE_INFINITY);
76
+ }, [ponteiroX]);
77
+
78
+ return (
79
+ <Box
80
+ component="div"
81
+ role="toolbar"
82
+ aria-label={ariaLabel}
83
+ aria-orientation="horizontal"
84
+ className={className}
85
+ onPointerMove={aoMover}
86
+ onPointerLeave={aoSair}
87
+ style={{
88
+ display: 'inline-flex',
89
+ alignItems: 'flex-end',
90
+ gap: 8,
91
+ padding: 8,
92
+ borderRadius: 16,
93
+ background: 'var(--mantine-color-default)',
94
+ border: '1px solid var(--mantine-color-default-border)',
95
+ boxShadow: 'var(--mantine-shadow-md)',
96
+ }}
97
+ >
98
+ {items.map((item) => (
99
+ <ItemDoDock
100
+ key={item.id}
101
+ item={item}
102
+ ponteiroX={ponteiroX}
103
+ baseSize={baseSize}
104
+ magnification={movimentoReduzido ? baseSize : magnification}
105
+ distance={distance}
106
+ spring={spring}
107
+ />
108
+ ))}
109
+ </Box>
110
+ );
111
+ }
112
+
113
+ interface ItemProps {
114
+ item: ArchbaseMagnifyDockItem;
115
+ ponteiroX: MotionValue<number>;
116
+ baseSize: number;
117
+ magnification: number;
118
+ distance: number;
119
+ spring: { mass: number; stiffness: number; damping: number };
120
+ }
121
+
122
+ function ItemDoDock({
123
+ item,
124
+ ponteiroX,
125
+ baseSize,
126
+ magnification,
127
+ distance,
128
+ spring,
129
+ }: ItemProps) {
130
+ const referencia = useRef<HTMLButtonElement>(null);
131
+ const { colorScheme } = useMantineColorScheme();
132
+
133
+ const distanciaDoCentro = useTransform(ponteiroX, (valor) => {
134
+ if (typeof valor !== 'number' || Number.isNaN(valor)) return 0;
135
+ const caixa = referencia.current?.getBoundingClientRect();
136
+ if (!caixa) return 0;
137
+ return valor - caixa.x - caixa.width / 2;
138
+ });
139
+
140
+ const tamanhoAlvo = useTransform(
141
+ distanciaDoCentro,
142
+ [-distance, 0, distance],
143
+ [baseSize, magnification, baseSize],
144
+ );
145
+ const tamanho = useSpring(tamanhoAlvo, spring);
146
+
147
+ return (
148
+ <Tooltip label={item.label} position="top" withArrow openDelay={120} events={{ hover: true, focus: true, touch: false }}>
149
+ {/* `motion.button` direto, e nao `UnstyledButton` polimorfico: o tipo de
150
+ `style` do Mantine nao aceita `MotionValue`, e o reset de botao sao
151
+ tres linhas. */}
152
+ <motion.button
153
+ ref={referencia}
154
+ type="button"
155
+ onClick={item.onClick}
156
+ disabled={item.disabled}
157
+ // O nome acessivel vem do rotulo: item so com icone seria anunciado
158
+ // apenas como "botao".
159
+ aria-label={item.label}
160
+ style={{
161
+ width: tamanho,
162
+ height: tamanho,
163
+ position: 'relative',
164
+ display: 'inline-flex',
165
+ alignItems: 'center',
166
+ justifyContent: 'center',
167
+ borderRadius: '50%',
168
+ background:
169
+ colorScheme === 'dark'
170
+ ? 'var(--mantine-color-dark-5)'
171
+ : 'var(--mantine-color-gray-0)',
172
+ border: '1px solid var(--mantine-color-default-border)',
173
+ cursor: item.disabled ? 'not-allowed' : 'pointer',
174
+ opacity: item.disabled ? 0.5 : 1,
175
+ padding: 0,
176
+ font: 'inherit',
177
+ color: 'inherit',
178
+ }}
179
+ >
180
+ {item.icon}
181
+
182
+ {item.badge !== undefined && item.badge > 0 && (
183
+ <Badge
184
+ size="xs"
185
+ circle
186
+ color="red"
187
+ style={{ position: 'absolute', top: -2, right: -2 }}
188
+ // O contador ja e anunciado no rotulo do botao pelo consumidor, se
189
+ // fizer sentido; aqui ele e decorativo para nao duplicar a fala.
190
+ aria-hidden
191
+ >
192
+ {item.badge > 99 ? '99+' : item.badge}
193
+ </Badge>
194
+ )}
195
+ </motion.button>
196
+ </Tooltip>
197
+ );
198
+ }
@@ -48,4 +48,6 @@ export type {
48
48
  UseArchbaseRegisterShortcutOptions,
49
49
  UseArchbaseShortcutScopeReturn,
50
50
  ArchbaseShortcutHintProps,
51
- } from './ArchbaseKeyboardShortcuts';
51
+ } from './ArchbaseKeyboardShortcuts';
52
+ export { ArchbaseMagnifyDock } from './ArchbaseMagnifyDock';
53
+ export type { ArchbaseMagnifyDockProps, ArchbaseMagnifyDockItem } from './ArchbaseMagnifyDock';
Binary file