@aurea-uds/native 0.8.6 → 0.8.8

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.
@@ -33,31 +33,66 @@ export interface BottomNavProps extends ViewProps {
33
33
  * ]} />
34
34
  * ```
35
35
  *
36
- * ⚠ **Ela respeita a barra de gestos sozinha.** Na web isso é
37
- * `padding-bottom: max(--space-1, env(safe-area-inset-bottom))` (`aurea.css:270`); aqui é o
38
- * `useSafeAreaInsets().bottom`, pela mesma conta de máximo. Sem isso, num aparelho com barra de
39
- * gestos, a última linha de rótulo fica embaixo dela.
36
+ * ⚠ **Ela respeita a área do sistema sozinha, e desde 19/09/2026 POR FORA da pílula.** No
37
+ * `floating` a folga vira `marginBottom`: a borda de baixo fica exatamente `space4` acima do topo
38
+ * da área do sistema, seja ela a faixa dos três botões do Android (≈48), a linha de gesto (≈16)
39
+ * ou o indicador do iPhone. No `edge`, que encosta na borda, ela continua sendo recheio por
40
+ * dentro — igual à web, onde `.bottom-nav-edge` faz exatamente isso.
40
41
  *
41
42
  * ⚠ **Não é `Tabs`, e isso é decisão registrada**, não esquecimento — ver o bloco acima.
42
43
  *
43
- * 🔴 **E VOCÊ FICA DONO DO FUNDO ATRÁS DELA — leia isto antes de montar a primeira tela.** Esta
44
- * barra é IRMÃ da tela, não filha: é assim que se põe uma barra fixa no RN. Só que o `Screen`
45
- * pinta o tema **na própria raiz**, então o que aparece atrás desta barra é a raiz do SEU app —
46
- * e raiz sem cor, no Android, é **BRANCA**. Num app de tema escuro isso é uma faixa branca no pé
47
- * da tela. Envolva os dois num `View` com `flex: 1` e `backgroundColor: tokens.color.background`:
44
+ * ── COMO MONTAR AS ABAS, com `expo-router` ou `react-navigation` ─────────────────────────────
48
45
  *
49
46
  * ```tsx
50
- * const t = useAureaTokens();
51
- * <View style={{flex: 1, backgroundColor: t.color.background}}>
52
- * <Screen scroll>{…}</Screen>
53
- * <BottomNav items={…} current={…} />
54
- * </View>
47
+ * import {Tabs} from "expo-router";
48
+ * import {BottomNav, BottomNavProvider} from "@aurea-uds/native";
49
+ *
50
+ * const ROTAS = [
51
+ * {name: "index", titulo: "Início", icone: "home"},
52
+ * {name: "perfil", titulo: "Perfil", icone: "user"},
53
+ * ];
54
+ *
55
+ * <BottomNavProvider>
56
+ * <Tabs
57
+ * screenOptions={{headerShown: false}}
58
+ * tabBar={({state, navigation}) => (
59
+ * <BottomNav
60
+ * label="Menu principal"
61
+ * current={state.routes[state.index].name}
62
+ * items={ROTAS.map((r) => ({
63
+ * id: r.name, label: r.titulo, icon: r.icone,
64
+ * onPress: () => navigation.navigate(r.name),
65
+ * }))} />
66
+ * )} />
67
+ * </BottomNavProvider>
55
68
  * ```
56
69
  *
70
+ * E as telas, sem conta nenhuma de respiro: `<Screen scroll edges={["top"]}>`.
71
+ *
72
+ * 🔴 **O `BottomNav` VAI SOLTO NO `tabBar`, SEM `View` EM VOLTA — e esta linha já mandou o
73
+ * contrário.** Até a `0.8.7` a documentação aqui mandava embrulhar a barra num `View` pintado com
74
+ * o fundo do tema, porque a barra ficava no FLUXO e o que aparecia atrás dela era a raiz do app,
75
+ * que no Android é branca. O consumidor seguiu, e o resultado em aparelho foi uma **caixa cinza
76
+ * em volta da pílula**: ela não flutuava, e a tela terminava acima dela.
77
+ *
78
+ * ✅ **Hoje o `floating` é `position: absolute`** — ele sai do fluxo e passa por cima da tela,
79
+ * que volta a ocupar a altura inteira. Não há nada atrás da pílula para pintar, então **o
80
+ * embrulho não é mais necessário e atrapalha**: ele é quem desenha a faixa.
81
+ *
82
+ * ⚠ **O `edge` continua no fluxo**, e para ELE o conselho antigo continua valendo: embrulhe os
83
+ * dois num `View` com `flex: 1` e `backgroundColor: t.color.background`, senão sobra a faixa
84
+ * branca do Android no pé da tela.
85
+ *
57
86
  * ⚠ **E o sintoma engana.** Visto em foto no aparelho em 10/09/2026: com a faixa branca a barra
58
87
  * *parece* flutuando longe do pé, que é exatamente o sinal de que o respiro virou SOMA em vez de
59
88
  * `Math.max`. A conta estava certa; o branco é que empurrava a leitura. **Antes de acusar a
60
89
  * aritmética, pinte o fundo.**
90
+ *
91
+ * 🔴 **FLUTUAR TEM UM PREÇO, E A BIBLIOTECA É QUEM PAGA:** o fim da rolagem fica atrás da pílula.
92
+ * É para isso que o `BottomNavProvider` existe — a barra mede a própria altura e conta para ele,
93
+ * e o `Screen scroll` reserva o espaço sozinho. **O app não calcula nada.** Para uma tela que não
94
+ * usa `Screen` (uma fileira de botões presa no pé, por exemplo), o número está em
95
+ * `useBottomNavSpace()`. Fora de um provedor os dois valem zero, e nada muda.
61
96
  */
62
97
  export declare function BottomNav({ items, current, variant, indicator, label, style, ...rest }: BottomNavProps): React.JSX.Element;
63
98
  export type AureaTopbarVariant = "floating" | "flush" | "pill";
@@ -1,6 +1,7 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Pressable, View } from "react-native";
3
3
  import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import { useAnotarBottomNav } from "./barranav.js";
4
5
  import { Badge } from "./display.js";
5
6
  import { comOpacidade, criarFolha } from "./estilos.js";
6
7
  import { Icon } from "./icon.js";
@@ -32,14 +33,28 @@ const folha = criarFolha((t) => ({
32
33
  borderRadius: t.size.radiusControl, backgroundColor: t.color.card,
33
34
  },
34
35
  // ── BottomNav ──────────────────────────────────────────────────────────────────────────────
36
+ // 🔴 `floating` FLUTUA POR CIMA DA TELA, e isto é conserto de 19/09/2026 — a variante prometia
37
+ // isso no nome e não fazia. `position: absolute` a tira do fluxo: a tela passa a ocupar a altura
38
+ // inteira e rola POR BAIXO da pílula. É o mesmo que a web faz desde sempre com
39
+ // `position: sticky; bottom: 0` (`aurea.css:270`) — a divergência era só deste lado.
40
+ //
41
+ // ⚠ **As margens são o que deixa o toque passar.** Com `left/right/bottom: 0` e `margin`, a
42
+ // caixa deste `View` É a pílula: o que sobra em volta não pertence a ela, e o dedo encontra a
43
+ // tela de baixo. Se a barra fosse uma faixa de largura cheia com a pílula centralizada dentro,
44
+ // a faixa comeria o toque em silêncio.
45
+ //
46
+ // `zSticky` é token (20), o mesmo `--z-sticky` que a web usa na mesma peça.
35
47
  barra: {
48
+ position: "absolute", left: 0, right: 0, bottom: 0, zIndex: t.size.zSticky,
36
49
  flexDirection: "row", gap: t.size.space1, margin: t.size.space4, padding: t.size.space1,
37
50
  borderWidth: t.size.borderWidth, borderColor: t.color.border,
38
51
  borderRadius: t.size.radiusControl, backgroundColor: t.color.card,
39
52
  boxShadow: [t.shadow.shadowMd],
40
53
  },
41
- // `edge` encosta na borda: sem margem, sem raio, só a linha de cima.
54
+ // `edge` encosta na borda: sem margem, sem raio, só a linha de cima — e continua NO FLUXO,
55
+ // porque uma barra encostada que flutuasse esconderia o fim da tela sem nada reservar.
42
56
  barra_edge: {
57
+ position: "relative", left: undefined, right: undefined, bottom: undefined, zIndex: undefined,
43
58
  margin: 0, paddingHorizontal: 0, borderRadius: 0, backgroundColor: t.color.background,
44
59
  borderWidth: 0, borderTopWidth: t.size.borderWidth, boxShadow: undefined,
45
60
  },
@@ -112,31 +127,66 @@ const folha = criarFolha((t) => ({
112
127
  * ]} />
113
128
  * ```
