@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.
package/dist/actions.js CHANGED
@@ -25,7 +25,7 @@ import { Pressable, View } from "react-native";
25
25
  import { criarFolha } from "./estilos.js";
26
26
  import { Icon } from "./icon.js";
27
27
  import { Text } from "./text.js";
28
- import { useAureaTokens } from "./theme.js";
28
+ import { useAureaTokens, useSobreAMarca } from "./theme.js";
29
29
  const ALTURA = {
30
30
  xs: "controlHXs", sm: "controlHSm", md: "controlHMd", lg: "controlHLg", xl: "controlHXl",
31
31
  };
@@ -67,6 +67,29 @@ function pintar(t, tone) {
67
67
  };
68
68
  return m[tone];
69
69
  }
70
+ /**
71
+ * O mesmo par, corrigido quando a peça está DENTRO de um `Card variant="brand"`.
72
+ *
73
+ * 🔴 **Sem isto o botão não existe sobre o amarelo — medido nas quatro aparências, 17/09/2026:**
74
+ * cheio neutro **1,61** no claro · cheio da marca **1,00** (amarelo no amarelo) · contornado
75
+ * **1,38** no claro · só-texto **1,83** no escuro. O `Card variant="brand"` EXIGE `action` no
76
+ * tipo, então o cartão obrigava a ter um botão e não dava a ele nenhuma forma de aparecer.
77
+ *
78
+ * A saída não é paleta nova: **uma única cor do sistema passa de 3:1 contra o amarelo nos dois
79
+ * temas**, e é a tinta que o cartão já manda para dentro (4,54). Então aqui:
80
+ *
81
+ * - **cheio** — o fundo vira a TINTA e a letra vira o amarelo do cartão. O botão fica escuro
82
+ * sobre o amarelo, que é o único desenho que se enxerga nos dois temas;
83
+ * - **contornado e sem fundo** — contorno e letra viram a tinta.
84
+ *
85
+ * ⚠ **O tom pedido é IGNORADO aqui**, como já acontece no `Text`: um `tone="danger"` sobre o
86
+ * amarelo mede menos que a norma, e obedecer entregaria um botão ilegível em nome da obediência.
87
+ */
88
+ function pintarSobreAMarca(base, marca) {
89
+ if (marca == null)
90
+ return base;
91
+ return { solido: marca.tinta, texto: marca.fundo, sobre: marca.tinta };
92
+ }
70
93
  const folha = criarFolha((t) => ({
71
94
  // ⚠ O ALVO DE TOQUE, e é a decisão mais importante deste arquivo.
72
95
  //
@@ -108,15 +131,17 @@ const folha = criarFolha((t) => ({
108
131
  export function Button({ children, appearance = "solid", tone = "neutral", size = "md", leadingIcon, trailingIcon, leading, trailing, icons, fullWidth = false, pressed, disabled, accessibilityLabel, ...rest }) {
109
132
  const t = useAureaTokens();
110
133
  const s = folha(t);
111
- const cor = pintar(t, tone);
134
+ const marca = useSobreAMarca();
135
+ const cor = pintarSobreAMarca(pintar(t, tone), marca);
136
+ const corDaBorda = marca?.tinta ?? t.color.border;
112
137
  const caixa = React.useMemo(() => ({
113
138
  height: t.size[ALTURA[size]],
114
139
  paddingHorizontal: PADDING[size],
115
140
  gap: GAP[size],
116
141
  backgroundColor: appearance === "solid" ? cor.solido : "transparent",
117
- borderColor: appearance === "outline" ? t.color.border : "transparent",
142
+ borderColor: appearance === "outline" ? corDaBorda : "transparent",
118
143
  ...(fullWidth ? { flex: 1 } : null),
119
- }), [t, size, appearance, cor.solido, fullWidth]);
144
+ }), [t, size, appearance, cor.solido, corDaBorda, fullWidth]);
120
145
  const corDoTexto = appearance === "solid" ? cor.texto : cor.sobre;
121
146
  return (_jsx(Pressable, { disabled: disabled, accessibilityRole: "button", accessibilityLabel: accessibilityLabel, accessibilityState: { disabled: !!disabled, ...(pressed === undefined ? null : { checked: pressed }) }, style: ({ pressed: tocando }) => [
122
147
  s.alvo, fullWidth && s.alvoLargura,
@@ -137,14 +162,16 @@ export function Button({ children, appearance = "solid", tone = "neutral", size
137
162
  export function IconButton({ name, label, appearance = "ghost", tone = "neutral", size = "md", icons, pressed, disabled, ...rest }) {
138
163
  const t = useAureaTokens();
139
164
  const s = folha(t);
140
- const cor = pintar(t, tone);
165
+ const marca = useSobreAMarca();
166
+ const cor = pintarSobreAMarca(pintar(t, tone), marca);
167
+ const corDaBorda = marca?.tinta ?? t.color.border;
141
168
  const lado = t.size[ALTURA[size]];
142
169
  const caixa = React.useMemo(() => ({
143
170
  height: lado, width: lado, paddingHorizontal: 0,
144
171
  borderRadius: size === "xs" || size === "sm" ? t.size.radiusSm : t.size.radiusMd,
145
172
  backgroundColor: appearance === "solid" ? cor.solido : "transparent",
146
- borderColor: appearance === "outline" ? t.color.border : "transparent",
147
- }), [t, lado, size, appearance, cor.solido]);
173
+ borderColor: appearance === "outline" ? corDaBorda : "transparent",
174
+ }), [t, lado, size, appearance, cor.solido, corDaBorda]);
148
175
  return (_jsx(Pressable, { disabled: disabled, accessibilityRole: "button", accessibilityLabel: label, accessibilityState: { disabled: !!disabled, ...(pressed === undefined ? null : { checked: pressed }) }, style: ({ pressed: tocando }) => [
149
176
  s.alvo, { minWidth: t.size.targetMin, alignItems: "center" },
150
177
  tocando && s.pressionado, disabled && s.inerte,
@@ -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/busca.js CHANGED
@@ -75,7 +75,7 @@ import { Icon } from "./icon.js";
75
75
  import { KeyboardAvoiding, useCampo } from "./inputs.js";
76
76
  import { useReduceMotion } from "./movimento.js";
77
77
  import { Text } from "./text.js";
78
- import { useAureaStrings, useAureaTokens } from "./theme.js";
78
+ import { useAureaStrings, useAureaTokens, ForaDaMarca, usePeleSobreAMarca } from "./theme.js";
79
79
  // As mesmas contas do `inputs.tsx`, e elas são repetidas AQUI de propósito: exportá-las de lá
80
80
  // tornaria pública uma conta interna do outro módulo, e um `import` de função privada entre
81
81
  // módulos irmãos é o começo de um acoplamento que ninguém declara. São três linhas.
@@ -205,6 +205,7 @@ const folha = criarFolha((t) => ({
205
205
  export function Combobox({ items, value, onValueChange, onSearchChange, searchDelay = ESPERA_PADRAO, loading, onEndReached, placeholder, searchPlaceholder, empty, clearable = true, draggable = true, disabled, size, chevron = "chevron--down", searchIcon = "search", style, testID, }) {
206
206
  const t = useAureaTokens();
207
207
  const s = folha(t);
208
+ const peleDaMarca = usePeleSobreAMarca();
208
209
  const strings = useAureaStrings();
209
210
  const campo = useCampo();
210
211
  const tam = size ?? campo?.size ?? "md";
@@ -292,31 +293,33 @@ export function Combobox({ items, value, onValueChange, onSearchChange, searchDe
292
293
  s.caixa,
293
294
  { height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) },
294
295
  campo?.invalido && s.invalido,
296
+ // Sobre a marca a tinta vence inválido — ver `usePeleSobreAMarca`.
297
+ peleDaMarca,
295
298
  inativo && s.desabilitado,
296
299
  style,
297
- ], children: [_jsx(Pressable, { testID: testID, onPress: inativo ? undefined : () => setAberto(true), disabled: inativo, accessibilityRole: "button", accessibilityLabel: campo?.label, accessibilityHint: campo?.hint, accessibilityValue: { text: value?.label }, accessibilityState: { disabled: !!inativo, expanded: aberto }, style: s.gatilho, children: _jsx(Text, { size: tam === "sm" ? "xs" : tam === "lg" ? "base" : "md", tone: value ? "default" : "subtle", numberOfLines: 1, style: { flex: 1, minWidth: 0 }, children: value?.label ?? placeholder ?? "" }) }), _jsxs(View, { style: s.acoes, children: [clearable && value != null && !inativo && (_jsx(IconButton, { name: "close", label: strings.comboboxClear, appearance: "ghost", size: "sm", onPress: () => onValueChange?.(null), testID: testID ? `${testID}-limpar` : undefined })), chevron && (_jsx(Pressable, { accessible: false, disabled: inativo, style: s.setaToque, onPress: inativo ? undefined : () => setAberto(true), testID: testID ? `${testID}-seta` : undefined, children: _jsx(Icon, { name: chevron, size: "sm", color: t.color.subtleForeground }) }))] })] }), _jsx(Modal, { visible: aberto, transparent: true, animationType: reduzir !== false ? "none" : "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: fechar, children: _jsxs(KeyboardAvoiding, { style: s.fundoDaLista, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: fechar, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { style: [s.lista, { transform: [{ translateY: arrasto }] }], onLayout: (e) => { altura.current = e.nativeEvent.layout.height; }, onStartShouldSetResponder: () => true, children: [_jsx(View, { style: s.puxadorArea, ...(draggable ? gestos.panHandlers : null), accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: _jsx(View, { style: s.puxador }) }), _jsxs(View, { style: [s.grupo, { height: alturaDoTamanho(t, tam) }], ...(draggable ? gestos.panHandlers : null), children: [searchIcon && _jsx(Icon, { name: searchIcon, size: "sm", color: t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-busca` : undefined, value: texto, onChangeText: digitar, placeholder: searchPlaceholder ?? strings.comboboxSearch, placeholderTextColor: t.color.subtleForeground,
298
- // ⚠ `autoFocus` é o que faz a folha valer a pena: abrir um campo de busca e
299
- // exigir um segundo toque para o teclado subir é um toque a mais em cada
300
- // cadastro. O foco entra com a folha; o teclado vem junto.
301
- autoFocus: true, autoCorrect: false, autoCapitalize: "none",
302
- // Mapeia nos dois sistemas — conferido no fonte do RN, tabela no topo do módulo.
303
- accessibilityRole: "search", accessibilityLabel: searchPlaceholder ?? strings.comboboxSearch,
304
- // `returnKeyType="search"` troca o "enter" do teclado pela lupa. É pista de
305
- // plataforma, não decoração: diz à pessoa que aquele campo é de busca antes de
306
- // ela digitar a primeira letra.
307
- returnKeyType: "search", style: [
308
- s.campoDeTexto,
309
- { fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
310
- color: t.color.foreground },
311
- ] }), texto.length > 0 && (_jsx(IconButton, { name: "close", label: strings.searchClear, appearance: "ghost", size: "sm", onPress: () => digitar(""), testID: testID ? `${testID}-busca-limpar` : undefined }))] }), loading && (_jsx(View, { style: s.carregando, children: _jsx(Spinner, { label: strings.comboboxLoading }) })), nada && (_jsx(View, { style: s.vazio, children: typeof empty === "string" || empty == null
312
- ? _jsx(Text, { size: "sm", tone: "muted", children: empty ?? strings.comboboxEmpty })
313
- : empty })), _jsx(FlatList, { data: filtrados, keyExtractor: (i) => i.value, keyboardShouldPersistTaps: "handled", onEndReached: onEndReached, onEndReachedThreshold: ANTECEDENCIA_DE_PAGINA, renderItem: ({ item }) => (_jsx(Pressable, { disabled: item.disabled, onPress: () => { onValueChange?.(item); fechar(); }, accessibilityRole: "menuitem", accessibilityState: {
314
- selected: item.value === value?.value, disabled: !!item.disabled,
315
- }, style: [
316
- s.opcao,
317
- item.value === value?.value && s.opcaoEscolhida,
318
- item.disabled && s.desabilitado,
319
- ], children: _jsx(Text, { size: "md", weight: item.value === value?.value ? 600 : 400, children: item.label }) })) })] })] }) })] }));
300
+ ], children: [_jsx(Pressable, { testID: testID, onPress: inativo ? undefined : () => setAberto(true), disabled: inativo, accessibilityRole: "button", accessibilityLabel: campo?.label, accessibilityHint: campo?.hint, accessibilityValue: { text: value?.label }, accessibilityState: { disabled: !!inativo, expanded: aberto }, style: s.gatilho, children: _jsx(Text, { size: tam === "sm" ? "xs" : tam === "lg" ? "base" : "md", tone: value ? "default" : "subtle", numberOfLines: 1, style: { flex: 1, minWidth: 0 }, children: value?.label ?? placeholder ?? "" }) }), _jsxs(View, { style: s.acoes, children: [clearable && value != null && !inativo && (_jsx(IconButton, { name: "close", label: strings.comboboxClear, appearance: "ghost", size: "sm", onPress: () => onValueChange?.(null), testID: testID ? `${testID}-limpar` : undefined })), chevron && (_jsx(Pressable, { accessible: false, disabled: inativo, style: s.setaToque, onPress: inativo ? undefined : () => setAberto(true), testID: testID ? `${testID}-seta` : undefined, children: _jsx(Icon, { name: chevron, size: "sm", color: t.color.subtleForeground }) }))] })] }), _jsx(ForaDaMarca, { children: _jsx(Modal, { visible: aberto, transparent: true, animationType: reduzir !== false ? "none" : "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: fechar, children: _jsxs(KeyboardAvoiding, { style: s.fundoDaLista, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: fechar, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { style: [s.lista, { transform: [{ translateY: arrasto }] }], onLayout: (e) => { altura.current = e.nativeEvent.layout.height; }, onStartShouldSetResponder: () => true, children: [_jsx(View, { style: s.puxadorArea, ...(draggable ? gestos.panHandlers : null), accessibilityElementsHidden: true, importantForAccessibility: "no-hide-descendants", children: _jsx(View, { style: s.puxador }) }), _jsxs(View, { style: [s.grupo, { height: alturaDoTamanho(t, tam) }], ...(draggable ? gestos.panHandlers : null), children: [searchIcon && _jsx(Icon, { name: searchIcon, size: "sm", color: t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-busca` : undefined, value: texto, onChangeText: digitar, placeholder: searchPlaceholder ?? strings.comboboxSearch, placeholderTextColor: t.color.subtleForeground,
301
+ // ⚠ `autoFocus` é o que faz a folha valer a pena: abrir um campo de busca e
302
+ // exigir um segundo toque para o teclado subir é um toque a mais em cada
303
+ // cadastro. O foco entra com a folha; o teclado vem junto.
304
+ autoFocus: true, autoCorrect: false, autoCapitalize: "none",
305
+ // Mapeia nos dois sistemas — conferido no fonte do RN, tabela no topo do módulo.
306
+ accessibilityRole: "search", accessibilityLabel: searchPlaceholder ?? strings.comboboxSearch,
307
+ // `returnKeyType="search"` troca o "enter" do teclado pela lupa. É pista de
308
+ // plataforma, não decoração: diz à pessoa que aquele campo é de busca antes de
309
+ // ela digitar a primeira letra.
310
+ returnKeyType: "search", style: [
311
+ s.campoDeTexto,
312
+ { fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
313
+ color: t.color.foreground },
314
+ ] }), texto.length > 0 && (_jsx(IconButton, { name: "close", label: strings.searchClear, appearance: "ghost", size: "sm", onPress: () => digitar(""), testID: testID ? `${testID}-busca-limpar` : undefined }))] }), loading && (_jsx(View, { style: s.carregando, children: _jsx(Spinner, { label: strings.comboboxLoading }) })), nada && (_jsx(View, { style: s.vazio, children: typeof empty === "string" || empty == null
315
+ ? _jsx(Text, { size: "sm", tone: "muted", children: empty ?? strings.comboboxEmpty })
316
+ : empty })), _jsx(FlatList, { data: filtrados, keyExtractor: (i) => i.value, keyboardShouldPersistTaps: "handled", onEndReached: onEndReached, onEndReachedThreshold: ANTECEDENCIA_DE_PAGINA, renderItem: ({ item }) => (_jsx(Pressable, { disabled: item.disabled, onPress: () => { onValueChange?.(item); fechar(); }, accessibilityRole: "menuitem", accessibilityState: {
317
+ selected: item.value === value?.value, disabled: !!item.disabled,
318
+ }, style: [
319
+ s.opcao,
320
+ item.value === value?.value && s.opcaoEscolhida,
321
+ item.disabled && s.desabilitado,
322
+ ], children: _jsx(Text, { size: "md", weight: item.value === value?.value ? 600 : 400, children: item.label }) })) })] })] }) }) })] }));
320
323
  }
