@aurea-uds/native 0.30.0 → 0.32.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,457 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Aurea nativo — quatro peças que acompanham o conteúdo: `Accordion`, `ScrollShadow`,
3
+ // `SkeletonGroup` e `PressableFeedback` (0.32.0, passo 9b).
4
+ //
5
+ // As quatro são as do pacote nativo da referência — anatomia, medidas, estados, comportamento e
6
+ // acessibilidade lidos no pacote publicado dela —, com o que é da Aurea por cima: as cores do tema,
7
+ // o canto 22 do cartão e a cápsula, a Atkinson, os ícones Phosphor, sem degradê, "menos movimento".
8
+ //
9
+ // Medido no pacote dela antes de escrito (a classe vira dp: `px-3` = 12, `py-4` = 16, `gap-4` = 16):
10
+ //
11
+ // accordion gatilho linha, 16 em cima e embaixo, 12 dos lados (20 no `surface`), 16 entre as partes
12
+ // seta 16, gira de 0 a −180° numa mola (amortecimento 140, rigidez 1000, massa 4)
13
+ // resposta 12 dos lados (20 no `surface`), 16 embaixo; entra esmaecendo em 200 ms
14
+ // fio entre os itens, nenhum depois do último; no `surface`, 12 de recuo dos lados
15
+ // surface o cartão: fundo, canto 3xl (aqui o 22 da Aurea) e a sombra de superfície
16
+ // scroll-shadow um degradê de 50 nas pontas — TROCADO por um fio (decisão do Victor, abaixo)
17
+ // skeleton-group um provedor: `isLoading` e a animação para os esqueletos de dentro
18
+ // pressable-feedback escala 0,985 numa peça de 300 de largura (proporcional), 300 ms
19
+ // realce: camada cheia que vai a 10% em 200 ms
20
+ // onda: círculo de 1,25 × a diagonal que nasce no dedo, de 750 a 2000 ms
21
+ //
22
+ // ⚠ O QUE NÃO VEIO, e por quê:
23
+ // • a mola de ALTURA do accordion (os vizinhos deslizando ao abrir). Altura não roda no motor
24
+ // nativo de animação, e toda animação da Aurea roda nele (`driverNativo()`, E13). A resposta
25
+ // entra esmaecendo, como a dela; os vizinhos descem de uma vez, e fechar é na hora.
26
+ // • o degradê do scroll-shadow e o da onda — a Aurea não tem degradê (CLAUDE.md §5).
27
+ // • `asChild` — o encaixe sem caixa própria dela não existe neste pacote.
28
+ import * as React from "react";
29
+ import { Animated, Easing, Pressable, View, } from "react-native";
30
+ import { canto, comOpacidade, criarFolha, estadoAcessivel, REACAO_AO_TOQUE } from "./estilos.js";
31
+ import { GrupoDeEsqueleto, Skeleton, usePulsoDoEsqueleto } from "./feedback.js";
32
+ import { Icon } from "./icon.js";
33
+ import { driverNativo, useReduceMotion } from "./movimento.js";
34
+ import { Text } from "./text.js";
35
+ import { useAureaTokens } from "./theme.js";
36
+ const folha = criarFolha((t) => ({
37
+ // ── Accordion ──────────────────────────────────────────────────────────────────────────────
38
+ accordion: { flexDirection: "column" },
39
+ // O `surface` da referência: o cartão com a sombra de superfície (a `shadowSm` inteira, como o
40
+ // `Alert`). O canto é o 22 da Aurea, no lugar do 24 dela.
41
+ accordionSurface: { ...canto(t.size.radiusCard), backgroundColor: t.color.card, boxShadow: [...t.shadowLayers.shadowSm] },
42
+ // O gatilho tem o canto da cápsula sempre: é o desenho do realce de quando o dedo está em cima
43
+ // (decisão do Victor para a web, 11/10/2026: *"a seleção é quadrada deles quero nossa como capsula"*).
44
+ gatilho: {
45
+ flexDirection: "row", alignItems: "center", justifyContent: "space-between",
46
+ paddingVertical: t.size.space4, paddingHorizontal: t.size.space3, gap: t.size.space4,
47
+ ...canto(t.size.radiusControl),
48
+ },
49
+ gatilhoSurface: { paddingHorizontal: t.size.space5 },
50
+ titulo: { flex: 1, minWidth: 0 },
51
+ seta: { alignItems: "center", justifyContent: "center" },
52
+ resposta: { paddingHorizontal: t.size.space3, paddingBottom: t.size.space4 },
53
+ respostaSurface: { paddingHorizontal: t.size.space5 },
54
+ // O fio dela é o `hairlineWidth` (um pixel do aparelho: 0,33 num telefone de 3×). Aqui é a borda
55
+ // da Aurea, 1 — a mesma linha da web (`--border-width`), e uma que se enxerga.
56
+ fio: { height: t.size.borderWidth, backgroundColor: t.color.border },
57
+ fioSurface: { marginHorizontal: t.size.space3 },
58
+ inativo: { opacity: t.size.opacityDisabled },
59
+ // ── ScrollShadow ───────────────────────────────────────────────────────────────────────────
60
+ // A superfície (o padrão, 12/10/2026): a MESMA pele do `Accordion` no cartão — fundo do cartão, canto
61
+ // 22 e a sombra de superfície —, com o recheio dos lados dele (20) e 12 em cima e embaixo.
62
+ superficieDaLista: { ...canto(t.size.radiusCard), backgroundColor: t.color.card, boxShadow: [...t.shadowLayers.shadowSm],
63
+ paddingHorizontal: t.size.space5, paddingVertical: t.size.space3 },
64
+ // Dentro da superfície a janela da lista já está longe dos cantos: o fio vai de lado a lado dela.
65
+ fioDeCimaNaSuperficie: { left: 0, right: 0 },
66
+ fioDaEsquerdaNaSuperficie: { top: 0, bottom: 0 },
67
+ // Cresce com a caixa: quem dá altura ao `ScrollShadow` (um `flex: 1`) a dá também à lista de dentro.
68
+ miolo: { position: "relative", flexGrow: 1, flexShrink: 1 },
69
+ // No `plain` (sem caixa), o fio NÃO vai de ponta a ponta: recua o raio do cartão (22) dos dois lados e tem as pontas em
70
+ // cápsula (escolha do Victor, 12/10/2026, opção "A"). De ponta a ponta ele desenhava uma quina, que
71
+ // a Aurea não tem; recuado, ele para onde o canto de um cartão em volta começa a curvar.
72
+ fioDeCima: { position: "absolute", top: 0, left: t.size.radiusCard, right: t.size.radiusCard,
73
+ height: t.size.borderWidth, ...canto(t.size.radiusControl), backgroundColor: t.color.border },
74
+ fioDeBaixo: { position: "absolute", bottom: 0, left: t.size.radiusCard, right: t.size.radiusCard,
75
+ height: t.size.borderWidth, ...canto(t.size.radiusControl), backgroundColor: t.color.border },
76
+ fioDaEsquerda: { position: "absolute", left: 0, top: t.size.radiusCard, bottom: t.size.radiusCard,
77
+ width: t.size.borderWidth, ...canto(t.size.radiusControl), backgroundColor: t.color.border },
78
+ fioDaDireita: { position: "absolute", right: 0, top: t.size.radiusCard, bottom: t.size.radiusCard,
79
+ width: t.size.borderWidth, ...canto(t.size.radiusControl), backgroundColor: t.color.border },
80
+ // ── PressableFeedback ──────────────────────────────────────────────────────────────────────
81
+ // `overflow: hidden` é o da raiz dela: o realce e a onda ficam dentro do canto de quem usa.
82
+ raizDoToque: { overflow: "hidden" },
83
+ camada: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0 },
84
+ onda: { position: "absolute", top: 0, left: 0 },
85
+ }));
86
+ /**
87
+ * Leva um valor animado ao `destino` quando o destino MUDA — com a animação de `criar`, ou de um
88
+ * salto quando não há movimento (a pessoa pediu menos, o sistema ainda não respondeu). No primeiro
89
+ * desenho não faz nada: o valor já nasce no lugar. E a resposta do sistema chegando depois não
90
+ * dispara animação nenhuma — só a troca de destino dispara.
91
+ */
92
+ function useLevarA(valor, destino, semMovimento, criar) {
93
+ const ultimo = React.useRef(destino);
94
+ const corrente = React.useRef(null);
95
+ React.useEffect(() => () => corrente.current?.stop(), []);
96
+ React.useEffect(() => {
97
+ if (ultimo.current === destino)
98
+ return;
99
+ ultimo.current = destino;
100
+ corrente.current?.stop();
101
+ corrente.current = null;
102
+ if (semMovimento) {
103
+ valor.setValue(destino);
104
+ return;
105
+ }
106
+ const a = criar(destino);
107
+ a.start();
108
+ corrente.current = a;
109
+ // `criar` fica de fora de propósito: ele nasce a cada desenho, e o efeito só tem de rodar na troca.
110
+ }, [valor, destino, semMovimento]);
111
+ }
112
+ /** O `Easing.out(Easing.ease)` da referência, escrito como a curva que ele é: o `ease` do RN é
113
+ * `bezier(0.42, 0, 1, 1)`, e o `out` o espelha. */
114
+ const SAIDA_SUAVE = Easing.bezier(0, 0, 0.58, 1);
115
+ // A mola da seta, a da referência. Passa do amortecimento crítico (2·√(1000·4) ≈ 126), então não
116
+ // balança: assenta em cerca de 0,4 s.
117
+ const MOLA_DA_SETA = { damping: 140, stiffness: 1000, mass: 4 };
118
+ const ESMAECER_DA_RESPOSTA = 200;
119
+ /**
120
+ * Seções que abrem e fecham — o accordion da referência, com a API da web (`items`, `variant`,
121
+ * `multiple`, `defaultValue`).
122
+ *
123
+ * Um de cada vez por padrão, e o aberto fecha no toque (o `isCollapsible` dela, ligado). O
124
+ * gatilho é um botão que diz se está aberto (`expanded`) dentro de um título (`header`), como o
125
+ * dela; a resposta fechada não está na árvore, e o leitor de tela não a lê.
126
+ */
127
+ export function Accordion({ items, variant = "default", multiple = false, defaultValue, value, onValueChange, style, testID, }) {
128
+ const t = useAureaTokens();
129
+ const s = folha(t);
130
+ const [interno, setInterno] = React.useState(defaultValue ?? []);
131
+ const abertos = value !== undefined ? value : interno;
132
+ const surface = variant === "surface";
133
+ const alternar = (id) => {
134
+ const aberto = abertos.includes(id);
135
+ const novo = multiple
136
+ ? (aberto ? abertos.filter((x) => x !== id) : [...abertos, id])
137
+ : (aberto ? [] : [id]);
138
+ if (value === undefined)
139
+ setInterno(novo);
140
+ onValueChange?.(novo);
141
+ };
142
+ return (_jsx(View, { testID: testID, style: [s.accordion, surface && s.accordionSurface, style], children: items.map((item, i) => (_jsxs(React.Fragment, { children: [i > 0 ? _jsx(View, { style: [s.fio, surface && s.fioSurface] }) : null, _jsx(ItemDoAccordion, { item: item, aberto: abertos.includes(item.id), surface: surface, onAlternar: alternar, testID: testID ? `${testID}-${item.id}` : undefined })] }, item.id))) }));
143
+ }
144
+ function ItemDoAccordion({ item, aberto, surface, onAlternar, testID }) {
145
+ const t = useAureaTokens();
146
+ const s = folha(t);
147
+ const reduzir = useReduceMotion();
148
+ const inativo = !!item.disabled;
149
+ const giro = React.useRef(new Animated.Value(aberto ? 1 : 0)).current;
150
+ useLevarA(giro, aberto ? 1 : 0, reduzir !== false, (destino) => Animated.spring(giro, { toValue: destino, ...MOLA_DA_SETA, useNativeDriver: driverNativo() }));
151
+ // A resposta esmaece ao ABRIR. Um valor novo a cada troca, que já nasce em 0 quando vai esmaecer:
152
+ // zerar depois, num efeito, deixaria passar um quadro com a resposta cheia. No primeiro desenho
153
+ // ela nasce em 1 — o item que começa aberto não "abre".
154
+ const montado = React.useRef(false);
155
+ const entrada = React.useMemo(() => {
156
+ const esmaecer = montado.current && aberto && reduzir === false;
157
+ return { valor: new Animated.Value(esmaecer ? 0 : 1), esmaecer };
158
+ }, [aberto]); // só a troca: a resposta do "menos movimento" chegando depois não esmaece de novo
159
+ React.useEffect(() => { montado.current = true; }, []);
160
+ React.useEffect(() => {
161
+ if (!entrada.esmaecer)
162
+ return;
163
+ const a = Animated.timing(entrada.valor, {
164
+ toValue: 1, duration: ESMAECER_DA_RESPOSTA, easing: SAIDA_SUAVE, useNativeDriver: driverNativo(),
165
+ });
166
+ a.start();
167
+ return () => a.stop();
168
+ }, [entrada]);
169
+ // O realce do dedo é o da web: 3% da cor do texto, ou o cinza neutro no `surface`; e só no
170
+ // fechado, como lá (`:not([open])`).
171
+ const realce = surface ? t.color.secondary : comOpacidade(t.color.foreground, 0.03);
172
+ const emTexto = (n) => typeof n === "string" || typeof n === "number";
173
+ return (_jsxs(View, { children: [_jsx(View, { accessibilityRole: "header", children: _jsxs(Pressable, { testID: testID, disabled: inativo,
174
+ // A guarda repete o `disabled` de propósito, como o gatilho da referência: o toque que chega
175
+ // por outro caminho (o leitor de tela, um teste) também não abre o item desligado.
176
+ onPress: () => { if (!inativo)
177
+ onAlternar(item.id); }, accessibilityRole: "button", ...estadoAcessivel({ expanded: aberto, disabled: inativo }), style: ({ pressed }) => [
178
+ s.gatilho, surface && s.gatilhoSurface,
179
+ pressed && !aberto && { backgroundColor: realce },
180
+ inativo && s.inativo,
181
+ ], children: [_jsx(View, { style: s.titulo, children: emTexto(item.title) ? _jsx(Text, { size: "sm", weight: 500, children: item.title }) : item.title }), _jsx(Animated.View, { style: [s.seta, { transform: [{ rotate: giro.interpolate({
182
+ inputRange: [0, 1], outputRange: ["0deg", "-180deg"],
183
+ }) }] }], children: _jsx(Icon, { name: "caret-down", size: "sm", color: t.color.foreground }) })] }) }), aberto ? (_jsx(Animated.View, { testID: testID ? `${testID}-content` : undefined, style: [s.resposta, surface && s.respostaSurface, { opacity: entrada.valor }], children: emTexto(item.content)
184
+ ? _jsx(Text, { size: "sm", tone: "muted", leading: "relaxed", children: item.content })
185
+ : item.content })) : null] }));
186
+ }
187
+ const ESMAECER_DO_FIO = 200;
188
+ // O da referência: um aviso de rolagem por quadro.
189
+ const AVISO_DE_ROLAGEM = 16;
190
+ /**
191
+ * "Tem mais para lá" — o scroll-shadow da referência, com um FIO no lugar do degradê.
192
+ *
193
+ * 🔴 **Decisão do Victor:** a referência esmaece a ponta com um degradê de 50; a Aurea não tem
194
+ * degradê (CLAUDE.md §5). Aqui é uma linha de 1 (a borda da Aurea, na cor de borda do tema), com as
195
+ * pontas em cápsula, na ponta onde há mais conteúdo escondido, e SÓ lá: no topo da lista não há fio
196
+ * em cima; no fim, não há embaixo. Na horizontal, à esquerda e à direita.
197
+ *
198
+ * 🔴 **E a lista mora numa superfície da Aurea** (Victor, 12/10/2026: *"o ScrollShadow não é [arredondado]
199
+ * como todos da Aurea"*, apontando o `Accordion` no cartão): por padrão, fundo do cartão, canto 22 e a
200
+ * sombra de superfície — a referência não tem caixa nenhuma. `variant="plain"` tira a caixa, para dentro
201
+ * de um cartão; aí o fio recua 22 dos lados, para não fazer quina.
202
+ *
203
+ * Ele mede como o dela, pelos três avisos do rolador (`onLayout`, `onContentSizeChange` e
204
+ * `onScroll`), e entrega ao rolador os avisos que o app já tinha posto, depois dos dele. O
205
+ * `inverted` (a conversa, de baixo para cima) troca as pontas. O fio entra e sai esmaecendo em
206
+ * 200 ms; com "menos movimento", aparece e some de uma vez.
207
+ *
208
+ * ⚠ Um `onScroll` que não é função — o `Animated.event` com o motor nativo — não é repassado: o
209
+ * aviso dele vive no motor, e não há como chamá-lo daqui.
210
+ */
211
+ export function ScrollShadow({ children, orientation, visibility = "auto", isEnabled = true, variant = "surface", style, testID, }) {
212
+ const t = useAureaTokens();
213
+ const s = folha(t);
214
+ const reduzir = useReduceMotion();
215
+ const filho = React.isValidElement(children) ? children : null;
216
+ const p = filho?.props ?? {};
217
+ const horizontal = orientation ? orientation === "horizontal" : !!p.horizontal;
218
+ const invertido = !!p.inverted;
219
+ const medidas = React.useRef({ deslocamento: 0, visivel: 0, conteudo: 0 });
220
+ const [mais, setMais] = React.useState({ antes: false, depois: false });
221
+ const recalcular = () => {
222
+ const { deslocamento, visivel, conteudo } = medidas.current;
223
+ // A sobra menor que a espessura do próprio fio não conta: o arredondamento de pixel deixa o
224
+ // deslocamento em 0,33 com a lista parada no topo, e o fio acenderia sem nada escondido.
225
+ const folga = t.size.borderWidth;
226
+ const antes = deslocamento > folga;
227
+ const depois = visivel > 0 && conteudo - (deslocamento + visivel) > folga;
228
+ setMais((a) => (a.antes === antes && a.depois === depois ? a : { antes, depois }));
229
+ };
230
+ const comecoPode = visibility !== "none" && visibility !== "bottom" && visibility !== "right";
231
+ const fimPode = visibility !== "none" && visibility !== "top" && visibility !== "left";
232
+ const noComeco = isEnabled && comecoPode && (invertido ? mais.depois : mais.antes);
233
+ const noFim = isEnabled && fimPode && (invertido ? mais.antes : mais.depois);
234
+ const opComeco = React.useRef(new Animated.Value(0)).current;
235
+ const opFim = React.useRef(new Animated.Value(0)).current;
236
+ const esmaecer = (v) => (destino) => Animated.timing(v, { toValue: destino, duration: ESMAECER_DO_FIO, useNativeDriver: driverNativo() });
237
+ useLevarA(opComeco, noComeco ? 1 : 0, reduzir !== false, esmaecer(opComeco));
238
+ useLevarA(opFim, noFim ? 1 : 0, reduzir !== false, esmaecer(opFim));
239
+ if (!filho)
240
+ return null;
241
+ const rolador = React.cloneElement(filho, {
242
+ scrollEventThrottle: p.scrollEventThrottle ?? AVISO_DE_ROLAGEM,
243
+ onLayout: (e) => {
244
+ const { width, height } = e.nativeEvent.layout;
245
+ medidas.current.visivel = horizontal ? width : height;
246
+ recalcular();
247
+ p.onLayout?.(e);
248
+ },
249
+ onContentSizeChange: (w, h) => {
250
+ medidas.current.conteudo = horizontal ? w : h;
251
+ recalcular();
252
+ p.onContentSizeChange?.(w, h);
253
+ },
254
+ onScroll: (e) => {
255
+ const { contentOffset, layoutMeasurement, contentSize } = e.nativeEvent;
256
+ const m = medidas.current;
257
+ m.deslocamento = horizontal ? contentOffset.x : contentOffset.y;
258
+ if (layoutMeasurement)
259
+ m.visivel = horizontal ? layoutMeasurement.width : layoutMeasurement.height;
260
+ if (contentSize)
261
+ m.conteudo = horizontal ? contentSize.width : contentSize.height;
262
+ recalcular();
263
+ if (typeof p.onScroll === "function")
264
+ p.onScroll(e);
265
+ },
266
+ });
267
+ // Os fios são enfeite: fora do toque e fora do leitor de tela — quem lê a lista já ouve onde está.
268
+ const decorativo = {
269
+ pointerEvents: "none",
270
+ accessibilityElementsHidden: true,
271
+ importantForAccessibility: "no-hide-descendants",
272
+ };
273
+ const superficie = variant === "surface";
274
+ const noLado = superficie && (horizontal ? s.fioDaEsquerdaNaSuperficie : s.fioDeCimaNaSuperficie);
275
+ const fios = (_jsxs(_Fragment, { children: [_jsx(Animated.View, { ...decorativo, testID: testID ? `${testID}-start` : undefined, style: [horizontal ? s.fioDaEsquerda : s.fioDeCima, noLado, { opacity: opComeco }] }), _jsx(Animated.View, { ...decorativo, testID: testID ? `${testID}-end` : undefined, style: [horizontal ? s.fioDaDireita : s.fioDeBaixo, noLado, { opacity: opFim }] })] }));
276
+ if (!superficie) {
277
+ return _jsxs(View, { testID: testID, style: style, children: [rolador, fios] });
278
+ }
279
+ // Na superfície, os fios ficam na borda da JANELA da lista (o miolo), e não na da caixa.
280
+ return (_jsx(View, { testID: testID, style: [s.superficieDaLista, style], children: _jsxs(View, { style: s.miolo, children: [rolador, fios] }) }));
281
+ }
282
+ /**
283
+ * Um comando para vários esqueletos — o skeleton-group da referência. O `isLoading` e a `animation`
284
+ * descem para todo `Skeleton` de dentro (a qualquer profundidade), e cada um pode trocar o seu.
285
+ *
286
+ * O PULSO é um só, do grupo: as caixas sobem e descem juntas, em vez de cada uma no tempo em que
287
+ * montou. `SkeletonGroup.Item` é o próprio `Skeleton` — o nome existe porque a referência o tem.
288
+ */
289
+ function SkeletonGroupRaiz({ children, isLoading = true, isSkeletonOnly = false, animation = "pulse", style, testID, }) {
290
+ const pulso = usePulsoDoEsqueleto(isLoading && animation === "pulse");
291
+ const valor = React.useMemo(() => ({ isLoading, animation, pulso }), [isLoading, animation, pulso]);
292
+ if (isSkeletonOnly && !isLoading)
293
+ return null;
294
+ return (_jsx(GrupoDeEsqueleto.Provider, { value: valor, children: _jsx(View, { testID: testID, style: style, children: children }) }));
295
+ }
296
+ export const SkeletonGroup = Object.assign(SkeletonGroupRaiz, { Item: Skeleton });
297
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
298
+ // PressableFeedback
299
+ // ─────────────────────────────────────────────────────────────────────────────────────────────
300
+ // Os números da referência.
301
+ const ESCALA_PRESSIONADA = 0.985;
302
+ const LARGURA_DE_REFERENCIA = 300; // a peça de 300 encolhe 0,985; a de 600, metade disso
303
+ const DURACAO_DA_ESCALA = 300;
304
+ const OPACIDADE_DO_REALCE = 0.1;
305
+ const DURACAO_DO_REALCE = 200;
306
+ const FATOR_DO_RAIO = 1.25; // o círculo da onda: 1,25 × a diagonal da peça
307
+ const DIAGONAL_DE_REFERENCIA = 450; // a onda de 1000 ms é a de uma peça com 450 de diagonal
308
+ const DURACAO_DA_ONDA = 1000;
309
+ const DURACAO_MINIMA_DA_ONDA = 750;
310
+ /** O quanto a peça encolhe: `escala` numa peça de 300; mais larga, menos; mais estreita, mais. */
311
+ const alvoDaEscala = (escala, largura) => 1 - (1 - escala) * (largura > 0 ? LARGURA_DE_REFERENCIA / largura : 1);
312
+ const Toque = React.createContext(null);
313
+ function useToque(peca) {
314
+ const v = React.useContext(Toque);
315
+ if (!v)
316
+ throw new Error(`Aurea ${peca}: use dentro de um PressableFeedback.`);
317
+ return v;
318
+ }
319
+ // A raiz é o `Pressable` ANIMADO, como o dela: a escala vai no próprio alvo do toque, com o fundo e
320
+ // a borda de quem usa junto. Criado no primeiro desenho, e não ao importar o módulo — importar o
321
+ // pacote não pode depender de uma função de animação existir (o dublê dos testes não a tem).
322
+ const criarPressionavel = () => Animated.createAnimatedComponent(Pressable);
323
+ let pressionavelAnimado = null;
324
+ /**
325
+ * Um alvo de toque que responde — o pressable-feedback da referência. Por padrão a peça inteira
326
+ * encolhe um pouco com o dedo em cima (0,985 numa peça de 300, proporcional à largura, em 300 ms).
327
+ * Dentro dele, três partes que se combinam: `PressableFeedback.Highlight` (a camada que acende),
328
+ * `PressableFeedback.Ripple` (a onda que nasce no dedo) e `PressableFeedback.Scale` (a escala num
329
+ * filho só).
330
+ *
331
+ * É um botão para o leitor de tela (`accessibilityRole` troca). Com "menos movimento" a escala e o
332
+ * realce SALTAM em vez de deslizar — como o `Button` da Aurea, que salta para 0,97 —, e a onda vira
333
+ * uma camada parada.
334
+ *
335
+ * ⚠ Ele é UM elemento para o leitor de tela: não ponha botão dentro (no iPhone, o de dentro some —
336
+ * o `check 43`). O que vai dentro é conteúdo.
337
+ */
338
+ function PressableFeedbackRaiz({ children, isDisabled = false, animation = true, scale = ESCALA_PRESSIONADA, style, onLayout, onPressIn, onPressOut, accessibilityRole, accessibilityState, ...resto }) {
339
+ const s = folha(useAureaTokens());
340
+ const reduzir = useReduceMotion();
341
+ const [pressionado, setPressionado] = React.useState(false);
342
+ const [medida, setMedida] = React.useState({ largura: 0, altura: 0 });
343
+ const tudoParado = animation === "disable-all";
344
+ const comEscala = animation !== false && !tudoParado;
345
+ const escala = React.useRef(new Animated.Value(1)).current;
346
+ useLevarA(escala, pressionado && comEscala ? alvoDaEscala(scale, medida.largura) : 1, reduzir !== false, (destino) => Animated.timing(escala, {
347
+ toValue: destino, duration: DURACAO_DA_ESCALA, easing: SAIDA_SUAVE, useNativeDriver: driverNativo(),
348
+ }));
349
+ const contexto = React.useMemo(() => ({ pressionado, largura: medida.largura, altura: medida.altura, tudoParado }), [pressionado, medida, tudoParado]);
350
+ const Raiz = (pressionavelAnimado ??= criarPressionavel());
351
+ return (_jsx(Toque.Provider, { value: contexto, children: _jsx(Raiz, { ...resto, disabled: isDisabled, accessibilityRole: accessibilityRole ?? "button", ...estadoAcessivel({ ...accessibilityState, disabled: isDisabled }), onLayout: (e) => {
352
+ const { width, height } = e.nativeEvent.layout;
353
+ setMedida((m) => (m.largura === width && m.altura === height ? m : { largura: width, altura: height }));
354
+ onLayout?.(e);
355
+ }, onPressIn: (e) => { setPressionado(true); onPressIn?.(e); }, onPressOut: (e) => { setPressionado(false); onPressOut?.(e); }, style: [s.raizDoToque, isDisabled && REACAO_AO_TOQUE.inerte,
356
+ comEscala && { transform: [{ scale: escala }] }, style], children: children }) }));
357
+ }
358
+ /** A escala num filho só — com `animation={false}` na raiz, para não encolher duas vezes. */
359
+ function EscalaDoToque({ children, scale = ESCALA_PRESSIONADA, animation = true, style, testID }) {
360
+ const toque = useToque("PressableFeedback.Scale");
361
+ const reduzir = useReduceMotion();
362
+ const ligado = animation && !toque.tudoParado;
363
+ const valor = React.useRef(new Animated.Value(1)).current;
364
+ useLevarA(valor, toque.pressionado && ligado ? alvoDaEscala(scale, toque.largura) : 1, reduzir !== false, (destino) => Animated.timing(valor, {
365
+ toValue: destino, duration: DURACAO_DA_ESCALA, easing: SAIDA_SUAVE, useNativeDriver: driverNativo(),
366
+ }));
367
+ return (_jsx(Animated.View, { testID: testID, style: [ligado && { transform: [{ scale: valor }] }, style], children: children }));
368
+ }
369
+ /** A camada que acende com o dedo em cima (o toque do iPhone): cobre a peça e vai a 10% em 200 ms. */
370
+ function RealceDoToque({ color, opacity = OPACIDADE_DO_REALCE, animation = true, style, testID }) {
371
+ const t = useAureaTokens();
372
+ const s = folha(t);
373
+ const toque = useToque("PressableFeedback.Highlight");
374
+ const reduzir = useReduceMotion();
375
+ const ligado = animation && !toque.tudoParado;
376
+ const valor = React.useRef(new Animated.Value(0)).current;
377
+ useLevarA(valor, toque.pressionado && ligado ? opacity : 0, reduzir !== false, (destino) => Animated.timing(valor, { toValue: destino, duration: DURACAO_DO_REALCE, useNativeDriver: driverNativo() }));
378
+ if (!ligado)
379
+ return null;
380
+ return (_jsx(Animated.View, { testID: testID, pointerEvents: "none", accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", style: [s.camada, { backgroundColor: color ?? t.color.foreground, opacity: valor }, style] }));
381
+ }
382
+ /**
383
+ * A onda que nasce no dedo (o toque do Android): um círculo de 1,25 × a diagonal da peça, que
384
+ * cresce de 0 até cobrir tudo e some ao soltar. Duas camadas que se revezam, como na referência: o
385
+ * toque novo começa numa camada limpa enquanto a anterior termina de sumir, e a onda não pisca.
386
+ *
387
+ * Círculo cheio, de UMA cor — a dela é um degradê do centro para fora, e a Aurea não tem degradê.
388
+ * Com "menos movimento" ela não cresce: acende inteira ao tocar e apaga ao soltar.
389
+ */
390
+ function OndaDoToque({ color, opacity = OPACIDADE_DO_REALCE, animation = true, style, testID }) {
391
+ const t = useAureaTokens();
392
+ const s = folha(t);
393
+ const toque = useToque("PressableFeedback.Ripple");
394
+ const reduzir = useReduceMotion();
395
+ const ligado = animation && !toque.tudoParado;
396
+ const camadas = React.useRef([0, 1].map(() => ({
397
+ x: new Animated.Value(0), y: new Animated.Value(0), progresso: new Animated.Value(0), viva: false, animacao: null,
398
+ }))).current;
399
+ const ativa = React.useRef(0);
400
+ React.useEffect(() => () => camadas.forEach((c) => c.animacao?.stop()), [camadas]);
401
+ const diagonal = Math.hypot(toque.largura, toque.altura);
402
+ const raio = diagonal * FATOR_DO_RAIO;
403
+ // A duração acompanha o tamanho: 1000 ms numa diagonal de 450, nunca menos de 750 nem mais de 2000.
404
+ const duracao = Math.min(Math.max(DURACAO_DA_ONDA * (diagonal > 0 ? diagonal / DIAGONAL_DE_REFERENCIA : 1), DURACAO_MINIMA_DA_ONDA), DURACAO_DA_ONDA * 2);
405
+ const levar = (c, destino) => {
406
+ c.animacao?.stop();
407
+ c.animacao = null;
408
+ if (reduzir !== false) {
409
+ c.progresso.setValue(destino);
410
+ return;
411
+ }
412
+ const a = Animated.timing(c.progresso, { toValue: destino, duration: duracao, useNativeDriver: driverNativo() });
413
+ a.start();
414
+ c.animacao = a;
415
+ };
416
+ const tocou = (e) => {
417
+ if (!ligado)
418
+ return;
419
+ const anterior = camadas[ativa.current];
420
+ // Só a onda que ainda está no ar termina de sumir; a camada parada em 0 iria a 2 passando pelo
421
+ // pico, e piscaria no lugar antigo. A que já foi solta segue o próprio caminho até 2.
422
+ if (anterior.viva) {
423
+ anterior.viva = false;
424
+ levar(anterior, 2);
425
+ }
426
+ ativa.current = 1 - ativa.current;
427
+ const nova = camadas[ativa.current];
428
+ nova.animacao?.stop();
429
+ nova.x.setValue(e.nativeEvent.locationX - raio);
430
+ nova.y.setValue(e.nativeEvent.locationY - raio);
431
+ nova.progresso.setValue(0);
432
+ nova.viva = true;
433
+ levar(nova, 1);
434
+ };
435
+ const soltou = () => {
436
+ const c = camadas[ativa.current];
437
+ if (ligado && c.viva) {
438
+ c.viva = false;
439
+ levar(c, 2);
440
+ }
441
+ };
442
+ if (!ligado)
443
+ return null;
444
+ return (_jsx(View, { testID: testID, accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", onTouchStart: tocou, onTouchEnd: soltou, onTouchCancel: soltou, style: [s.camada, style], children: camadas.map((c, i) => (_jsx(Animated.View, { pointerEvents: "none", style: [s.onda, {
445
+ width: raio * 2, height: raio * 2, ...canto(raio), backgroundColor: color ?? t.color.foreground,
446
+ opacity: c.progresso.interpolate({ inputRange: [0, 1, 2], outputRange: [0, opacity, 0] }),
447
+ transform: [
448
+ { translateX: c.x }, { translateY: c.y },
449
+ { scale: c.progresso.interpolate({ inputRange: [0, 1, 2], outputRange: [0, 1, 1] }) },
450
+ ],
451
+ }] }, i))) }));
452
+ }
453
+ export const PressableFeedback = Object.assign(PressableFeedbackRaiz, {
454
+ Scale: EscalaDoToque,
455
+ Highlight: RealceDoToque,
456
+ Ripple: OndaDoToque,
457
+ });
@@ -1,10 +1,13 @@
1
1
  import * as React from "react";