114
129
  *
115
- * ⚠ **Ela respeita a barra de gestos sozinha.** Na web isso é
116
- * `padding-bottom: max(--space-1, env(safe-area-inset-bottom))` (`aurea.css:270`); aqui é o
117
- * `useSafeAreaInsets().bottom`, pela mesma conta de máximo. Sem isso, num aparelho com barra de
118
- * gestos, a última linha de rótulo fica embaixo dela.
130
+ * ⚠ **Ela respeita a área do sistema sozinha, e desde 19/09/2026 POR FORA da pílula.** No
131
+ * `floating` a folga vira `marginBottom`: a borda de baixo fica exatamente `space4` acima do topo
132
+ * da área do sistema, seja ela a faixa dos três botões do Android (≈48), a linha de gesto (≈16)
133
+ * ou o indicador do iPhone. No `edge`, que encosta na borda, ela continua sendo recheio por
134
+ * dentro — igual à web, onde `.bottom-nav-edge` faz exatamente isso.
119
135
  *
120
136
  * ⚠ **Não é `Tabs`, e isso é decisão registrada**, não esquecimento — ver o bloco acima.
121
137
  *
122
- * 🔴 **E VOCÊ FICA DONO DO FUNDO ATRÁS DELA — leia isto antes de montar a primeira tela.** Esta
123
- * barra é IRMÃ da tela, não filha: é assim que se põe uma barra fixa no RN. Só que o `Screen`
124
- * pinta o tema **na própria raiz**, então o que aparece atrás desta barra é a raiz do SEU app —
125
- * e raiz sem cor, no Android, é **BRANCA**. Num app de tema escuro isso é uma faixa branca no pé
126
- * da tela. Envolva os dois num `View` com `flex: 1` e `backgroundColor: tokens.color.background`:
138
+ * ── COMO MONTAR AS ABAS, com `expo-router` ou `react-navigation` ─────────────────────────────
127
139
  *
