@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,204 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useRef,
7
+ type CSSProperties,
8
+ type KeyboardEvent,
9
+ type ReactNode,
10
+ type RefObject,
11
+ } from 'react';
12
+ import { Box, Paper } from '@mantine/core';
13
+ import { useReducedMotion } from '@mantine/hooks';
14
+
15
+ export interface ArchbaseGlowingCardsProps {
16
+ children: ReactNode;
17
+ /** Raio do brilho que acompanha o ponteiro, em pixels. */
18
+ glowRadius?: number;
19
+ /** Opacidade do brilho, entre 0 e 1. */
20
+ glowOpacity?: number;
21
+ gap?: number | string;
22
+ /** Colunas fixas. Ausente, ajusta ao espaco disponivel. */
23
+ columns?: number;
24
+ /** Largura minima do cartao quando as colunas sao automaticas. */
25
+ minCardWidth?: number;
26
+ className?: string;
27
+ style?: CSSProperties;
28
+ }
29
+
30
+ export interface ArchbaseGlowingCardProps {
31
+ children: ReactNode;
32
+ /** Cor do brilho deste cartao. */
33
+ glowColor?: string;
34
+ /** Eleva o cartao ao passar o ponteiro. */
35
+ hoverEffect?: boolean;
36
+ onClick?: () => void;
37
+ className?: string;
38
+ style?: CSSProperties;
39
+ }
40
+
41
+ const ContextoDoGrupo = createContext<RefObject<HTMLDivElement | null> | null>(null);
42
+
43
+ /**
44
+ * Grade de cartoes com um unico brilho que atravessa todos, acompanhando o
45
+ * ponteiro.
46
+ *
47
+ * Adaptado de Lightswind UI (MIT, Muhilan / codewithMUHILAN).
48
+ *
49
+ * A posicao do ponteiro e publicada em custom properties no container, e cada
50
+ * cartao publica o proprio deslocamento dentro dele. O gradiente subtrai um do
51
+ * outro, o que faz o foco atravessar a grade como uma luz unica em vez de
52
+ * acender dentro de cada cartao isoladamente.
53
+ *
54
+ * Nada disso passa por estado do React: mover o ponteiro sobre uma grade grande
55
+ * nao dispara render. O original recalculava estado por evento, o que
56
+ * transforma um enfeite em travamento quando os cartoes se multiplicam.
57
+ */
58
+ export function ArchbaseGlowingCards({
59
+ children,
60
+ glowRadius = 280,
61
+ glowOpacity = 0.35,
62
+ gap = 16,
63
+ columns,
64
+ minCardWidth = 240,
65
+ className,
66
+ style,
67
+ }: ArchbaseGlowingCardsProps) {
68
+ const container = useRef<HTMLDivElement>(null);
69
+ const movimentoReduzido = useReducedMotion();
70
+
71
+ const aoMover = useCallback(
72
+ (evento: React.PointerEvent<HTMLDivElement>) => {
73
+ if (movimentoReduzido) return;
74
+ const elemento = container.current;
75
+ if (!elemento) return;
76
+
77
+ const caixa = elemento.getBoundingClientRect();
78
+ elemento.style.setProperty('--archbase-glow-x', `${evento.clientX - caixa.left}px`);
79
+ elemento.style.setProperty('--archbase-glow-y', `${evento.clientY - caixa.top}px`);
80
+ elemento.style.setProperty('--archbase-glow-alpha', String(glowOpacity));
81
+ },
82
+ [glowOpacity, movimentoReduzido],
83
+ );
84
+
85
+ const aoSair = useCallback(() => {
86
+ container.current?.style.setProperty('--archbase-glow-alpha', '0');
87
+ }, []);
88
+
89
+ return (
90
+ <ContextoDoGrupo.Provider value={container}>
91
+ <Box
92
+ ref={container}
93
+ className={className}
94
+ onPointerMove={aoMover}
95
+ onPointerLeave={aoSair}
96
+ style={
97
+ {
98
+ display: 'grid',
99
+ gap,
100
+ gridTemplateColumns: columns
101
+ ? `repeat(${columns}, minmax(0, 1fr))`
102
+ : `repeat(auto-fit, minmax(${minCardWidth}px, 1fr))`,
103
+ '--archbase-glow-x': '50%',
104
+ '--archbase-glow-y': '50%',
105
+ '--archbase-glow-radius': `${glowRadius}px`,
106
+ // Com movimento reduzido o brilho fica visivel e parado no centro:
107
+ // o desenho permanece, o movimento nao.
108
+ '--archbase-glow-alpha': movimentoReduzido ? String(glowOpacity) : '0',
109
+ ...style,
110
+ } as CSSProperties
111
+ }
112
+ >
113
+ {children}
114
+ </Box>
115
+ </ContextoDoGrupo.Provider>
116
+ );
117
+ }
118
+
119
+ /**
120
+ * Cartao do grupo. Fora de `ArchbaseGlowingCards` continua valido — apenas sem
121
+ * o brilho, porque as custom properties nao existem.
122
+ */
123
+ export function ArchbaseGlowingCard({
124
+ children,
125
+ glowColor = 'var(--mantine-primary-color-filled)',
126
+ hoverEffect = true,
127
+ onClick,
128
+ className,
129
+ style,
130
+ }: ArchbaseGlowingCardProps) {
131
+ const cartao = useRef<HTMLDivElement>(null);
132
+ const container = useContext(ContextoDoGrupo);
133
+ const interativo = typeof onClick === 'function';
134
+
135
+ // O deslocamento do cartao dentro do grupo muda com o layout, nao com o
136
+ // ponteiro: medir aqui mantem o movimento livre de JavaScript.
137
+ useEffect(() => {
138
+ const elemento = cartao.current;
139
+ const pai = container?.current;
140
+ if (!elemento || !pai) return;
141
+
142
+ const medir = () => {
143
+ const c = elemento.getBoundingClientRect();
144
+ const p = pai.getBoundingClientRect();
145
+ elemento.style.setProperty('--archbase-card-x', `${c.left - p.left}px`);
146
+ elemento.style.setProperty('--archbase-card-y', `${c.top - p.top}px`);
147
+ };
148
+
149
+ medir();
150
+ if (typeof ResizeObserver === 'undefined') return;
151
+ const observador = new ResizeObserver(medir);
152
+ observador.observe(pai);
153
+ observador.observe(elemento);
154
+ return () => observador.disconnect();
155
+ }, [container]);
156
+
157
+ const aoTeclar = useCallback(
158
+ (evento: KeyboardEvent<HTMLDivElement>) => {
159
+ if (!interativo) return;
160
+ if (evento.key === 'Enter' || evento.key === ' ') {
161
+ evento.preventDefault();
162
+ onClick?.();
163
+ }
164
+ },
165
+ [interativo, onClick],
166
+ );
167
+
168
+ return (
169
+ <Paper
170
+ ref={cartao}
171
+ withBorder
172
+ radius="md"
173
+ p="md"
174
+ className={className}
175
+ onClick={onClick}
176
+ // Cartao clicavel precisa ser alcancavel por teclado; cartao decorativo
177
+ // nao deve entrar na ordem de foco.
178
+ role={interativo ? 'button' : undefined}
179
+ tabIndex={interativo ? 0 : undefined}
180
+ onKeyDown={interativo ? aoTeclar : undefined}
181
+ style={{
182
+ position: 'relative',
183
+ overflow: 'hidden',
184
+ cursor: interativo ? 'pointer' : undefined,
185
+ transition: hoverEffect ? 'transform 160ms ease, box-shadow 160ms ease' : undefined,
186
+ ...style,
187
+ }}
188
+ >
189
+ <span
190
+ aria-hidden
191
+ style={{
192
+ position: 'absolute',
193
+ inset: 0,
194
+ pointerEvents: 'none',
195
+ opacity: 'var(--archbase-glow-alpha, 0)' as unknown as number,
196
+ transition: 'opacity 200ms ease',
197
+ background: `radial-gradient(var(--archbase-glow-radius, 280px) circle at calc(var(--archbase-glow-x, 50%) - var(--archbase-card-x, 0px)) calc(var(--archbase-glow-y, 50%) - var(--archbase-card-y, 0px)), ${glowColor}, transparent 60%)`,
198
+ }}
199
+ />
200
+
201
+ <div style={{ position: 'relative' }}>{children}</div>
202
+ </Paper>
203
+ );
204
+ }
@@ -96,3 +96,12 @@ export type {
96
96
  ArchbasePhotoAlbumLayout,
97
97
  } from './ArchbasePhotoAlbum';