321
324
  // ── A DOBRA DE ACENTO, e por que ela é sondada em vez de presumida ───────────────────────────
322
325
  // Buscar "acucar" tem de achar "açúcar" — num catálogo em português, exigir o acento certo é
@@ -358,6 +361,7 @@ const dobrar = (v) => {
358
361
  export function SearchField({ value, onChangeText, onSearchChange, searchDelay = ESPERA_PADRAO, placeholder, disabled, size, icon = "search", clearable = true, onSubmit, style, testID, }) {
359
362
  const t = useAureaTokens();
360
363
  const s = folha(t);
364
+ const peleDaMarca = usePeleSobreAMarca();
361
365
  const strings = useAureaStrings();
362
366
  const campo = useCampo();
363
367
  const tam = size ?? campo?.size ?? "md";
@@ -400,9 +404,12 @@ export function SearchField({ value, onChangeText, onSearchChange, searchDelay =
400
404
  // O foco é a única pista de campo ativo que sobra no telefone — mesma decisão do `Input`
401
405
  // do Lote 4, e ela vale igual aqui.
402
406
  focado && { borderColor: t.color.focusStrong },
407
+ // Sobre a marca a tinta vence inválido e foco; o foco vira ESPESSURA — `usePeleSobreAMarca`.
408
+ peleDaMarca,
409
+ peleDaMarca && focado && { borderWidth: t.size.borderWidth * 2 },
403
410
  inativo && s.desabilitado,
404
411
  style,
405
- ], children: [icon && _jsx(Icon, { name: icon, size: "sm", color: t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: value, onChangeText: digitar, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, editable: !inativo, autoCorrect: false, autoCapitalize: "none", returnKeyType: "search", onSubmitEditing: onSubmit, onFocus: () => setFocado(true), onBlur: () => setFocado(false), accessibilityRole: "search", accessibilityLabel: campo?.label ?? placeholder, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, style: [
412
+ ], children: [icon && _jsx(Icon, { name: icon, size: "sm", color: peleDaMarca?.color ?? t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: value, onChangeText: digitar, placeholder: placeholder, placeholderTextColor: peleDaMarca?.color ?? t.color.subtleForeground, editable: !inativo, autoCorrect: false, autoCapitalize: "none", returnKeyType: "search", onSubmitEditing: onSubmit, onFocus: () => setFocado(true), onBlur: () => setFocado(false), accessibilityRole: "search", accessibilityLabel: campo?.label ?? placeholder, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, style: [
406
413
  s.campoDeTexto,
407
414
  { fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
408
415
  color: t.color.foreground },
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
@@ -45,7 +45,7 @@ import { IconButton } from "./actions.js";
45
45
  import { Icon } from "./icon.js";
46
46
  import { useReduceMotion } from "./movimento.js";
47
47
  import { Text } from "./text.js";
48
- import { useAureaStrings, useAureaTokens } from "./theme.js";
48
+ import { useAureaStrings, useAureaTokens, usePeleSobreAMarca, ForaDaMarca } from "./theme.js";
49
49
  const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
50
50
  const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
51
51
  const respiroDoTamanho = (t, s) => s === "sm" ? t.size.space3 : s === "lg" ? t.size.space4 : 13;
@@ -190,6 +190,7 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
190
190
  ...rest }) {
191
191
  const t = useAureaTokens();
192
192
  const s = folha(t);
193
+ const peleDaMarca = usePeleSobreAMarca();
193
194
  const campo = useCampo();
194
195
  const grupo = useGrupoDeCampo();
195
196
  const tam = size ?? grupo?.size ?? campo?.size ?? "md";
@@ -199,7 +200,7 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
199
200
  // O resto vem PRIMEIRO de propósito: o que a Aurea calcula (respiro, cor, nome do `Field`,
200
201
  // `editable`) tem de vencer, e as props que ela redefine estão no `Omit` da interface —
201
202
  // então não chegam por aqui e não há o que sobrescrever por acidente.
202
- , { ...rest, testID: testID, value: value, defaultValue: defaultValue, onChangeText: onChangeText, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, editable: !inativo, keyboardType: keyboardType, secureTextEntry: secureTextEntry, autoCapitalize: autoCapitalize, multiline: multiline, onFocus: () => { setFocado(true); grupo?.aoFocar(true); onFocus?.(); }, onBlur: () => {
203
+ , { ...rest, testID: testID, value: value, defaultValue: defaultValue, onChangeText: onChangeText, placeholder: placeholder, placeholderTextColor: peleDaMarca?.color ?? t.color.subtleForeground, editable: !inativo, keyboardType: keyboardType, secureTextEntry: secureTextEntry, autoCapitalize: autoCapitalize, multiline: multiline, onFocus: () => { setFocado(true); grupo?.aoFocar(true); onFocus?.(); }, onBlur: () => {
203
204
  setFocado(false);
204
205
  grupo?.aoFocar(false);
205
206
  // Só avisa quando o texto MUDOU: emitir o mesmo valor a cada saída de foco faria o app
@@ -244,6 +245,16 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
244
245
  // O foco é o único estado que sobra do trio da web (hover/focus/active), e ele importa
245
246
  // mais aqui: no telefone é a única pista de qual campo o teclado está alimentando.
246
247
  !grupo && focado && { borderColor: t.color.focusStrong },
248
+ // 🔴 DENTRO DO CARTÃO DA MARCA A TINTA VENCE TUDO — inclusive inválido e foco, e isso é
249
+ // medição, não preferência (os números estão no `usePeleSobreAMarca`): sobre o amarelo a
250
+ // borda de inválido mede **1,86** e o `focusStrong` do tema ESCURO mede **1,00**, porque
251
+ // lá ele É o amarelo. Deixá-los vencer apagaria o campo justamente quando ele mais
252
+ // precisa ser achado.
253
+ peleDaMarca && { color: peleDaMarca.color },
254
+ !grupo && peleDaMarca,
255
+ // E o foco continua existindo, por ESPESSURA em vez de cor — a única pista que sobra
256
+ // quando só há uma tinta. O dobro da borda do token, não número novo.
257
+ !grupo && peleDaMarca && focado && { borderWidth: t.size.borderWidth * 2 },
247
258
  !grupo && inativo && s.desabilitado,
248
259
  style,
249
260
  ] }));
@@ -277,6 +288,7 @@ export function Textarea({ rows, size, style, ...rest }) {
277
288
  export function InputGroup({ size, disabled, children, style, testID, ...rest }) {
278
289
  const t = useAureaTokens();
279
290
  const s = folha(t);
291
+ const peleDaMarca = usePeleSobreAMarca();
280
292
  const campo = useCampo();
281
293
  const tam = size ?? campo?.size ?? "md";
282
294
  const inativo = disabled ?? campo?.disabled ?? false;
@@ -291,6 +303,9 @@ export function InputGroup({ size, disabled, children, style, testID, ...rest })
291
303
  // O foco vem do campo lá dentro, por contexto: no React Native não existe `:focus-within`,
292
304
  // e sem isto a única pista de qual campo o teclado alimenta sumiria ao entrar num grupo.
293
305
  focado && { borderColor: t.color.focusStrong },
306
+ // Sobre a marca a tinta vence inválido e foco — ver `usePeleSobreAMarca`.
307
+ peleDaMarca,
308
+ peleDaMarca && focado && { borderWidth: t.size.borderWidth * 2 },
294
309
  inativo && s.desabilitado,
295
310
  style,
296
311
  ], ...rest, children: _jsx(GrupoDeCampo.Provider, { value: ctx, children: children }) }));
@@ -312,16 +327,23 @@ export function PasswordField({ defaultVisible = false, size, disabled, autoCapi
312
327
  const inativo = disabled ?? campo?.disabled;
313
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 }) })] }));
314
329
  }