128
140
  * ```tsx
129
- * const t = useAureaTokens();
130
- * <View style={{flex: 1, backgroundColor: t.color.background}}>
131
- * <Screen scroll>{…}</Screen>
132
- * <BottomNav items={…} current={…} />
133
- * </View>
141
+ * import {Tabs} from "expo-router";
142
+ * import {BottomNav, BottomNavProvider} from "@aurea-uds/native";
143
+ *
144
+ * const ROTAS = [
145
+ * {name: "index", titulo: "Início", icone: "home"},
146
+ * {name: "perfil", titulo: "Perfil", icone: "user"},
147
+ * ];
148
+ *
149
+ * <BottomNavProvider>
150
+ * <Tabs
151
+ * screenOptions={{headerShown: false}}
152
+ * tabBar={({state, navigation}) => (
153
+ * <BottomNav
154
+ * label="Menu principal"
155
+ * current={state.routes[state.index].name}
156
+ * items={ROTAS.map((r) => ({
157
+ * id: r.name, label: r.titulo, icon: r.icone,
158
+ * onPress: () => navigation.navigate(r.name),
159
+ * }))} />
160
+ * )} />
161
+ * </BottomNavProvider>
134
162
  * ```
135
163
  *
164
+ * E as telas, sem conta nenhuma de respiro: `<Screen scroll edges={["top"]}>`.
165
+ *
166
+ * 🔴 **O `BottomNav` VAI SOLTO NO `tabBar`, SEM `View` EM VOLTA — e esta linha já mandou o
167
+ * contrário.** Até a `0.8.7` a documentação aqui mandava embrulhar a barra num `View` pintado com
168
+ * o fundo do tema, porque a barra ficava no FLUXO e o que aparecia atrás dela era a raiz do app,
169
+ * que no Android é branca. O consumidor seguiu, e o resultado em aparelho foi uma **caixa cinza
170
+ * em volta da pílula**: ela não flutuava, e a tela terminava acima dela.
171
+ *
172
+ * ✅ **Hoje o `floating` é `position: absolute`** — ele sai do fluxo e passa por cima da tela,
173
+ * que volta a ocupar a altura inteira. Não há nada atrás da pílula para pintar, então **o
174
+ * embrulho não é mais necessário e atrapalha**: ele é quem desenha a faixa.
175
+ *
176
+ * ⚠ **O `edge` continua no fluxo**, e para ELE o conselho antigo continua valendo: embrulhe os
177
+ * dois num `View` com `flex: 1` e `backgroundColor: t.color.background`, senão sobra a faixa
178
+ * branca do Android no pé da tela.
179
+ *
136
180
  * ⚠ **E o sintoma engana.** Visto em foto no aparelho em 10/09/2026: com a faixa branca a barra
137
181
  * *parece* flutuando longe do pé, que é exatamente o sinal de que o respiro virou SOMA em vez de
138
182
  * `Math.max`. A conta estava certa; o branco é que empurrava a leitura. **Antes de acusar a
139
183
  * aritmética, pinte o fundo.**
184
+ *
185
+ * 🔴 **FLUTUAR TEM UM PREÇO, E A BIBLIOTECA É QUEM PAGA:** o fim da rolagem fica atrás da pílula.
186
+ * É para isso que o `BottomNavProvider` existe — a barra mede a própria altura e conta para ele,
187
+ * e o `Screen scroll` reserva o espaço sozinho. **O app não calcula nada.** Para uma tela que não
188
+ * usa `Screen` (uma fileira de botões presa no pé, por exemplo), o número está em
189
+ * `useBottomNavSpace()`. Fora de um provedor os dois valem zero, e nada muda.
140
190
  */