98
98
 
99
+
100
+ export { ArchbaseGlowingCards, ArchbaseGlowingCard } from './ArchbaseGlowingCards';
101
+ export type {
102
+ ArchbaseGlowingCardsProps,
103
+ ArchbaseGlowingCardProps,
104
+ } from './ArchbaseGlowingCards';
105
+
106
+ export { ArchbaseDeviceFrame } from './ArchbaseDeviceFrame';
107
+ export type { ArchbaseDeviceFrameProps, ArchbaseDeviceVariant } from './ArchbaseDeviceFrame';
@@ -0,0 +1,160 @@
1
+ import { useCallback, useEffect, useState, type ReactNode } from 'react';
2
+ import { ActionIcon, Box, Group, type MantineColor } from '@mantine/core';
3
+ import { useReducedMotion } from '@mantine/hooks';
4
+ import { IconX } from '@tabler/icons-react';
5
+ import { AnimatePresence, motion } from 'framer-motion';
6
+
7
+ export interface ArchbaseStickyTopBarProps {
8
+ children: ReactNode;
9
+ /**
10
+ * Visibilidade controlada. Ignorada quando `showOnScroll` esta ligado.
11
+ */
12
+ visible?: boolean;
13
+ /** Aparece depois de rolar; util para barra de acao que so importa adiante. */
14
+ showOnScroll?: boolean;
15
+ /** Rolagem, em pixels, a partir da qual a barra aparece. */
16
+ scrollThreshold?: number;
17
+ /** Botao de fechar. */
18
+ dismissible?: boolean;
19
+ onDismiss?: () => void;
20
+ /**
21
+ * Chave de persistencia da dispensa. Definida, a barra nao volta a aparecer
22
+ * para quem ja a fechou — mensagem de sistema que reaparece a cada
23
+ * navegacao vira ruido e ensina o usuario a ignorar o lugar todo.
24
+ */
25
+ dismissStorageKey?: string;
26
+ color?: MantineColor;
27
+ /** Fixa no topo da janela. Desligado, acompanha o fluxo da pagina. */
28
+ sticky?: boolean;
29
+ /** Altura reservada, em pixels. */
30
+ height?: number;
31
+ zIndex?: number;
32
+ /**
33
+ * Papel semantico. `status` para informacao passageira; `region` para
34
+ * conteudo que o usuario pode querer reencontrar.
35
+ */
36
+ role?: 'status' | 'region' | 'alert';
37
+ 'aria-label'?: string;
38
+ className?: string;
39
+ }
40
+
41
+ function jaDispensada(chave?: string): boolean {
42
+ if (!chave || typeof window === 'undefined') return false;
43
+ try {
44
+ return window.localStorage.getItem(chave) === 'dispensada';
45
+ } catch {
46
+ // Modo privado ou armazenamento bloqueado: a barra volta a aparecer, que e
47
+ // o comportamento menos surpreendente entre os dois ruins.
48
+ return false;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Barra fixa no topo, para aviso de sistema ou acao contextual.
54
+ *
55
+ * Adaptado de Lightswind UI (MIT, Muhilan / codewithMUHILAN), com o que faltava
56
+ * para uso em produto:
57
+ *
58
+ * - **Dispensavel, com memoria opcional.** O original nao fechava. Aviso que
59
+ * nao se fecha e o usuario cobre com a mao.
60
+ * - **Semantica.** Era um `div` mudo; agora recebe papel e rotulo, e um leitor
61
+ * de tela anuncia a mensagem quando ela chega.
62
+ * - **`useSyncExternalStore` nao entra aqui, mas o listener de rolagem e
63
+ * passivo** — sem isso o navegador nao pode adiantar a rolagem enquanto o
64
+ * manipulador roda.
65
+ * - **Movimento reduzido** troca o deslize por aparecer e sumir.
66
+ */
67
+ export function ArchbaseStickyTopBar({
68
+ children,
69
+ visible = true,
70
+ showOnScroll = false,
71
+ scrollThreshold = 200,
72
+ dismissible = false,
73
+ onDismiss,
74
+ dismissStorageKey,
75
+ color = 'blue',
76
+ sticky = true,
77
+ height = 44,
78
+ zIndex = 200,
79
+ role = 'status',
80
+ 'aria-label': ariaLabel,
81
+ className,
82
+ }: ArchbaseStickyTopBarProps) {
83
+ const movimentoReduzido = useReducedMotion();
84
+ const [dispensada, setDispensada] = useState(() => jaDispensada(dismissStorageKey));
85
+ const [passouDoLimite, setPassouDoLimite] = useState(false);
86
+
87
+ useEffect(() => {
88
+ if (!showOnScroll || typeof window === 'undefined') return;
89
+
90
+ const aoRolar = () => setPassouDoLimite(window.scrollY > scrollThreshold);
91
+ aoRolar();
92
+ // `passive` permite ao navegador rolar sem esperar este manipulador.
93
+ window.addEventListener('scroll', aoRolar, { passive: true });
94
+ return () => window.removeEventListener('scroll', aoRolar);
95
+ }, [showOnScroll, scrollThreshold]);
96
+
97
+ const dispensar = useCallback(() => {
98
+ setDispensada(true);
99
+ if (dismissStorageKey && typeof window !== 'undefined') {
100
+ try {
101
+ window.localStorage.setItem(dismissStorageKey, 'dispensada');
102
+ } catch {
103
+ // Sem armazenamento, a dispensa vale so para esta sessao.
104
+ }
105
+ }
106
+ onDismiss?.();
107
+ }, [dismissStorageKey, onDismiss]);
108
+
109
+ const aparecendo = !dispensada && (showOnScroll ? passouDoLimite : visible);
110
+
111
+ const transicao = movimentoReduzido
112
+ ? { duration: 0.01 }
113
+ : { type: 'spring' as const, stiffness: 320, damping: 30 };
114
+
115
+ return (
116
+ <AnimatePresence>
117
+ {aparecendo && (
118
+ <motion.div
119
+ initial={movimentoReduzido ? { opacity: 0 } : { y: -height, opacity: 0 }}
120
+ animate={movimentoReduzido ? { opacity: 1 } : { y: 0, opacity: 1 }}
121
+ exit={movimentoReduzido ? { opacity: 0 } : { y: -height, opacity: 0 }}
122
+ transition={transicao}
123
+ style={{
124
+ position: sticky ? 'sticky' : 'relative',
125
+ top: 0,
126
+ zIndex,
127
+ width: '100%',
128
+ }}
129
+ >
130
+ <Box
131
+ className={className}
132
+ role={role}
133
+ aria-label={ariaLabel}
134
+ aria-live={role === 'alert' ? 'assertive' : 'polite'}
135
+ bg={color}
136
+ style={{ minHeight: height, color: 'var(--mantine-color-white)' }}
137
+ px="md"
138
+ py={6}
139
+ >
140
+ <Group justify="space-between" wrap="nowrap" h="100%">
141
+ <Box style={{ flex: 1, minWidth: 0 }}>{children}</Box>
142
+
143
+ {dismissible && (
144
+ <ActionIcon
145
+ variant="subtle"
146
+ color="gray"
147
+ aria-label="Fechar aviso"
148
+ onClick={dispensar}
149
+ style={{ color: 'inherit' }}
150
+ >
151
+ <IconX size={16} />
152
+ </ActionIcon>
153
+ )}
154
+ </Group>
155
+ </Box>
156
+ </motion.div>
157
+ )}
158
+ </AnimatePresence>
159
+ );
160
+ }
@@ -23,4 +23,6 @@ export type {
23
23
  } from './ArchbaseSkeleton';
24
24
 
25
25
  export { ArchbaseRipple, useArchbaseRipple } from './ArchbaseRipple';
26
- export type { ArchbaseRippleProps, UseArchbaseRippleOptions, UseArchbaseRippleReturn } from './ArchbaseRipple';
26
+ export type { ArchbaseRippleProps, UseArchbaseRippleOptions, UseArchbaseRippleReturn } from './ArchbaseRipple';
27
+ export { ArchbaseStickyTopBar } from './ArchbaseStickyTopBar';
28
+ export type { ArchbaseStickyTopBarProps } from './ArchbaseStickyTopBar';
@@ -0,0 +1,249 @@
1
+ import { useCallback, useRef, useState, type KeyboardEvent, type ReactNode } from 'react';
2
+ import { ActionIcon, Group, Paper, Text, VisuallyHidden } from '@mantine/core';
3
+ import { IconGripVertical, IconX } from '@tabler/icons-react';
4
+ import { Reorder, useDragControls } from 'framer-motion';
5
+
6
+ export interface ArchbaseReorderListProps<T> {
7
+ /** Itens na ordem corrente. Componente controlado. */
8
+ items: T[];
9
+ /** Identidade estavel do item. */
10
+ getItemId: (item: T) => string | number;
11
+ /** Nova ordem. Sem isto o componente nao teria como refletir o arrasto. */
12
+ onReorder: (items: T[]) => void;
13
+ /** Conteudo do item. Ausente, usa `getItemLabel`. */
14
+ renderItem?: (item: T, estado: { arrastando: boolean; indice: number }) => ReactNode;
15
+ /** Rotulo textual, usado no conteudo padrao e nos anuncios de acessibilidade. */
16
+ getItemLabel?: (item: T) => string;
17
+ /** Descricao secundaria no conteudo padrao. */
18
+ getItemDescription?: (item: T) => string | undefined;
19
+ onRemove?: (item: T) => void;
20
+ disabled?: boolean;
21
+ gap?: number;
22
+ className?: string;
23
+ labels?: {
24
+ dragHandle?: string;
25
+ remove?: string;
26
+ /** `{item}`, `{de}` e `{para}` sao substituidos. */
27
+ moved?: string;
28
+ instructions?: string;
29
+ };
30
+ }
31
+
32
+ const ROTULOS_PADRAO = {
33
+ dragHandle: 'Arrastar para reordenar',
34
+ remove: 'Remover',
35
+ moved: '{item} movido da posicao {de} para {para}',
36
+ instructions:
37
+ 'Use as setas para cima e para baixo com a tecla Alt para mover o item selecionado.',
38
+ };
39
+
40
+ function interpolar(texto: string, valores: Record<string, string | number>): string {
41
+ return texto.replace(/\{(\w+)\}/g, (m, chave: string) => String(valores[chave] ?? m));
42
+ }
43
+
44
+ /**
45
+ * Lista reordenavel por arrasto e por teclado.
46
+ *
47
+ * Unifica dois componentes do Lightswind UI (MIT, Muhilan / codewithMUHILAN) —
48
+ * `drag-order-list` e `draggable-reorder-list` — que resolviam o mesmo problema
49
+ * com implementacoes diferentes. Entregar os dois seria oferecer duas respostas
50
+ * para uma pergunta.
51
+ *
52
+ * Diferencas em relacao aos originais:
53
+ *
54
+ * - **Reordenacao por teclado.** Arrastar e, por natureza, inacessivel: quem
55
+ * navega por teclado nao tem gesto equivalente. Aqui `Alt+Setas` move o item
56
+ * focado, e cada movimento e anunciado por regiao viva. Sem isso, a
57
+ * funcionalidade simplesmente nao existe para parte dos usuarios.
58
+ * - **Controlado.** O original copiava `items` para estado interno e disparava
59
+ * `onReorder` num efeito com `[list]` na dependencia — o que notificava ja na
60
+ * montagem, sem ninguem ter reordenado nada, e ignorava mudancas vindas de
61
+ * fora depois disso.
62
+ * - **Generico.** O `drag-order-list` fixava a forma do item em `{ title,
63
+ * subtitle, date, link }`. Numa biblioteca isso obriga o consumidor a
64
+ * traduzir o dominio dele para um formato alheio.
65
+ */
66
+ export function ArchbaseReorderList<T>({
67
+ items,
68
+ getItemId,
69
+ onReorder,
70
+ renderItem,
71
+ getItemLabel,
72
+ getItemDescription,
73
+ onRemove,
74
+ disabled = false,
75
+ gap = 8,
76
+ className,
77
+ labels,
78
+ }: ArchbaseReorderListProps<T>) {
79
+ const textos = { ...ROTULOS_PADRAO, ...labels };
80
+ const [anuncio, setAnuncio] = useState('');
81
+
82
+ const rotuloDe = useCallback(
83
+ (item: T) => getItemLabel?.(item) ?? String(getItemId(item)),
84
+ [getItemLabel, getItemId],
85
+ );
86
+
87
+ const mover = useCallback(
88
+ (de: number, para: number) => {
89
+ if (disabled) return;
90
+ if (para < 0 || para >= items.length || de === para) return;
91
+
92
+ const proximos = [...items];
93
+ const [movido] = proximos.splice(de, 1);
94
+ if (movido === undefined) return;
95
+ proximos.splice(para, 0, movido);
96
+
97
+ onReorder(proximos);
98
+ setAnuncio(
99
+ interpolar(textos.moved, { item: rotuloDe(movido), de: de + 1, para: para + 1 }),
100
+ );
101
+ },
102
+ [items, onReorder, disabled, textos.moved, rotuloDe],
103
+ );
104
+
105
+ return (
106
+ <>
107
+ {/* Regiao viva: o leitor de tela precisa saber que a ordem mudou, senao a
108
+ reordenacao por teclado acontece sem retorno algum. */}
109
+ <VisuallyHidden aria-live="polite" role="status">
110
+ {anuncio}
111
+ </VisuallyHidden>
112
+
113
+ <Reorder.Group
114
+ axis="y"
115
+ values={items}
116
+ onReorder={onReorder}
117
+ className={className}
118
+ style={{ listStyle: 'none', margin: 0, padding: 0, display: 'grid', gap }}
119
+ >
120
+ {items.map((item, indice) => (
121
+ <ArchbaseReorderItem
122
+ key={getItemId(item)}
123
+ item={item}
124
+ indice={indice}
125
+ total={items.length}
126
+ disabled={disabled}
127
+ rotulo={rotuloDe(item)}
128
+ descricao={getItemDescription?.(item)}
129
+ renderItem={renderItem}
130
+ onMover={mover}
131
+ onRemove={onRemove}
132
+ textos={textos}
133
+ />
134
+ ))}
135
+ </Reorder.Group>
136
+ </>
137
+ );
138
+ }
139
+
140
+ interface ItemProps<T> {
141
+ item: T;
142
+ indice: number;
143
+ total: number;
144
+ disabled: boolean;
145
+ rotulo: string;
146
+ descricao?: string;
147
+ renderItem?: (item: T, estado: { arrastando: boolean; indice: number }) => ReactNode;
148
+ onMover: (de: number, para: number) => void;
149
+ onRemove?: (item: T) => void;
150
+ textos: typeof ROTULOS_PADRAO;
151
+ }
152
+
153
+ function ArchbaseReorderItem<T>({
154
+ item,
155
+ indice,
156
+ total,
157
+ disabled,
158
+ rotulo,
159
+ descricao,
160
+ renderItem,
161
+ onMover,
162
+ onRemove,
163
+ textos,
164
+ }: ItemProps<T>) {
165
+ const controles = useDragControls();
166
+ const [arrastando, setArrastando] = useState(false);
167
+ const referencia = useRef<HTMLLIElement>(null);
168
+
169
+ const aoTeclar = useCallback(
170
+ (evento: KeyboardEvent<HTMLLIElement>) => {
171
+ if (disabled || !evento.altKey) return;
172
+
173
+ if (evento.key === 'ArrowUp') {
174
+ evento.preventDefault();
175
+ onMover(indice, indice - 1);
176
+ // O foco acompanha o item, nao a posicao: sem isto, mover duas vezes
177
+ // seguidas exigiria reencontrar o item na lista.
178
+ requestAnimationFrame(() => referencia.current?.focus());
179
+ } else if (evento.key === 'ArrowDown') {
180
+ evento.preventDefault();
181
+ onMover(indice, indice + 1);
182
+ requestAnimationFrame(() => referencia.current?.focus());
183
+ }
184
+ },
185
+ [disabled, indice, onMover],
186
+ );
187
+
188
+ return (
189
+ <Reorder.Item
190
+ ref={referencia}
191
+ value={item}
192
+ dragListener={false}
193
+ dragControls={controles}
194
+ onDragStart={() => setArrastando(true)}
195
+ onDragEnd={() => setArrastando(false)}
196
+ tabIndex={disabled ? -1 : 0}
197
+ onKeyDown={aoTeclar}
198
+ aria-roledescription="Item reordenavel"
199
+ aria-label={`${rotulo}. ${indice + 1} de ${total}. ${textos.instructions}`}
200
+ style={{ listStyle: 'none' }}
201
+ >
202
+ <Paper withBorder p="xs" radius="md" shadow={arrastando ? 'md' : undefined}>
203
+ <Group gap="sm" wrap="nowrap">
204
+ <ActionIcon
205
+ variant="subtle"
206
+ color="gray"
207
+ aria-label={textos.dragHandle}
208
+ disabled={disabled}
209
+ // O arrasto so comeca pela alca: arrastar o item inteiro impede
210
+ // selecionar texto e clicar em controles dentro dele.
211
+ onPointerDown={(evento) => !disabled && controles.start(evento)}
212
+ style={{ cursor: disabled ? 'not-allowed' : 'grab', touchAction: 'none' }}
213
+ >
214
+ <IconGripVertical size={18} />
215
+ </ActionIcon>
216
+
217
+ <div style={{ flex: 1, minWidth: 0 }}>
218
+ {renderItem ? (
219
+ renderItem(item, { arrastando, indice })
220
+ ) : (
221
+ <>
222
+ <Text size="sm" fw={500} truncate>
223
+ {rotulo}
224
+ </Text>
225
+ {descricao && (
226
+ <Text size="xs" c="dimmed" truncate>
227
+ {descricao}
228
+ </Text>
229
+ )}
230
+ </>
231
+ )}
232
+ </div>
233
+
234
+ {onRemove && (
235
+ <ActionIcon
236
+ variant="subtle"
237
+ color="red"
238
+ aria-label={`${textos.remove}: ${rotulo}`}
239
+ disabled={disabled}
240
+ onClick={() => onRemove(item)}
241
+ >
242
+ <IconX size={16} />
243
+ </ActionIcon>
244
+ )}
245
+ </Group>
246
+ </Paper>
247
+ </Reorder.Item>
248
+ );
249
+ }
package/src/list/index.ts CHANGED
@@ -10,3 +10,6 @@ export * from './treeview';
10
10
 
11
11
  export { ArchbaseTreeList } from './ArchbaseTreeList';
12
12
  export type { ArchbaseTreeListProps, ArchbaseTreeListColumn } from './ArchbaseTreeList';
13
+
14
+ export { ArchbaseReorderList } from './ArchbaseReorderList';
15
+ export type { ArchbaseReorderListProps } from './ArchbaseReorderList';