315
- function ControleMarcado({ papel, label, description, checked, onChange, disabled, size, style, testID, }) {
330
+ function ControleMarcado({ papel, label, description, checked, onChange, disabled, size, style, testID, accessibilityLabel, }) {
316
331
  const t = useAureaTokens();
317
332
  const s = folha(t);
318
333
  const campo = useCampo();
319
334
  const tam = size ?? campo?.size ?? "md";
320
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;
321
339
  // A marca é METADE da altura do controle — `aurea.css:871`, e é o que faz ela crescer junto
322
340
  // com a densidade sem token próprio.
323
341
  const lado = alturaDoTamanho(t, tam) / 2;
324
- 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: [
325
347
  s.marcaBase,
326
348
  { width: lado, height: lado,
327
349
  borderRadius: papel === "radio" ? t.size.radiusFull : 5 },
@@ -340,8 +362,8 @@ function ControleMarcado({ papel, label, description, checked, onChange, disable
340
362
  borderLeftWidth: 2, borderBottomWidth: 2,
341
363
  borderColor: t.color.controlSelectedForeground,
342
364
  transform: [{ rotate: "-45deg" }],
343
- } })) }), _jsxs(View, { style: { flex: 1 }, children: [typeof label === "string" ? _jsx(Text, { size: "sm", children: label }) : label, description != null && (typeof description === "string"
344
- ? _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)] }))] }));
345
367
  }