141
191
  export function BottomNav({ items, current, variant = "floating", indicator = "none", label, style, ...rest }) {
142
192
  const t = useAureaTokens();
@@ -144,11 +194,29 @@ export function BottomNav({ items, current, variant = "floating", indicator = "n
144
194
  const strings = useAureaStrings();
145
195
  const inset = useSafeAreaInsets();
146
196
  const redondo = indicator.startsWith("circle");
147
- return (_jsx(View, { accessibilityLabel: label ?? strings.bottomNavLabel, style: [
197
+ const anotar = useAnotarBottomNav();
198
+ const flutua = variant !== "edge";
199
+ return (_jsx(View, { accessibilityLabel: label ?? strings.bottomNavLabel,
200
+ // 🔴 A BARRA CONTA A PRÓPRIA ALTURA — é o que permite o `Screen` reservar o fim da rolagem
201
+ // sem o app calcular nada. A conta é a pílula (o que o layout mede) mais as DUAS margens
202
+ // mais a folga do sistema; `edge` conta zero, porque ela fica no fluxo e já encurta a tela.
203
+ onLayout: anotar == null ? undefined : (e) => anotar(flutua ? e.nativeEvent.layout.height + t.size.space4 * 2 + inset.bottom : 0), style: [
148
204
  s.barra, variant === "edge" && s.barra_edge,
149
- // `max`, e não soma: com barra de gestos o respiro dela substitui o nosso; sem ela, o
150
- // nosso vale. Somar daria duas vezes o espaço num aparelho e o certo no outro.
151
- { paddingBottom: Math.max(t.size.space1, inset.bottom) },
205
+ // 🔴 NO `floating` A FOLGA DO SISTEMA VAI POR FORA — conserto de 19/09/2026, visto em
206
+ // aparelho Samsung com os três botões (|||, ○, <).
207
+ //
208
+ // Antes, os ≈48 pontos da faixa do sistema entravam como RECHEIO dentro da pílula: ela
209
+ // crescia para baixo e a barriga dela ficava atrás dos botões. A `margin: space4` não
210
+ // compensava, porque o que cresceu foi o miolo.
211
+ //
212
+ // Agora: recheio de baixo igual aos outros lados, e a folga empurra a pílula para cima.
213
+ // A borda de baixo fica exatamente `space4` acima do topo da área do sistema, em aparelho
214
+ // de botões, de gesto ou com o indicador do iPhone — os três caem na mesma conta.
215
+ flutua
216
+ ? { paddingBottom: t.size.space1, marginBottom: t.size.space4 + inset.bottom }
217
+ // `edge` NÃO MUDA: encostada na borda, a folga é recheio mesmo, senão sobraria uma
218
+ // faixa de fundo entre a barra e o pé da tela. `max` e não soma, como sempre foi.
219
+ : { paddingBottom: Math.max(t.size.space1, inset.bottom) },
152
220
  style,
153
221
  ], ...rest, children: items.map((it) => {
154
222
  const ativo = it.id === current;
package/dist/numero.d.ts CHANGED
@@ -103,6 +103,17 @@ type IconNameLocal = string;
103
103
  *
104
104
  * ⚠ **Formata no blur, e só no blur.** Enquanto o campo tem foco, ele mostra exatamente o que foi
105
105
  * digitado — essa é a decisão inteira da ADR-0024, e o teste que a cobra está no lote.
106
+ *
107
+ * ⚠ **`onValueChange` dispara a CADA TECLA**, com o número lido pelas regras do `locale` — "12,"
108
+ * em pt-BR entrega `12`, "1.234,5" entrega `1234.5`, apagar tudo entrega `null`. Texto que ainda
109
+ * não é número (só "," ou só "-") não dispara nada. **`min`/`max` NÃO prendem durante a
110
+ * digitação**, só no blur — e se prender mudar o número, ele é emitido de novo.
111
+ *
112
+ * ⚠ **Com FOCO, quem manda é quem digita.** Se o pai trocar o `value` por outro número enquanto o
113
+ * campo está sendo editado, o texto na tela **não muda** — inclusive quando o pai só devolve o
114
+ * mesmo número que acabou de receber, que é o caso comum de um `useState` controlado. Sem foco, o
115
+ * campo obedece ao `value` e mostra o número novo formatado, que é o que faz um campo calculado a
116
+ * partir de outros dois funcionar.
106
117
  */
107
118
  export declare function NumberField({ value, defaultValue, onValueChange, min, max, step, format, locale, disabled, readOnly, size, fullWidth, label, placeholder, keyboardType, icons, style, testID, }: NumberFieldProps): React.JSX.Element;
108
119
  export {};
package/dist/numero.js CHANGED
@@ -49,7 +49,7 @@ import { Platform, TextInput, View } from "react-native";
49
49
  import { IconButton } from "./actions.js";
50
50
  import { criarFolha } from "./estilos.js";
51
51
  import { useCampo } from "./inputs.js";
52
- import { useAureaStrings, useAureaTokens } from "./theme.js";
52
+ import { useAureaStrings, useAureaTokens, usePeleSobreAMarca } from "./theme.js";
53
53
  const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
54
54
  const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
55
55
  const folha = criarFolha((t) => ({
@@ -231,10 +231,22 @@ const paraEdicao = (n, locale) => {
231
231
  *
232
232
  * ⚠ **Formata no blur, e só no blur.** Enquanto o campo tem foco, ele mostra exatamente o que foi
233
233
  * digitado — essa é a decisão inteira da ADR-0024, e o teste que a cobra está no lote.
234
+ *
235
+ * ⚠ **`onValueChange` dispara a CADA TECLA**, com o número lido pelas regras do `locale` — "12,"
236
+ * em pt-BR entrega `12`, "1.234,5" entrega `1234.5`, apagar tudo entrega `null`. Texto que ainda
237
+ * não é número (só "," ou só "-") não dispara nada. **`min`/`max` NÃO prendem durante a
238
+ * digitação**, só no blur — e se prender mudar o número, ele é emitido de novo.
239
+ *
240
+ * ⚠ **Com FOCO, quem manda é quem digita.** Se o pai trocar o `value` por outro número enquanto o
241
+ * campo está sendo editado, o texto na tela **não muda** — inclusive quando o pai só devolve o
242
+ * mesmo número que acabou de receber, que é o caso comum de um `useState` controlado. Sem foco, o
243
+ * campo obedece ao `value` e mostra o número novo formatado, que é o que faz um campo calculado a
244
+ * partir de outros dois funcionar.
234
245
  */
235
246
  export function NumberField({ value, defaultValue, onValueChange, min, max, step = 1, format, locale, disabled, readOnly, size, fullWidth = false, label, placeholder, keyboardType, icons = { increment: "add", decrement: "subtract" }, style, testID, }) {
236
247
  const t = useAureaTokens();
237
248
  const s = folha(t);
249
+ const peleDaMarca = usePeleSobreAMarca();
238
250
  const strings = useAureaStrings();
239
251
  const campo = useCampo();
240
252
  const tam = size ?? campo?.size ?? "md";
@@ -276,8 +288,41 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
276
288
  emitir(null);
277
289
  return;
278
290
  }
291
+ // ⚠ **Só aqui é que `min`/`max` prendem**, e é no blur de propósito — ver `digitar` abaixo.
292
+ // Se prender mudou o número, o pai recebe o preso: quem ouviu "1" durante a digitação precisa
293
+ // ouvir o "10" final, senão fica com um valor que o campo não mostra mais.
279
294
  emitir(prender(lido, min, max));
280
295
  }, [emEdicao, locale, emitir, min, max]);
296
+ // 🔴 CADA TECLA ENTREGA O NÚMERO — e isto é conserto de defeito que GRAVA DADO ERRADO, não
297
+ // refinamento. Achado pelo consumidor em aparelho, 19/09/2026.
298
+ //
299
+ // Antes, `onChangeText` só guardava o texto aqui dentro e o pai só era avisado no blur. Num
300
+ // cartão com o campo e o botão de confirmar lado a lado, o `Screen` tem
301
+ // `keyboardShouldPersistTaps="handled"` (`screen.tsx:154`) — então o toque no botão CHEGA ao
302
+ // botão sem passar pelo blur. O `onPress` roda com o valor ANTIGO.
303
+ //
304
+ // ⚠ **A cara disso na tela é "o campo está vazio"**, e o dano real é maior e mudo: num
305
+ // formulário que corrige um número já salvo, tocar em Salvar com o teclado aberto regrava o
306
+ // número velho, sem erro e sem aviso.
307
+ //
308
+ // O que NÃO muda com isto, e é a ADR-0024 inteira: o campo continua mostrando o que foi
309
+ // digitado enquanto tem foco (`emEdicao` é quem manda em `mostrar`), e continua formatando só
310
+ // no blur. **Digitar nunca é interrompido** — nem pelo formato, nem pelo eco do pai: enquanto
311
+ // `emEdicao` não é `null`, nada que venha por `value` reescreve o texto.
312
+ const digitar = React.useCallback((texto) => {
313
+ setEmEdicao(texto);
314
+ const lido = lerNumero(texto, locale);
315
+ // Apagar tudo é VAZIO, que é diferente de ilegível: o vazio é uma intenção e vira `null`.
316
+ if (lido == null) {
317
+ if (texto.trim() === "")
318
+ emitir(null);
319
+ return;
320
+ }
321
+ // ⚠ **NÃO prende aqui.** Com `min={10}`, prender durante a digitação faria o "1" virar 10 na
322
+ // cara de quem ainda ia digitar o "5" de 15 — o campo escreveria por cima da pessoa. Prender
323
+ // é do blur, e só.
324
+ emitir(lido);
325
+ }, [locale, emitir]);
281
326
  const empurrar = React.useCallback((direcao) => {
282
327
  // Se o campo está sendo editado, o que vale é o que está escrito — empurrar por cima do valor
283
328
  // antigo descartaria a digitação em silêncio.
@@ -310,7 +355,7 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
310
355
  // (`aurea.css:685`), com os dois botões FORA dela. O estado que faltava é o deles; a geometria
311
356
  // continua sendo a nossa.
312
357
  const [focado, setFocado] = React.useState(false);
313
- return (_jsxs(View, { testID: testID, style: [s.grupo, fullWidth && s.grupoLargo, inativo && s.desabilitado, style], children: [mostrarBotoes && (_jsx(IconButton, { name: icons.decrement, label: strings.decrement, appearance: "ghost", size: tam, disabled: inativo || noLimite(-1), onPress: () => empurrar(-1), testID: testID ? `${testID}-menos` : undefined })), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: mostrar, onChangeText: setEmEdicao, onFocus: () => { setFocado(true); setEmEdicao(paraEdicao(numero, locale)); }, onBlur: () => { setFocado(false); confirmar(); }, editable: !inativo && !readOnly, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, keyboardType: keyboardType ?? tecladoPadrao,
358
+ return (_jsxs(View, { testID: testID, style: [s.grupo, fullWidth && s.grupoLargo, inativo && s.desabilitado, style], children: [mostrarBotoes && (_jsx(IconButton, { name: icons.decrement, label: strings.decrement, appearance: "ghost", size: tam, disabled: inativo || noLimite(-1), onPress: () => empurrar(-1), testID: testID ? `${testID}-menos` : undefined })), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: mostrar, onChangeText: digitar, onFocus: () => { setFocado(true); setEmEdicao(paraEdicao(numero, locale)); }, onBlur: () => { setFocado(false); confirmar(); }, editable: !inativo && !readOnly, placeholder: placeholder, placeholderTextColor: peleDaMarca?.color ?? t.color.subtleForeground, keyboardType: keyboardType ?? tecladoPadrao,
314
359
  // O nome vem do `Field`, como em todo controle deste pacote — e `label` cobre quem usa o
315
360
  // campo solto. Sem um dos dois, é um nó anônimo para o leitor de tela.
316
361
  accessibilityLabel: label ?? campo?.label, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, "aria-invalid": campo?.invalido, style: [
@@ -323,5 +368,10 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
323
368
  // depois. Um campo inválido que está sendo corrigido tem de mostrar que está ativo —
324
369
  // invertendo, a pessoa digita sem pista nenhuma de onde o teclado está batendo.
325
370
  focado && { borderColor: t.color.focusStrong },
371
+ // 🔴 E DENTRO DO CARTÃO DA MARCA A TINTA VENCE OS DOIS — `usePeleSobreAMarca` tem os
372
+ // números: sobre o amarelo o inválido mede 1,86 e o foco do tema escuro mede 1,00.
373
+ // O foco continua existindo, por espessura.
374
+ peleDaMarca,
375
+ peleDaMarca && focado && { borderWidth: t.size.borderWidth * 2 },
326
376
  ] }), mostrarBotoes && (_jsx(IconButton, { name: icons.increment, label: strings.increment, appearance: "ghost", size: tam, disabled: inativo || noLimite(1), onPress: () => empurrar(1), testID: testID ? `${testID}-mais` : undefined }))] }));
327
377
  }
package/dist/overlays.js CHANGED
@@ -61,7 +61,7 @@ import { Button } from "./actions.js";
61
61
  import { criarFolha } from "./estilos.js";
62
62
  import { IconButton } from "./actions.js";
63
63
  import { Text } from "./text.js";
64
- import { useAureaStrings, useAureaTokens } from "./theme.js";
64
+ import { useAureaStrings, useAureaTokens, ForaDaMarca } from "./theme.js";
65
65
  import { useReduceMotion } from "./movimento.js";
66
66
  const folha = criarFolha((t) => ({
67
67
  fundo: { flex: 1, backgroundColor: t.color.overlay },
@@ -157,7 +157,7 @@ export function Dialog({ open, title, children, footer, onClose, scroll = true,
157
157
  const s = folha(t);
158
158
  const strings = useAureaStrings();
159
159
  const Corpo = scroll ? ScrollView : View;
160
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: [s.fundo, s.centro], children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(View, { ...naoAtravessa, style: [s.superficie, style], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children }), footer ? _jsx(View, { style: s.rodape, children: footer }) : null] })] }) }));
160
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: [s.fundo, s.centro], children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(View, { ...naoAtravessa, style: [s.superficie, style], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children }), footer ? _jsx(View, { style: s.rodape, children: footer }) : null] })] }) }) }));
161
161
  }
162
162
  /**
163
163
  * A decisão que não se desfaz.
@@ -185,9 +185,9 @@ export function ConfirmDialog({ open, title, description, confirmLabel, cancelLa
185
185
  const t = useAureaTokens();
186
186
  const s = folha(t);
187
187
  const strings = useAureaStrings();
188
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onCancel, testID: testID, children: _jsx(View, { style: [s.fundo, s.centro], children: _jsxs(View, { ...naoAtravessa, style: [s.superficie, s.confirmar], children: [_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) }), _jsx(View, { style: s.corpo, children: typeof description === "string"
189
- ? _jsx(Text, { size: "md", tone: "muted", children: description })
190
- : description }), _jsxs(View, { style: s.rodape, children: [_jsx(Button, { appearance: "outline", onPress: onCancel, testID: testID ? `${testID}-cancelar` : undefined, children: cancelLabel ?? strings.confirmCancel }), _jsx(Button, { appearance: "solid", tone: destructive ? "danger" : "brand", onPress: onConfirm, testID: testID ? `${testID}-confirmar` : undefined, children: confirmLabel ?? strings.confirmProceed })] })] }) }) }));
188
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onCancel, testID: testID, children: _jsx(View, { style: [s.fundo, s.centro], children: _jsxs(View, { ...naoAtravessa, style: [s.superficie, s.confirmar], children: [_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) }), _jsx(View, { style: s.corpo, children: typeof description === "string"
189
+ ? _jsx(Text, { size: "md", tone: "muted", children: description })
190
+ : description }), _jsxs(View, { style: s.rodape, children: [_jsx(Button, { appearance: "outline", onPress: onCancel, testID: testID ? `${testID}-cancelar` : undefined, children: cancelLabel ?? strings.confirmCancel }), _jsx(Button, { appearance: "solid", tone: destructive ? "danger" : "brand", onPress: onConfirm, testID: testID ? `${testID}-confirmar` : undefined, children: confirmLabel ?? strings.confirmProceed })] })] }) }) }) }));
191
191
  }
192
192
  /**
193
193
  * O painel que entra pela borda.
@@ -229,10 +229,10 @@ export function Drawer({ open, title, children, onClose, side = "right", scroll
229
229
  inputRange: [0, 1],
230
230
  outputRange: [side === "right" ? 480 : -480, 0],
231
231
  });
232
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "none", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, style: [
233
- s.gaveta, side === "right" ? s.gavetaDireita : s.gavetaEsquerda,
234
- { transform: [{ translateX: desloca }] }, style,
235
- ], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }));
232
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: "none", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, style: [
233
+ s.gaveta, side === "right" ? s.gavetaDireita : s.gavetaEsquerda,
234
+ { transform: [{ translateX: desloca }] }, style,
235
+ ], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }) }));
236
236
  }
237
237
  /**
238
238
  * A folha que sobe de baixo. **NÃO existe na web** — é o único componente deste lote sem ficha.
@@ -287,5 +287,5 @@ export function BottomSheet({ open, title, children, onClose, grabber = true, dr
287
287
  }).start();
288
288
  },
289
289
  }), [draggable, onClose, arrasto, reduzir, t.easing.easeEmphasized]);
290
- return (_jsx(Modal, { visible: open, transparent: true, animationType: reduzir !== false ? "none" : "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, ...gestos.panHandlers, onLayout: (e) => { altura.current = e.nativeEvent.layout.height; }, style: [s.folhaBaixo, { transform: [{ translateY: arrasto }] }, style], children: [grabber ? (_jsx(View, { style: s.puxadorArea, accessible: false, importantForAccessibility: "no-hide-descendants", children: _jsx(View, { style: s.puxador }) })) : null, title ? (_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) })) : null, _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }));
290
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: reduzir !== false ? "none" : "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, ...gestos.panHandlers, onLayout: (e) => { altura.current = e.nativeEvent.layout.height; }, style: [s.folhaBaixo, { transform: [{ translateY: arrasto }] }, style], children: [grabber ? (_jsx(View, { style: s.puxadorArea, accessible: false, importantForAccessibility: "no-hide-descendants", children: _jsx(View, { style: s.puxador }) })) : null, title ? (_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) })) : null, _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }) }));
291
291
  }
package/dist/screen.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { RefreshControl, ScrollView } from "react-native";
3
3
  import { SafeAreaView } from "react-native-safe-area-context";
4
+ import { useBottomNavSpace } from "./barranav.js";
4
5
  import { criarFolha } from "./estilos.js";
5
6
  import { useAureaTokens } from "./theme.js";
6
7
  const folha = criarFolha((t) => ({
@@ -43,7 +44,18 @@ export function Screen({ edges = TODAS_AS_BORDAS, padded = true, scroll = false,
43
44
  const t = useAureaTokens();
44
45
  const s = folha(t);
45
46
  const fundo = s[`fundo_${background}`];
46
- return (_jsx(SafeAreaView, { style: [s.raiz, fundo, !scroll && padded && s.respiro, style], edges: edges, ...rest, children: scroll ? (_jsx(ScrollView, { style: s.raiz, contentContainerStyle: [s.conteudo, padded && s.respiro],
47
+ // 🔴 O ESPAÇO DA BARRA DE ABAS, quando ela flutua por cima desta tela — ver `barranav.tsx`.
48
+ // **Zero** fora de um `BottomNavProvider`, que é o caso de toda tela cheia: sem abas, nada muda.
49
+ const respiroDaBarra = useBottomNavSpace();
50
+ return (_jsx(SafeAreaView, { style: [s.raiz, fundo, !scroll && padded && s.respiro, style], edges: edges, ...rest, children: scroll ? (_jsx(ScrollView, { style: s.raiz,
51
+ // ⚠ O respiro da barra SOMA ao do tema em vez de substituí-lo: escrever
52
+ // `paddingBottom` depois de `s.respiro` apagaria os 16 do tema e colaria o último
53
+ // item na pílula. A conta é explícita porque a folha não soma sozinha.
54
+ contentContainerStyle: [
55
+ s.conteudo, padded && s.respiro,
56
+ respiroDaBarra > 0
57
+ && { paddingBottom: (padded ? t.size.space4 : 0) + respiroDaBarra },
58
+ ],
47
59
  // A rosca do sistema é a única aparência da plataforma que a Aurea aceita aqui, e a
48
60
  // razão é que ela NÃO É desenho nosso: é o gesto do sistema operacional, com a física
49
61
  // que a pessoa já conhece. O que dá para pintar, pintamos — a cor é do tema.
package/dist/text.js CHANGED
@@ -23,6 +23,36 @@ import * as React from "react";
23
23
  import { Text as TextRN } from "react-native";
24
24
  import { criarFolha } from "./estilos.js";
25
25
  import { useAureaTokens, useSobreAMarca } from "./theme.js";
26
+ // 🔴 A ESCALA TEM CINCO DEGRAUS QUE SE ENXERGAM, E DEZ NOMES — ordem do Victor, 19/09/2026,
27
+ // olhando o app no aparelho: *"o tamanho das fontes: existem variações demais, fica muito
28
+ // estranho"*.
29
+ //
30
+ // Medido no token antes de mexer: **12 · 13 · 14 · 16** eram quatro degraus em quatro pontos.
31
+ // Lado a lado, isso não lê como hierarquia — lê como erro. Uma tela só (a de Início) mostrava de
32
+ // 12 a 36 em SETE tamanhos.
33
+ //
34
+ // A escala nova sobe 25% por degrau, que é o mínimo para dois vizinhos serem distinguíveis a olho
35
+ // nu, e **nada encolhe** — o que importa, porque o Victor tem baixa visão:
36
+ //
37
+ // legenda, apoio .............. 13 (era 12)
38
+ // texto corrido, controle ..... 16 (era 13 e 14)
39
+ // nome de cartão .............. 20 (era 16 e 18)
40
+ // título de tela .............. 25 (era 20 e 24)
41
+ // número em destaque .......... 31 (era 30)
42
+ //
43
+ // ⚠ **Os DEZ nomes continuam existindo, e alguns apontam para o mesmo número** — `sm`, `md` e
44
+ // `base` são 16; `lg` e `xl` são 20. Isso é de propósito: `size` é API pública dos dois alvos, e
45
+ // apagar nome quebraria consumidor sem ganhar nada. O que encolheu foi a lista de VALORES, que é
46
+ // o que a pessoa vê. Por isso nenhum componente do pacote precisou trocar de degrau: os degraus é
47
+ // que se juntaram.
48
+ //
49
+ // ⚠ **O topo (36 e 48) não foi tocado.** A queixa é do pé da escala, e ali as razões já eram boas
50
+ // (1,2 e 1,33). Mexer sem queixa é inventar trabalho.
51
+ //
52
+ // ⚠ **O limite, medido e declarado:** com a letra do sistema em 1,3× o corpo de 16 vira ~21 e a
53
+ // linha ~29. Isso cabe em toda cápsula, MENOS o controle `sm` na densidade compacta (28 de
54
+ // altura), que estoura por 1 ponto. Em 1,5× vários estouram — e isso já era verdade na escala
55
+ // antiga.
26
56
  const TAMANHO = {
27
57
  xs: "textXs", sm: "textSm", md: "textMd", base: "textBase", lg: "textLg",
28
58
  xl: "textXl", "2xl": "text2xl", "3xl": "text3xl", "4xl": "text4xl", "5xl": "text5xl",
@@ -88,5 +118,5 @@ export function Text({ size = "md", weight = 400, font = "ui", tone = "default",
88
118
  // ⚠ **A consequência está declarada e não é neutra:** dentro de um cartão da marca não existe
89
119
  // texto esmaecido. A hierarquia sai de PESO e TAMANHO.
90
120
  // ⚠ E `style` continua por último de propósito: quem passa cor à mão assume a conta.
91
- return _jsx(TextRN, { style: [s[tone], sobreAMarca && { color: sobreAMarca }, proprio, style], ...rest });
121
+ return _jsx(TextRN, { style: [s[tone], sobreAMarca && { color: sobreAMarca.tinta }, proprio, style], ...rest });
92
122
  }
package/dist/theme.d.ts CHANGED
@@ -73,13 +73,66 @@ export declare function useAureaTokens(): AureaTokens;
73
73
  * `strings.ts`.
74
74
  */
75
75
  export declare function useAureaStrings(): AureaStrings;
76
+ export interface SobreAMarcaValor {
77
+ /** A única cor que se pode desenhar sobre a marca: letra, contorno e glifo. 4,54 nos dois temas. */
78
+ tinta: string;
79
+ /** A cor do cartão por baixo. Serve de letra para quem se pinta com a tinta INTEIRA. */
80
+ fundo: string;
81
+ }
76
82
  /** O provedor. Quem usa é o `Card variant="brand"`. */
77
- export declare const SobreAMarca: React.Context<string | null>;
83
+ export declare const SobreAMarca: React.Context<SobreAMarcaValor | null>;
78
84
  /**
79
- * A cor que o texto tem de usar quando está sobre a marca, ou `null` quando não está.
85
+ * O par de cores obrigatório quando se está sobre a marca, ou `null` quando não se está.
80
86
  *
81
- * ⚠ **Ela vence TODOS os tons, inclusive os explícitos.** Um `tone="danger"` sobre o amarelo
87
+ * ⚠ **Ele vence TODOS os tons, inclusive os explícitos.** Um `tone="danger"` sobre o amarelo
82
88
  * mediria menos que os 4,5 da norma, então respeitá-lo entregaria texto ilegível em nome da
83
- * obediência. **Sobre o amarelo existe UMA cor de texto** — a hierarquia sai de peso e tamanho.
89
+ * obediência. **Sobre o amarelo existe UMA tinta** — a hierarquia sai de peso e tamanho.
90
+ *
91
+ * ⚠ **E o alcance dele é DECLARADO, não completo.** Leem o contexto: `Text`, a família de campo,
92
+ * o `Button` e o `IconButton`. **Componente fora dessa lista continua quebrado dentro do cartão
93
+ * amarelo**, e gate estático nenhum vê isso — o que o consumidor encaixa não está no nosso
94
+ * código, exatamente o ponto cego que o `check 43` já declara.
95
+ */
96
+ export declare function useSobreAMarca(): SobreAMarcaValor | null;
97
+ /**
98
+ * Corta a marca para dentro.
99
+ *
100
+ * 🔴 **Existe por uma armadilha do React Native que é invisível na tela e óbvia no código:** o
101
+ * `Modal` desenha numa camada POR CIMA de tudo, mas em React ele continua sendo FILHO de quem o
102
+ * escreveu. Um `Select` dentro de um `Card variant="brand"` abre a folha dele **fora** do cartão
103
+ * amarelo — e a folha herdaria a tinta do amarelo mesmo assim, pintando texto marrom sobre fundo
104
+ * normal.
105
+ *
106
+ * ⚠ **Camada visual e árvore de contexto são coisas diferentes**, e é só o segundo que manda na
107
+ * cor. Toda superfície que sai do fluxo — as duas folhas de escolha e os quatro sobrepostos —
108
+ * passa por aqui.
109
+ */
110
+ export declare function ForaDaMarca({ children }: {
111
+ children?: React.ReactNode;
112
+ }): React.JSX.Element;
113
+ /**
114
+ * A pele que um CAMPO tem de vestir quando está dentro de um `Card variant="brand"`, ou `null`
115
+ * quando não está.
116
+ *
117
+ * O campo perde o fundo e passa a ser um contorno da tinta: **4,54 contra o amarelo nos dois
118
+ * temas**, contra os 2,43/2,24 do `borderStrong` e os 1,61 do `fieldBg` no tema claro. A norma
119
+ * 1.4.11 pede 3:1 para o contorno que identifica um campo, e só a tinta chega lá.
120
+ *
121
+ * ⚠ **O fundo some porque NÃO HÁ fundo possível** — medidas as três superfícies neutras do
122
+ * sistema contra o amarelo no tema claro: `card` 1,91 · `background` 1,73 · `secondary` 1,61.
123
+ * Nenhuma separa. Um contorno forte separa.
124
+ *
125
+ * 🔴 **E a tinta vence o estado de INVÁLIDO, que é o contrário do que a intuição manda.** Medido:
126
+ * a borda de inválido (`danger-400`, `#9f2330`) mede **1,86** contra o amarelo — ela é MENOS
127
+ * visível que a tinta. Pintar de vermelho ali deixaria o campo errado mais difícil de achar que o
128
+ * campo certo. Dentro deste cartão o erro é dito pela MENSAGEM do `Field` e pelo estado que o
129
+ * leitor de tela anuncia, não pela cor — que é o que a norma 1.4.1 já exige de qualquer forma.
130
+ *
131
+ * ⚠ **O texto digitado e o marcador de dica ficam da MESMA cor**, porque só existe uma tinta. É
132
+ * perda real de hierarquia, declarada e não escondida: sobre o amarelo não há segunda cor.
84
133
  */
85
- export declare function useSobreAMarca(): string | null;
134
+ export declare function usePeleSobreAMarca(): {
135
+ borderColor: string;
136
+ backgroundColor: string;
137
+ color: string;
138
+ } | null;