@aurea-uds/native 0.8.7 → 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.
@@ -0,0 +1,52 @@
1
+ import * as React from "react";
2
+ /**
3
+ * Põe em volta do navegador de abas. O `BottomNav` mede a própria altura e conta para ele; o
4
+ * `Screen` com `scroll` lê e reserva o espaço no fim do conteúdo, sozinho.
5
+ *
6
+ * ```tsx
7
+ * <BottomNavProvider>
8
+ * <Tabs
9
+ * screenOptions={{headerShown: false}}
10
+ * tabBar={({state, navigation}) => (
11
+ * <BottomNav
12
+ * label="Menu principal"
13
+ * current={state.routes[state.index].name}
14
+ * items={rotas.map((r) => ({
15
+ * id: r.name, label: r.titulo, icon: r.icone,
16
+ * onPress: () => navigation.navigate(r.name),
17
+ * }))} />
18
+ * )} />
19
+ * </BottomNavProvider>
20
+ * ```
21
+ *
22
+ * ⚠ **O `BottomNav` vai SOLTO no `tabBar`, sem `View` em volta.** Um embrulho com fundo pinta uma
23
+ * faixa atrás da pílula e acaba com a flutuação — era o que a documentação antiga mandava fazer,
24
+ * e era o defeito.
25
+ *
26
+ * ⚠ **Fora daqui, `useBottomNavSpace()` devolve 0** e o `Screen` não reserva nada. É o que faz
27
+ * uma tela cheia, sem abas, continuar exatamente como era.
28
+ */
29
+ export declare function BottomNavProvider({ children }: {
30
+ children?: React.ReactNode;
31
+ }): React.JSX.Element;
32
+ /**
33
+ * Quanto espaço a barra de abas ocupa no pé da tela, em pontos — pílula, margens e folga do
34
+ * sistema somadas. **Zero** quando não há `BottomNavProvider` em volta, ou quando a barra é
35
+ * `edge` (que fica no fluxo e já encurta a tela sozinha).
36
+ *
37
+ * O `Screen` com `scroll` já usa isto sozinho. Este gancho é para o resto: uma fileira de botões
38
+ * presa no pé da tela, por exemplo, que precisa subir acima da pílula.
39
+ *
40
+ * ```tsx
41
+ * const respiro = useBottomNavSpace();
42
+ * <View style={{position: "absolute", left: 0, right: 0, bottom: respiro}}>
43
+ * <Button fullWidth>Salvar</Button>
44
+ * </View>
45
+ * ```
46
+ */
47
+ export declare function useBottomNavSpace(): number;
48
+ /**
49
+ * Interno: como o `BottomNav` conta a própria altura. Devolve `null` fora de um provedor, e nesse
50
+ * caso a barra não mede nada — ninguém está ouvindo.
51
+ */
52
+ export declare function useAnotarBottomNav(): ((n: number) => void) | null;
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // Aurea nativo — quanto espaço a barra de abas ocupa, e quem precisa saber.
3
+ //
4
+ // 🔴 **ESTE MÓDULO EXISTE POR UM DEFEITO MEDIDO EM APARELHO, 19/09/2026**, e não por arquitetura.
5
+ // A variante `floating` do `BottomNav` passou a flutuar POR CIMA da tela — que é o que o nome
6
+ // dela sempre prometeu e o que a web já fazia (`.bottom-nav` é `position: sticky; bottom: 0`,
7
+ // `aurea.css:270`). Flutuar por cima tem um preço: **o fim da rolagem fica escondido atrás da
8
+ // pílula.** Alguém tem de reservar esse espaço, e a conta não pode cair no app.
9
+ //
10
+ // ⚠ **Por que a conta não pode ser do app:** ela é a altura da pílula (que depende da densidade,
11
+ // do tamanho da letra do sistema e de haver rótulo) mais as duas margens mais a folga do sistema
12
+ // — que muda entre um aparelho com três botões (≈48) e um com gesto (≈16). Deixar isso para o
13
+ // consumidor é pedir um número mágico que fica errado no primeiro aparelho diferente.
14
+ //
15
+ // ── POR QUE ELE MORA NUM ARQUIVO SÓ DELE ─────────────────────────────────────────────────────
16
+ // Quem MEDE é o `BottomNav` (`navigation.tsx`); quem LÊ é o `Screen` (`screen.tsx`). Os dois não
17
+ // se importam hoje, e pôr o contexto em qualquer um dos dois criaria um ciclo. É a mesma razão
18
+ // que fez a tinta da marca morar no `theme.tsx` — com a diferença de que ali os dois já
19
+ // importavam o `theme`, e aqui não há módulo comum. Então: um arquivo, sem dependência nenhuma
20
+ // além do React.
21
+ import * as React from "react";
22
+ const BarraCtx = React.createContext(null);
23
+ /**
24
+ * Põe em volta do navegador de abas. O `BottomNav` mede a própria altura e conta para ele; o
25
+ * `Screen` com `scroll` lê e reserva o espaço no fim do conteúdo, sozinho.
26
+ *
27
+ * ```tsx
28
+ * <BottomNavProvider>
29
+ * <Tabs
30
+ * screenOptions={{headerShown: false}}
31
+ * tabBar={({state, navigation}) => (
32
+ * <BottomNav
33
+ * label="Menu principal"
34
+ * current={state.routes[state.index].name}
35
+ * items={rotas.map((r) => ({
36
+ * id: r.name, label: r.titulo, icon: r.icone,
37
+ * onPress: () => navigation.navigate(r.name),
38
+ * }))} />
39
+ * )} />
40
+ * </BottomNavProvider>
41
+ * ```
42
+ *
43
+ * ⚠ **O `BottomNav` vai SOLTO no `tabBar`, sem `View` em volta.** Um embrulho com fundo pinta uma
44
+ * faixa atrás da pílula e acaba com a flutuação — era o que a documentação antiga mandava fazer,
45
+ * e era o defeito.
46
+ *
47
+ * ⚠ **Fora daqui, `useBottomNavSpace()` devolve 0** e o `Screen` não reserva nada. É o que faz
48
+ * uma tela cheia, sem abas, continuar exatamente como era.
49
+ */
50
+ export function BottomNavProvider({ children }) {
51
+ const [espaco, setEspaco] = React.useState(0);
52
+ // `setEspaco` tem identidade estável, então isto só muda quando o número muda — e o número só
53
+ // muda quando a barra muda de tamanho de verdade (rotação, letra do sistema, troca de variante).
54
+ const valor = React.useMemo(() => ({ espaco, anotar: setEspaco }), [espaco]);
55
+ return _jsx(BarraCtx.Provider, { value: valor, children: children });
56
+ }
57
+ /**
58
+ * Quanto espaço a barra de abas ocupa no pé da tela, em pontos — pílula, margens e folga do
59
+ * sistema somadas. **Zero** quando não há `BottomNavProvider` em volta, ou quando a barra é
60
+ * `edge` (que fica no fluxo e já encurta a tela sozinha).
61
+ *
62
+ * O `Screen` com `scroll` já usa isto sozinho. Este gancho é para o resto: uma fileira de botões
63
+ * presa no pé da tela, por exemplo, que precisa subir acima da pílula.
64
+ *
65
+ * ```tsx
66
+ * const respiro = useBottomNavSpace();
67
+ * <View style={{position: "absolute", left: 0, right: 0, bottom: respiro}}>
68
+ * <Button fullWidth>Salvar</Button>
69
+ * </View>
70
+ * ```
71
+ */
72
+ export function useBottomNavSpace() {
73
+ return React.useContext(BarraCtx)?.espaco ?? 0;
74
+ }
75
+ /**
76
+ * Interno: como o `BottomNav` conta a própria altura. Devolve `null` fora de um provedor, e nesse
77
+ * caso a barra não mede nada — ninguém está ouvindo.
78
+ */
79
+ export function useAnotarBottomNav() {
80
+ return React.useContext(BarraCtx)?.anotar ?? null;
81
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { AureaProvider, useAureaTheme, useAureaTokens } from "./theme.js";
2
2
  export type { AureaProviderProps, AureaThemeControl } from "./theme.js";
3
+ export { BottomNavProvider, useBottomNavSpace } from "./barranav.js";
3
4
  export { resolverTokens } from "./tokens.js";
4
5
  export type { AureaDensity, AureaFontFamilies, AureaFontInput, AureaFontScale, AureaShadow, AureaThemeName, AureaTokens, } from "./tokens.js";
5
6
  export { criarFolha } from "./estilos.js";
package/dist/index.js CHANGED
@@ -24,6 +24,9 @@
24
24
  // e o caminho profundo é a forma documentada (ADR-0038, cláusula 1) — importar daqui traria os
25
25
  // 2571 ao grafo do bundler.
26
26
  export { AureaProvider, useAureaTheme, useAureaTokens } from "./theme.js";
27
+ // A barra de abas que flutua precisa contar a própria altura para a tela reservar o fim da
28
+ // rolagem. Ver `barranav.tsx` — e o `BottomNav`, que é quem mede.
29
+ export { BottomNavProvider, useBottomNavSpace } from "./barranav.js";
27
30
  export { resolverTokens } from "./tokens.js";
28
31
  // A fábrica de folhas memoizada por (tema, densidade). Pública porque o consumidor tem o mesmo
29
32
  // problema que os componentes daqui — e a medição em aparelho mostrou que ele é real.
package/dist/inputs.d.ts CHANGED
@@ -150,6 +150,14 @@ interface ControleProps {
150
150
  size?: AureaFieldSize;
151
151
  style?: StyleProp<ViewStyle>;
152
152
  testID?: string;
153
+ /**
154
+ * O nome para quem usa leitor de tela, quando ele NÃO deve aparecer escrito no controle.
155
+ *
156
+ * É o caso de uma linha de `NavList`: o nome já está na linha, então repeti-lo no `label` o
157
+ * escreveria duas vezes — e omitir os dois deixaria o controle **MUDO** para o leitor de tela.
158
+ * Ele vence o `label` quando os dois vêm.
159
+ */
160
+ accessibilityLabel?: string;
153
161
  }
154
162
  export interface InputGroupProps extends ViewProps {
155
163
  /** O degrau do sistema. Cai no `Field` em volta quando não vem, como no `Input`. */
@@ -256,7 +264,7 @@ export interface SwitchProps extends ControleProps {
256
264
  * ⚠ **E ele respeita "menos movimento"**: com a preferência ligada, o polegar salta em vez de
257
265
  * deslizar. O estado continua o mesmo — o que some é a animação, não a informação.
258
266
  */
259
- export declare function Switch({ label, description, checked, onChange, disabled, size, style, testID, }: SwitchProps): React.JSX.Element;
267
+ export declare function Switch({ label, description, checked, onChange, disabled, size, style, testID, accessibilityLabel, }: SwitchProps): React.JSX.Element;
260
268
  export interface SegmentedControlProps extends ViewProps {
261
269
  items: Array<{
262
270
  value: string;
package/dist/inputs.js CHANGED
@@ -327,16 +327,23 @@ export function PasswordField({ defaultVisible = false, size, disabled, autoCapi
327
327
  const inativo = disabled ?? campo?.disabled;
328
328
  return (_jsxs(InputGroup, { size: tam, disabled: inativo, testID: testID, children: [leading ? _jsx(InputGroupAddon, { children: leading }) : null, _jsx(Input, { ...rest, size: tam, disabled: inativo, autoCapitalize: autoCapitalize, secureTextEntry: !visivel, style: style, testID: testID ? `${testID}-campo` : undefined }), _jsx(InputGroupAddon, { children: _jsx(IconButton, { name: visivel ? "view--off" : "view", label: visivel ? strings.passwordHide : strings.passwordShow, appearance: "ghost", size: tam === "lg" ? "md" : "sm", disabled: inativo, icons: icons, onPress: () => setVisivel(v => !v), testID: testID ? `${testID}-olho` : undefined }) })] }));
329
329
  }
330
- function ControleMarcado({ papel, label, description, checked, onChange, disabled, size, style, testID, }) {
330
+ function ControleMarcado({ papel, label, description, checked, onChange, disabled, size, style, testID, accessibilityLabel, }) {
331
331
  const t = useAureaTokens();
332
332
  const s = folha(t);
333
333
  const campo = useCampo();
334
334
  const tam = size ?? campo?.size ?? "md";
335
335
  const inativo = disabled ?? campo?.disabled;
336
+ // Mesma conta do `Switch`, pela mesma razão: sem texto, a coluna vazia com `flex: 1` tomaria a
337
+ // largura que sobra dentro de uma fileira e apagaria o que estivesse ao lado.
338
+ const temTexto = label != null || description != null;
336
339
  // A marca é METADE da altura do controle — `aurea.css:871`, e é o que faz ela crescer junto
337
340
  // com a densidade sem token próprio.
338
341
  const lado = alturaDoTamanho(t, tam) / 2;
339
- return (_jsxs(Pressable, { testID: testID, onPress: inativo ? undefined : () => onChange?.(!checked), disabled: inativo, accessibilityRole: papel, accessibilityState: { checked: !!checked, disabled: !!inativo }, accessibilityLabel: typeof label === "string" ? label : campo?.label, accessibilityHint: typeof description === "string" ? description : campo?.hint, style: [s.linhaDeControle, inativo && s.desabilitado, style], children: [_jsx(View, { style: [
342
+ return (_jsxs(Pressable, { testID: testID, onPress: inativo ? undefined : () => onChange?.(!checked), disabled: inativo, accessibilityRole: papel, accessibilityState: { checked: !!checked, disabled: !!inativo },
343
+ // ⚠ O `accessibilityLabel` de fora vence o `label` escrito, e existe para o caso em que o
344
+ // nome JÁ está na tela ao lado — uma linha de `NavList`, por exemplo. Sem ele, ou o nome
345
+ // aparece escrito duas vezes, ou o controle sobe MUDO para quem usa leitor de tela.
346
+ accessibilityLabel: accessibilityLabel ?? (typeof label === "string" ? label : campo?.label), accessibilityHint: typeof description === "string" ? description : campo?.hint, style: [s.linhaDeControle, inativo && s.desabilitado, style], children: [_jsx(View, { style: [
340
347
  s.marcaBase,
341
348
  { width: lado, height: lado,
342
349
  borderRadius: papel === "radio" ? t.size.radiusFull : 5 },
@@ -355,8 +362,8 @@ function ControleMarcado({ papel, label, description, checked, onChange, disable
355
362
  borderLeftWidth: 2, borderBottomWidth: 2,
356
363
  borderColor: t.color.controlSelectedForeground,
357
364
  transform: [{ rotate: "-45deg" }],
358
- } })) }), _jsxs(View, { style: { flex: 1 }, children: [typeof label === "string" ? _jsx(Text, { size: "sm", children: label }) : label, description != null && (typeof description === "string"
359
- ? _jsx(Text, { size: "xs", tone: "muted", children: description }) : description)] })] }));
365
+ } })) }), temTexto && (_jsxs(View, { style: { flex: 1 }, children: [typeof label === "string" ? _jsx(Text, { size: "sm", children: label }) : label, description != null && (typeof description === "string"
366
+ ? _jsx(Text, { size: "xs", tone: "muted", children: description }) : description)] }))] }));
360
367
  }
361
368
  /** A caixa que marca. `accessibilityRole="checkbox"` + `state.checked`. */
362
369
  export function Checkbox(p) { return _jsx(ControleMarcado, { ...p, papel: "checkbox" }); }
@@ -378,13 +385,14 @@ export function Radio(p) { return _jsx(ControleMarcado, { ...p, papel: "radio" }
378
385
  * ⚠ **E ele respeita "menos movimento"**: com a preferência ligada, o polegar salta em vez de
379
386
  * deslizar. O estado continua o mesmo — o que some é a animação, não a informação.
380
387
  */
381
- export function Switch({ label, description, checked, onChange, disabled, size, style, testID, }) {
388
+ export function Switch({ label, description, checked, onChange, disabled, size, style, testID, accessibilityLabel, }) {
382
389
  const t = useAureaTokens();
383
390
  const s = folha(t);
384
391
  const campo = useCampo();
385
392
  const tam = size ?? campo?.size ?? "md";
386
393
  const inativo = disabled ?? campo?.disabled;
387
394
  const reduzir = useReduceMotion();
395
+ const temTexto = label != null || description != null;
388
396
  const h = alturaDoTamanho(t, tam);
389
397
  const larguraDoTrilho = h * 7 / 6;
390
398
  const alturaDoTrilho = h * 2 / 3;
@@ -403,10 +411,28 @@ export function Switch({ label, description, checked, onChange, disabled, size,
403
411
  a.start();
404
412
  return () => a.stop();
405
413
  }, [checked, pos, reduzir]);
406
- return (_jsxs(Pressable, { testID: testID, onPress: inativo ? undefined : () => onChange?.(!checked), disabled: inativo, accessibilityRole: "switch", accessibilityState: { checked: !!checked, disabled: !!inativo }, accessibilityLabel: typeof label === "string" ? label : campo?.label,
414
+ return (_jsxs(Pressable, { testID: testID, onPress: inativo ? undefined : () => onChange?.(!checked), disabled: inativo, accessibilityRole: "switch", accessibilityState: { checked: !!checked, disabled: !!inativo },
415
+ // O `accessibilityLabel` de fora vence o `label` escrito — ver a prop, em `ControleProps`.
416
+ accessibilityLabel: accessibilityLabel ?? (typeof label === "string" ? label : campo?.label),
407
417
  // Mesma regra do `ControleMarcado`: a descrição vira DICA quando é texto. Sem isto ela
408
418
  // desenha na tela e não existe para quem usa leitor de tela.
409
- accessibilityHint: typeof description === "string" ? description : campo?.hint, style: [{ flexDirection: "row", alignItems: "center", gap: 10 }, inativo && s.desabilitado, style], children: [_jsx(View, { style: [
419
+ accessibilityHint: typeof description === "string" ? description : campo?.hint,
420
+ // 🔴 SEM RÓTULO E SEM DESCRIÇÃO, A RAIZ É O TRILHO E MAIS NADA — e isto é conserto de um
421
+ // defeito que só aparece no ANDROID, achado pelo consumidor em 19/09/2026.
422
+ //
423
+ // A coluna de texto abaixo tem `flex: 1` e existia mesmo vazia. Dentro de uma fileira — uma
424
+ // linha de `NavList`, onde o valor-componente é IRMÃO do miolo (`navigation.tsx:248`) — esse
425
+ // `flex: 1` faz a raiz do `Switch` tomar toda a largura que sobra. O miolo, que também tem
426
+ // `flex: 1`, encolhe até restar só o ícone, e o NOME DA LINHA SOME.
427
+ //
428
+ // ⚠ **No navegador o defeito não aparece**, porque lá a divisão de espaço entre dois
429
+ // irmãos com `flex:1` parte da largura do conteúdo. É a mesma família do `padding-block` do
430
+ // `Input`: o que a cascata resolve de graça, aqui é conta explícita.
431
+ style: [
432
+ { flexDirection: "row", alignItems: "center" },
433
+ temTexto && { gap: 10 },
434
+ inativo && s.desabilitado, style,
435
+ ], children: [_jsx(View, { style: [
410
436
  s.trilho,
411
437
  { width: larguraDoTrilho, height: alturaDoTrilho },
412
438
  checked && { backgroundColor: t.color.controlSelected, borderColor: t.color.controlSelected },
@@ -415,8 +441,8 @@ export function Switch({ label, description, checked, onChange, disabled, size,
415
441
  { width: ladoDoPolegar, height: ladoDoPolegar,
416
442
  backgroundColor: checked ? t.color.controlSelectedForeground : t.color.mutedForeground,
417
443
  transform: [{ translateX: pos.interpolate({ inputRange: [0, 1], outputRange: [0, curso] }) }] },
418
- ] }) }), _jsxs(View, { style: { flex: 1 }, children: [typeof label === "string" ? _jsx(Text, { size: "sm", children: label }) : label, description != null && (typeof description === "string"
419
- ? _jsx(Text, { size: "xs", tone: "muted", children: description }) : description)] })] }));
444
+ ] }) }), temTexto && (_jsxs(View, { style: { flex: 1 }, children: [typeof label === "string" ? _jsx(Text, { size: "sm", children: label }) : label, description != null && (typeof description === "string"
445
+ ? _jsx(Text, { size: "xs", tone: "muted", children: description }) : description)] }))] }));
420
446
  }
421
447
  /**
422
448
  * Um de poucos, lado a lado.
package/dist/layout.js CHANGED
@@ -87,7 +87,29 @@ export function Cluster({ style, ...rest }) {
87
87
  export function Grid({ minColumnWidth = 240, style, children, ...rest }) {
88
88
  const s = folha(useAureaTokens());
89
89
  const celula = React.useMemo(() => ({ flexGrow: 0, flexShrink: 1, flexBasis: minColumnWidth, minWidth: minColumnWidth, maxWidth: "100%" }), [minColumnWidth]);
90
- return (_jsx(View, { style: [s.grid, style], ...rest, children: React.Children.map(children, (filho) => React.isValidElement(filho) ? _jsx(View, { style: celula, children: filho }) : filho) }));
90
+ return (_jsx(View, { style: [s.grid, style], ...rest, children: React.Children.map(children, (filho) => {
91
+ if (!React.isValidElement(filho))
92
+ return filho;
93
+ // 🔴 O `flexGrow: 1` É O ITEM C14, e ele existe porque a célula NÃO basta.
94
+ //
95
+ // A célula já tem a altura certa: a grade é `flexWrap: "wrap"`, e o padrão do Yoga é
96
+ // `alignItems: Stretch` — medido no fonte do motor, `Style.h:907` —, então cada célula
97
+ // estica até a mais alta da LINHA. O que não acontecia é o filho preencher a célula: numa
98
+ // coluna, um filho sem `flexGrow` mede o próprio conteúdo, e dois `KPI` lado a lado, um
99
+ // com duas linhas de texto e outro com uma, saíam de alturas diferentes.
100
+ //
101
+ // ⚠ **Na web isto é de graça e por isso não tinha sido notado:** o `.grid`
102
+ // (`aurea.css:54`) é `display: grid`, e ali o cartão é o PRÓPRIO item da grade —
103
+ // `align-items` vale `stretch` por padrão e ele preenche a linha. Aqui existe uma célula
104
+ // no meio, e é ela que corta a herança de altura.
105
+ //
106
+ // ⚠ **O estilo vai ANTES do que o filho já tinha**, para um `style` do consumidor
107
+ // continuar vencendo. E um filho que não aceite `style` simplesmente não estica — fica
108
+ // como estava, nunca pior.
109
+ const proprio = filho.props.style;
110
+ const estica = React.cloneElement(filho, { style: [{ flexGrow: 1 }, proprio] });
111
+ return _jsx(View, { style: celula, children: estica });
112
+ }) }));
91
113
  }
92
114
  /**
93
115
  * A linha de divisão do sistema. Mesma cor e mesma espessura do `.separator` da web.
@@ -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
@@ -231,6 +231,17 @@ 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();
@@ -277,8 +288,41 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
277
288
  emitir(null);
278
289
  return;
279
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.
280
294
  emitir(prender(lido, min, max));
281
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]);
282
326
  const empurrar = React.useCallback((direcao) => {
283
327
  // Se o campo está sendo editado, o que vale é o que está escrito — empurrar por cima do valor
284
328
  // antigo descartaria a digitação em silêncio.
@@ -311,7 +355,7 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
311
355
  // (`aurea.css:685`), com os dois botões FORA dela. O estado que faltava é o deles; a geometria
312
356
  // continua sendo a nossa.
313
357
  const [focado, setFocado] = React.useState(false);
314
- 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: peleDaMarca?.color ?? 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,
315
359
  // O nome vem do `Field`, como em todo controle deste pacote — e `label` cobre quem usa o
316
360
  // campo solto. Sem um dos dois, é um nó anônimo para o leitor de tela.
317
361
  accessibilityLabel: label ?? campo?.label, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, "aria-invalid": campo?.invalido, style: [
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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurea-uds/native",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "React Native target for Aurea UDS: the theme x density provider, the first components and the Carbon icons, drawn on react-native-svg.",
5
5
  "keywords": [
6
6
  "aurea",
@@ -47,7 +47,7 @@
47
47
  "NOTICE"
48
48
  ],
49
49
  "dependencies": {
50
- "@aurea-uds/tokens": "^0.8.7"
50
+ "@aurea-uds/tokens": "^0.8.8"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@react-native-community/datetimepicker": ">=8",