346
368
  /** A caixa que marca. `accessibilityRole="checkbox"` + `state.checked`. */
347
369
  export function Checkbox(p) { return _jsx(ControleMarcado, { ...p, papel: "checkbox" }); }
@@ -363,13 +385,14 @@ export function Radio(p) { return _jsx(ControleMarcado, { ...p, papel: "radio" }
363
385
  * ⚠ **E ele respeita "menos movimento"**: com a preferência ligada, o polegar salta em vez de
364
386
  * deslizar. O estado continua o mesmo — o que some é a animação, não a informação.
365
387
  */
366
- export function Switch({ label, description, checked, onChange, disabled, size, style, testID, }) {
388
+ export function Switch({ label, description, checked, onChange, disabled, size, style, testID, accessibilityLabel, }) {
367
389
  const t = useAureaTokens();
368
390
  const s = folha(t);
369
391
  const campo = useCampo();
370
392
  const tam = size ?? campo?.size ?? "md";
371
393
  const inativo = disabled ?? campo?.disabled;
372
394
  const reduzir = useReduceMotion();
395
+ const temTexto = label != null || description != null;
373
396
  const h = alturaDoTamanho(t, tam);
374
397
  const larguraDoTrilho = h * 7 / 6;
375
398
  const alturaDoTrilho = h * 2 / 3;
@@ -388,10 +411,28 @@ export function Switch({ label, description, checked, onChange, disabled, size,
388
411
  a.start();
389
412
  return () => a.stop();
390
413
  }, [checked, pos, reduzir]);
391
- 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),
392
417
  // Mesma regra do `ControleMarcado`: a descrição vira DICA quando é texto. Sem isto ela
393
418
  // desenha na tela e não existe para quem usa leitor de tela.
394
- 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: [
395
436
  s.trilho,
396
437
  { width: larguraDoTrilho, height: alturaDoTrilho },
397
438
  checked && { backgroundColor: t.color.controlSelected, borderColor: t.color.controlSelected },
@@ -400,8 +441,8 @@ export function Switch({ label, description, checked, onChange, disabled, size,
400
441
  { width: ladoDoPolegar, height: ladoDoPolegar,
401
442
  backgroundColor: checked ? t.color.controlSelectedForeground : t.color.mutedForeground,
402
443
  transform: [{ translateX: pos.interpolate({ inputRange: [0, 1], outputRange: [0, curso] }) }] },
403
- ] }) }), _jsxs(View, { style: { flex: 1 }, children: [typeof label === "string" ? _jsx(Text, { size: "sm", children: label }) : label, description != null && (typeof description === "string"
404
- ? _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)] }))] }));
405
446
  }