2
- import { type StyleProp, type ViewProps, type ViewStyle } from "react-native";
2
+ import { Animated, type StyleProp, type ViewProps, type ViewStyle } from "react-native";
3
3
  import { type IconName, type AureaIcon } from "./icon.js";
4
4
  import { type AureaUniversalState } from "./strings.js";
5
5
  export type AureaSpinnerSize = "sm" | "md" | "lg";
6
6
  export interface SpinnerProps {
7
- /** `sm` é o padrão, como na web (`.spinner` sem modificador usa `--icon-sm`). */
7
+ /**
8
+ * 16 · 24 · 32, e o padrão é o `md` (24) — a escala do pacote nativo da referência (0.32.0).
9
+ * Até a 0.31.0 era 16 · 20 · 24, com o `sm` de padrão, como a web.
10
+ */
8
11
  size?: AureaSpinnerSize;
9
12
  /** O que o leitor de tela anuncia. Sem ele, a frase `loading` do provider. */
10
13
  label?: string;
@@ -22,6 +25,9 @@ export interface SpinnerProps {
22
25
  * "algo está acontecendo" continua vendo o rótulo.
23
26
  */
24
27
  export declare function Spinner({ size, label, decorative, style, testID }: SpinnerProps): React.JSX.Element;
28
+ /** O pulso (`"pulse"`, o padrão) ou a caixa parada (`"none"`) — os nomes da web (0.32.0). O brilho que
29
+ * corre da referência ("shimmer") é degradê, e degradê a Aurea não tem. */
30
+ export type AureaSkeletonAnimation = "pulse" | "none";
25
31
  export interface SkeletonProps {
26
32
  /** Padrão: ocupa a largura disponível. */
27
33
  width?: number | `${number}%`;
@@ -30,7 +36,36 @@ export interface SkeletonProps {
30
36
  radius?: number;
31
37
  style?: StyleProp<ViewStyle>;
32
38
  testID?: string;
39
+ /**
40
+ * 0.32.0: `false` troca a caixa pelo conteúdo de verdade (`children`) — o `isLoading` da referência.
41
+ * Padrão: `true`, a caixa de sempre. Dentro de um `SkeletonGroup`, sem este, vale o do grupo.
42
+ */
43
+ isLoading?: boolean;
44
+ /** O conteúdo que aparece no lugar da caixa quando `isLoading` é `false`. */
45
+ children?: React.ReactNode;
46
+ /** `"none"` deixa a caixa parada, como na web. Dentro de um `SkeletonGroup`, sem este, vale o do grupo. */
47
+ animation?: AureaSkeletonAnimation;
48
+ }
49
+ /**
50
+ * O que o `SkeletonGroup` manda para os esqueletos de dentro: o `isLoading` e a `animation` do grupo,
51
+ * e o PULSO — um valor só, e por isso todos sobem e descem juntos, em vez de cada um no seu tempo.
52
+ *
53
+ * Mora aqui, ao lado do `Skeleton`, porque é ele quem lê. O grupo (`exibicao.tsx`) importa daqui;
54
+ * o contrário fecharia um ciclo de importação entre os dois arquivos. Ferramenta de dentro: não sai
55
+ * pela porta da frente do pacote.
56
+ */
57
+ export interface GrupoDeEsqueletoValor {
58
+ isLoading: boolean;
59
+ animation: AureaSkeletonAnimation;
60
+ pulso: Animated.Value;
33
61
  }
62
+ export declare const GrupoDeEsqueleto: React.Context<GrupoDeEsqueletoValor | null>;
63
+ /**
64
+ * O pulso do esqueleto: liga só quando `ativo`, e só depois de o sistema dizer que pode mexer.
65
+ * Desligado, a opacidade volta a 1 — um laço parado no meio deixaria a caixa meio apagada.
66
+ * O `SkeletonGroup` usa o mesmo, uma vez para o grupo inteiro.
67
+ */
68
+ export declare function usePulsoDoEsqueleto(ativo: boolean): Animated.Value;
34
69
  /**
35
70
  * A caixa cinza que pulsa enquanto o dado não chega.
36
71
  *
@@ -41,8 +76,13 @@ export interface SkeletonProps {
41
76
  *
42
77
  * Ele é **invisível para o leitor de tela** (`aria-hidden` na web, `accessibilityElementsHidden`
43
78
  * aqui): anunciar "caixa" três vezes enquanto a tela carrega não ajuda ninguém.
79
+ *
80
+ * 0.32.0: o `isLoading` e os `children` da referência. Com `isLoading={false}` a caixa sai e o
81
+ * conteúdo entra, num esmaecer da duração lenta da Aurea (a referência usa o esmaecer padrão do
82
+ * motor dela); com "menos movimento", ou no primeiro desenho, ele entra sem esmaecer. Sem as props
83
+ * novas, nada muda: é a caixa de sempre.
44
84
  */
45
- export declare function Skeleton({ width, height, radius, style, testID }: SkeletonProps): React.JSX.Element;
85
+ export declare function Skeleton({ width, height, radius, style, testID, isLoading, children, animation }: SkeletonProps): React.JSX.Element;
46
86
  /** O tom da barra: a mesma lista fechada do `tone` do `Button`. Pausado é `neutral`; falha é `danger`. */
47
87
  export type AureaProgressTone = "brand" | "neutral" | "success" | "warning" | "danger" | "info";
48
88
  export interface ProgressProps {
@@ -137,6 +177,10 @@ export interface DataStateProps extends Omit<ViewProps, "children"> {
137
177
  action?: React.ReactNode;
138
178
  /** Função para o conteúdo caro não ser construído enquanto o estado o esconde. */
139
179
  children: React.ReactNode | (() => React.ReactNode);
180
+ /** A barreira de erro (MNT-25): o que quebrar ao desenhar o conteúdo vira a cara de erro, com "Tentar de novo". */
181
+ catchErrors?: boolean;
182
+ /** Recebe o erro que a barreira pegou — para o app registrar. */
183
+ onCatch?: (error: unknown, info: React.ErrorInfo) => void;
140
184
  }
141
185
  /**
142
186
  * A região que troca de cara. **Ela não desenha nada próprio** — empilha o que já existe.
@@ -146,4 +190,4 @@ export interface DataStateProps extends Omit<ViewProps, "children"> {
146
190
  * dado está aí e tem ressalva — esconder o dado seria trocar informação parcial por informação
147
191
  * nenhuma. Só `loading`, `error` e `empty` tomam o lugar.
148
192
  */
149
- export declare function DataState({ state, message, skeleton, emptyTitle, emptyIcon, action, children, style, ...rest }: DataStateProps): React.JSX.Element;
193
+ export declare function DataState({ catchErrors, onCatch, ...props }: DataStateProps): React.JSX.Element;