406
447
  /**
407
448
  * Um de poucos, lado a lado.
@@ -463,6 +504,7 @@ export function SegmentedControl({ items, value, onChange, label, disabled, styl
463
504
  export function Select({ items, value, onChange, placeholder, disabled, size, chevron = "chevron--down", style, testID, }) {
464
505
  const t = useAureaTokens();
465
506
  const s = folha(t);
507
+ const peleDaMarca = usePeleSobreAMarca();
466
508
  const campo = useCampo();
467
509
  const tam = size ?? campo?.size ?? "md";
468
510
  const inativo = disabled ?? campo?.disabled;
@@ -473,9 +515,11 @@ export function Select({ items, value, onChange, placeholder, disabled, size, ch
473
515
  { flexDirection: "row", alignItems: "center", justifyContent: "space-between",
474
516
  height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) },
475
517
  campo?.invalido && s.invalido,
518
+ // Sobre a marca a tinta vence inválido — ver `usePeleSobreAMarca`.
519
+ peleDaMarca,
476
520
  inativo && s.desabilitado,
477
521
  style,
478
- ], children: [_jsx(Text, { size: tam === "sm" ? "xs" : tam === "lg" ? "base" : "md", tone: escolhido ? "default" : "subtle", numberOfLines: 1, children: escolhido?.label ?? placeholder ?? "" }), chevron && _jsx(Icon, { name: chevron, size: "sm", color: t.color.subtleForeground })] }), _jsx(Modal, { visible: aberto, transparent: true, animationType: "slide", onRequestClose: () => setAberto(false), children: _jsxs(View, { style: s.fundoDaLista, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: () => setAberto(false), accessible: false }), _jsx(View, { style: s.lista, onStartShouldSetResponder: () => true, children: _jsx(ScrollView, { children: items.map((it) => (_jsx(Pressable, { disabled: it.disabled, onPress: () => { onChange?.(it.value); setAberto(false); }, accessibilityRole: "menuitem", accessibilityState: { selected: it.value === value, disabled: !!it.disabled }, style: [s.opcao, it.disabled && s.desabilitado], children: _jsx(Text, { size: "md", weight: it.value === value ? 600 : 400, children: it.label }) }, it.value))) }) })] }) })] }));
522
+ ], children: [_jsx(Text, { size: tam === "sm" ? "xs" : tam === "lg" ? "base" : "md", tone: escolhido ? "default" : "subtle", numberOfLines: 1, children: escolhido?.label ?? placeholder ?? "" }), chevron && _jsx(Icon, { name: chevron, size: "sm", color: peleDaMarca?.color ?? t.color.subtleForeground })] }), _jsx(ForaDaMarca, { children: _jsx(Modal, { visible: aberto, transparent: true, animationType: "slide", onRequestClose: () => setAberto(false), children: _jsxs(View, { style: s.fundoDaLista, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: () => setAberto(false), accessible: false }), _jsx(View, { style: s.lista, onStartShouldSetResponder: () => true, children: _jsx(ScrollView, { children: items.map((it) => (_jsx(Pressable, { disabled: it.disabled, onPress: () => { onChange?.(it.value); setAberto(false); }, accessibilityRole: "menuitem", accessibilityState: { selected: it.value === value, disabled: !!it.disabled }, style: [s.opcao, it.disabled && s.desabilitado], children: _jsx(Text, { size: "md", weight: it.value === value ? 600 : 400, children: it.label }) }, it.value))) }) })] }) }) })] }));
479
523
  }
480
524
  /**
481
525
  * A pilha de campos, com o respiro do `--space-5`.
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.
@@ -134,7 +156,15 @@ export function Card({ variant = "base", style, ...rest }) {
134
156
  // ⚠ **Consequência declarada: sobre o amarelo existe UMA cor de texto, não duas.** O tom
135
157
  // `muted` mede 1,35 no escuro, então ele não pode sobreviver aqui. A hierarquia dentro deste
136
158
  // cartão sai de PESO e TAMANHO, não de cor — e isso é restrição do contraste, não gosto.
159
+ //
160
+ // 🔴 **E ISTO NASCEU PELA METADE — o consumidor achou em UM DIA, 17/09/2026.** O parágrafo
161
+ // acima fala só de TEXTO, e foi só o texto que passou a ler o contexto. Medido depois, dentro
162
+ // deste mesmo cartão: contorno de campo **2,43/2,24**, fundo de campo **1,61** no claro, glifo
163
+ // de botão sem fundo **1,83** no escuro — e as QUATRO aparências do `action` que este tipo
164
+ // EXIGE, todas invisíveis (1,61 · 1,00 · 1,38 · 1,83).
165
+ // **A mesma frase valia para linha e glifo desde o primeiro dia, e eu parei no texto.**
166
+ // ✅ Hoje o contexto carrega o par (tinta, fundo), e quem lê está listado no `useSobreAMarca`.
137
167
  return variant === "brand"
138
- ? _jsx(SobreAMarca.Provider, { value: t.color.primaryForeground, children: pele })
168
+ ? _jsx(SobreAMarca.Provider, { value: { tinta: t.color.primaryForeground, fundo: t.color.primary }, children: pele })
139
169
  : pele;
140
170
  }