@aurea-uds/native 0.8.5 → 0.8.7

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,
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/inputs.js CHANGED
@@ -40,11 +40,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
40
40
  import * as React from "react";
41
41
  import { Animated, KeyboardAvoidingView as KeyboardAvoidingViewRN, Modal, Platform, Pressable, ScrollView, TextInput, View, } from "react-native";
42
42
  import { comOpacidade, criarFolha } from "./estilos.js";
43
+ import { FilaRolante } from "./rolagem.js";
43
44
  import { IconButton } from "./actions.js";
44
45
  import { Icon } from "./icon.js";
45
46
  import { useReduceMotion } from "./movimento.js";
46
47
  import { Text } from "./text.js";
47
- import { useAureaStrings, useAureaTokens } from "./theme.js";
48
+ import { useAureaStrings, useAureaTokens, usePeleSobreAMarca, ForaDaMarca } from "./theme.js";
48
49
  const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
49
50
  const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
50
51
  const respiroDoTamanho = (t, s) => s === "sm" ? t.size.space3 : s === "lg" ? t.size.space4 : 13;
@@ -189,6 +190,7 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
189
190
  ...rest }) {
190
191
  const t = useAureaTokens();
191
192
  const s = folha(t);
193
+ const peleDaMarca = usePeleSobreAMarca();
192
194
  const campo = useCampo();
193
195
  const grupo = useGrupoDeCampo();
194
196
  const tam = size ?? grupo?.size ?? campo?.size ?? "md";
@@ -198,7 +200,7 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
198
200
  // O resto vem PRIMEIRO de propósito: o que a Aurea calcula (respiro, cor, nome do `Field`,
199
201
  // `editable`) tem de vencer, e as props que ela redefine estão no `Omit` da interface —
200
202
  // então não chegam por aqui e não há o que sobrescrever por acidente.
201
- , { ...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: () => {
202
204
  setFocado(false);
203
205
  grupo?.aoFocar(false);
204
206
  // Só avisa quando o texto MUDOU: emitir o mesmo valor a cada saída de foco faria o app
@@ -243,6 +245,16 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
243
245
  // O foco é o único estado que sobra do trio da web (hover/focus/active), e ele importa
244
246
  // mais aqui: no telefone é a única pista de qual campo o teclado está alimentando.
245
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 },
246
258
  !grupo && inativo && s.desabilitado,
247
259
  style,
248
260
  ] }));
@@ -276,6 +288,7 @@ export function Textarea({ rows, size, style, ...rest }) {
276
288
  export function InputGroup({ size, disabled, children, style, testID, ...rest }) {
277
289
  const t = useAureaTokens();
278
290
  const s = folha(t);
291
+ const peleDaMarca = usePeleSobreAMarca();
279
292
  const campo = useCampo();
280
293
  const tam = size ?? campo?.size ?? "md";
281
294
  const inativo = disabled ?? campo?.disabled ?? false;
@@ -290,6 +303,9 @@ export function InputGroup({ size, disabled, children, style, testID, ...rest })
290
303
  // O foco vem do campo lá dentro, por contexto: no React Native não existe `:focus-within`,
291
304
  // e sem isto a única pista de qual campo o teclado alimenta sumiria ao entrar num grupo.
292
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 },
293
309
  inativo && s.desabilitado,
294
310
  style,
295
311
  ], ...rest, children: _jsx(GrupoDeCampo.Provider, { value: ctx, children: children }) }));
@@ -415,30 +431,30 @@ export function SegmentedControl({ items, value, onChange, label, disabled, styl
415
431
  const s = folha(t);
416
432
  const campo = useCampo();
417
433
  const inativo = disabled ?? campo?.disabled;
418
- return (_jsx(View, { accessibilityRole: "radiogroup", accessibilityLabel: label ?? campo?.label, style: [s.segmentada, inativo && s.desabilitado, style], ...rest, children: items.map((it) => {
419
- const ativo = it.value === value;
420
- return (_jsxs(Pressable, { onPress: inativo ? undefined : () => onChange?.(it.value), disabled: inativo, accessibilityRole: "radio", accessibilityState: { checked: ativo, disabled: !!inativo }, accessibilityLabel: typeof it.label === "string" ? it.label : undefined, style: [s.segmento, ativo && s.segmentoAtivo], children: [typeof it.label === "string"
421
- ? _jsx(Text, { size: "sm", weight: ativo ? 600 : 400,
422
- // 🔴 ERA `foreground`, E ISSO QUEBRAVA A LINGUAGEM DE SELECIONADO.
423
- // Achado pelo consumidor em 17/09/2026. A web pinta o escolhido com a
424
- // MARCA (`aurea.css:937-938`), e o comentário de cima daquela regra diz
425
- // que nav ativo, aba ativa, item de lista e botão ligado mostram o mesmo
426
- // sinal — ordem do Victor, 24/07. O `SegmentedControl` daqui discordava
427
- // da web **e do `BottomNav` deste mesmo pacote**, que já usa este token.
428
- //
429
- // ⚠ **`primaryEmphasis` e NÃO `primary`, e a razão está no CSS:** *"sem
430
- // fundo, a marca vira TEXTO, e o amarelo puro"* não se lê no claro
431
- // (`aurea.css:358`). É a mesma lição que a `0.8.3` pagou no `Button`.
432
- // No escuro os dois valem o mesmo amarelo; no claro este aprofunda.
433
- //
434
- // ⚠ **Uma divergência declarada, não escondida:** para o `.segmented` a
435
- // web usa uma mistura própria (`primary 45% + foreground`) em vez do
436
- // token, enquanto para o `.bottom-nav` usa o token. São dois âmbares
437
- // parecidos para a MESMA linguagem, e isso é incoerência DA WEB. Escolhi
438
- // o token, que deixa os dois componentes daqui iguais entre si.
439
- style: { color: ativo ? t.color.primaryEmphasis : t.color.mutedForeground }, children: it.label })
440
- : it.label, ativo && _jsx(View, { style: s.fioDoSegmento })] }, it.value));
441
- }) }));
434
+ return (_jsx(FilaRolante, { children: _jsx(View, { accessibilityRole: "radiogroup", accessibilityLabel: label ?? campo?.label, style: [s.segmentada, inativo && s.desabilitado, style], ...rest, children: items.map((it) => {
435
+ const ativo = it.value === value;
436
+ return (_jsxs(Pressable, { onPress: inativo ? undefined : () => onChange?.(it.value), disabled: inativo, accessibilityRole: "radio", accessibilityState: { checked: ativo, disabled: !!inativo }, accessibilityLabel: typeof it.label === "string" ? it.label : undefined, style: [s.segmento, ativo && s.segmentoAtivo], children: [typeof it.label === "string"
437
+ ? _jsx(Text, { size: "sm", weight: ativo ? 600 : 400,
438
+ // 🔴 ERA `foreground`, E ISSO QUEBRAVA A LINGUAGEM DE SELECIONADO.
439
+ // Achado pelo consumidor em 17/09/2026. A web pinta o escolhido com a
440
+ // MARCA (`aurea.css:937-938`), e o comentário de cima daquela regra diz
441
+ // que nav ativo, aba ativa, item de lista e botão ligado mostram o mesmo
442
+ // sinal — ordem do Victor, 24/07. O `SegmentedControl` daqui discordava
443
+ // da web **e do `BottomNav` deste mesmo pacote**, que já usa este token.
444
+ //
445
+ // ⚠ **`primaryEmphasis` e NÃO `primary`, e a razão está no CSS:** *"sem
446
+ // fundo, a marca vira TEXTO, e o amarelo puro"* não se lê no claro
447
+ // (`aurea.css:358`). É a mesma lição que a `0.8.3` pagou no `Button`.
448
+ // No escuro os dois valem o mesmo amarelo; no claro este aprofunda.
449
+ //
450
+ // ⚠ **Uma divergência declarada, não escondida:** para o `.segmented` a
451
+ // web usa uma mistura própria (`primary 45% + foreground`) em vez do
452
+ // token, enquanto para o `.bottom-nav` usa o token. São dois âmbares
453
+ // parecidos para a MESMA linguagem, e isso é incoerência DA WEB. Escolhi
454
+ // o token, que deixa os dois componentes daqui iguais entre si.
455
+ style: { color: ativo ? t.color.primaryEmphasis : t.color.mutedForeground }, children: it.label })
456
+ : it.label, ativo && _jsx(View, { style: s.fioDoSegmento })] }, it.value));
457
+ }) }) }));
442
458
  }
443
459
  /**
444
460
  * A escolha de um de muitos.
@@ -462,6 +478,7 @@ export function SegmentedControl({ items, value, onChange, label, disabled, styl
462
478
  export function Select({ items, value, onChange, placeholder, disabled, size, chevron = "chevron--down", style, testID, }) {
463
479
  const t = useAureaTokens();
464
480
  const s = folha(t);
481
+ const peleDaMarca = usePeleSobreAMarca();
465
482
  const campo = useCampo();
466
483
  const tam = size ?? campo?.size ?? "md";
467
484
  const inativo = disabled ?? campo?.disabled;
@@ -472,9 +489,11 @@ export function Select({ items, value, onChange, placeholder, disabled, size, ch
472
489
  { flexDirection: "row", alignItems: "center", justifyContent: "space-between",
473
490
  height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) },
474
491
  campo?.invalido && s.invalido,
492
+ // Sobre a marca a tinta vence inválido — ver `usePeleSobreAMarca`.
493
+ peleDaMarca,
475
494
  inativo && s.desabilitado,
476
495
  style,
477
- ], 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))) }) })] }) })] }));
496
+ ], 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))) }) })] }) }) })] }));
478
497
  }
479
498
  /**
480
499
  * A pilha de campos, com o respiro do `--space-5`.
package/dist/layout.d.ts CHANGED
@@ -55,11 +55,34 @@ export interface SeparatorProps extends ViewProps {
55
55
  * comportamento certo.
56
56
  */
57
57
  export declare function Separator({ orientation, label, style, ...rest }: SeparatorProps): React.JSX.Element;
58
- export type AureaCardVariant = "base" | "raised" | "interactive" | "inset" | "selected" | "danger";
59
- export interface CardProps extends ViewProps {
60
- variant?: AureaCardVariant;
58
+ export type AureaCardVariant = "base" | "raised" | "interactive" | "inset" | "selected" | "danger" | "brand";
59
+ interface CardBase extends ViewProps {
61
60
  children?: React.ReactNode;
62
61
  }
62
+ /**
63
+ * 🔴 **O `brand` EXIGE `action`, e isso é de propósito — a regra é do TIPO, não de documento.**
64
+ *
65
+ * O cartão na cor da marca entrou em 17/09/2026 e é a **primeira superfície grande preenchida
66
+ * com o amarelo** neste sistema. Até aqui ele só pintava coisa pequena: o ponto do contador, a
67
+ * pílula do passo atual, o link de pular para o conteúdo. Um cartão amarelo é precedente, e
68
+ * precedente sem trava vira decoração — outras telas pedem o mesmo e o amarelo deixa de
69
+ * significar "aja aqui".
70
+ *
71
+ * A regra combinada foi *"o cartão amarelo é só para pedir uma ação da pessoa"*. **Escrita num
72
+ * documento, ela apagaria** — este repositório tem registro disso em cada arquivo, e a §"Afirmação
73
+ * velha" do `CLAUDE.md` existe por causa desse desgaste. Então ela mora no tipo: sem `action`, o
74
+ * `brand` não compila. É a mesma escolha que faz o `label` do `IconButton` ser obrigatório.
75
+ */
76
+ interface CardDaMarca extends CardBase {
77
+ variant: "brand";
78
+ /** O que a pessoa faz aqui — um `Button`, um `IconButton`, um campo. Sem isto não compila. */
79
+ action: React.ReactNode;
80
+ }
81
+ interface CardComum extends CardBase {
82
+ variant?: Exclude<AureaCardVariant, "brand">;
83
+ action?: never;
84
+ }
85
+ export type CardProps = CardDaMarca | CardComum;
63
86
  /**
64
87
  * Superfície flutuante, raio 22 (`--radius-card`) — identidade declarada INTOCÁVEL.
65
88
  *
@@ -69,3 +92,4 @@ export interface CardProps extends ViewProps {
69
92
  * que É um alvo — quem o torna tocável é a tela.
70
93
  */
71
94
  export declare function Card({ variant, style, ...rest }: CardProps): React.JSX.Element;
95
+ export {};
package/dist/layout.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  // Aurea nativo — os primitivos de layout: `Stack`, `Cluster`, `Grid`, `Card`.
3
3
  //
4
4
  // Medidos no core da web antes de escrever (passo 1 do BUILDING.md), e o que eles são lá:
@@ -19,7 +19,7 @@ import { View } from "react-native";
19
19
  // `...rest` sem nada a mais.
20
20
  import { criarFolha } from "./estilos.js";
21
21
  import { Text } from "./text.js";
22
- import { useAureaTokens } from "./theme.js";
22
+ import { useAureaTokens, SobreAMarca } from "./theme.js";
23
23
  const folha = criarFolha((t) => ({
24
24
  // A LINHA DO SISTEMA, e os números são os mesmos do `.separator` da web (`aurea.css`): cor de
25
25
  // `border` e espessura de UM pixel. ⚠ Um pixel não sai de `--space-*` de propósito — a linha
@@ -52,6 +52,11 @@ const folha = criarFolha((t) => ({
52
52
  card_inset: { backgroundColor: t.color.surfaceInset, borderColor: "transparent" },
53
53
  card_selected: { borderColor: t.color.primaryOutline, backgroundColor: t.color.surface2 },
54
54
  card_danger: { borderColor: t.color.danger400 ?? t.color.destructive, backgroundColor: t.color.dangerBg },
55
+ // O amarelo é o DE PREENCHER — `primary`, o mesmo do botão sólido. A borda some: uma superfície
56
+ // cheia não precisa de contorno, e um contorno sobre o amarelo mede 2,43 no claro (invisível).
57
+ card_brand: { backgroundColor: t.color.primary, borderColor: "transparent" },
58
+ // A ação fica embaixo, separada pelo mesmo respiro que o resto da casa usa entre blocos.
59
+ acaoDaMarca: { marginTop: t.size.space3 },
55
60
  }));
56
61
  /** Coluna com `--space-4` entre os filhos. **Sem prop de espaçamento, de propósito.** */
57
62
  export function Stack({ style, ...rest }) {
@@ -112,6 +117,32 @@ export function Separator({ orientation = "horizontal", label, style, ...rest })
112
117
  * que É um alvo — quem o torna tocável é a tela.
113
118
  */
114
119
  export function Card({ variant = "base", style, ...rest }) {
115
- const s = folha(useAureaTokens());
116
- return _jsx(View, { style: [s.cardBase, s[`card_${variant}`], style], ...rest });
120
+ const t = useAureaTokens();
121
+ const s = folha(t);
122
+ const { action, children, ...resto } = rest;
123
+ const corpo = (_jsxs(_Fragment, { children: [children, action != null && _jsx(View, { style: s.acaoDaMarca, children: action })] }));
124
+ const pele = _jsx(View, { style: [s.cardBase, s[`card_${variant}`], style], ...resto, children: corpo });
125
+ // 🔴 SOBRE O AMARELO A COR DO TEXTO NÃO É ESCOLHA, É OBRIGAÇÃO — e os números decidiram isto.
126
+ // Medido nos dois temas, texto contra o `primary`:
127
+ // texto comum ........ 10,34 no claro · **1,83 no escuro** 🔴 ilegível
128
+ // texto esmaecido ..... 4,24 no claro · **1,35 no escuro** 🔴 ilegível
129
+ // primary-foreground .. 4,54 nos DOIS ✅ único que serve
130
+ // Sem forçar, o cartão da marca sairia quebrado no tema escuro **sem ninguém ver no claro**.
131
+ // ⚠ **E no RN não há cascata de cor**: `color` num `View` não desce para o `Text` de dentro.
132
+ // A saída é CONTEXTO — a mesma decisão que o `Field` do Lote 4 tomou para empurrar nome e dica.
133
+ //
134
+ // ⚠ **Consequência declarada: sobre o amarelo existe UMA cor de texto, não duas.** O tom
135
+ // `muted` mede 1,35 no escuro, então ele não pode sobreviver aqui. A hierarquia dentro deste
136
+ // cartão sai de PESO e TAMANHO, não de cor — e isso é restrição do contraste, não gosto.
137
+ //
138
+ // 🔴 **E ISTO NASCEU PELA METADE — o consumidor achou em UM DIA, 17/09/2026.** O parágrafo
139
+ // acima fala só de TEXTO, e foi só o texto que passou a ler o contexto. Medido depois, dentro
140
+ // deste mesmo cartão: contorno de campo **2,43/2,24**, fundo de campo **1,61** no claro, glifo
141
+ // de botão sem fundo **1,83** no escuro — e as QUATRO aparências do `action` que este tipo
142
+ // EXIGE, todas invisíveis (1,61 · 1,00 · 1,38 · 1,83).
143
+ // **A mesma frase valia para linha e glifo desde o primeiro dia, e eu parei no texto.**
144
+ // ✅ Hoje o contexto carrega o par (tinta, fundo), e quem lê está listado no `useSobreAMarca`.
145
+ return variant === "brand"
146
+ ? _jsx(SobreAMarca.Provider, { value: { tinta: t.color.primaryForeground, fundo: t.color.primary }, children: pele })
147
+ : pele;
117
148
  }
@@ -1,10 +1,11 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Pressable, View, ScrollView } from "react-native";
2
+ import { Pressable, View } from "react-native";
3
3
  import { useSafeAreaInsets } from "react-native-safe-area-context";
4
4
  import { Badge } from "./display.js";
5
5
  import { comOpacidade, criarFolha } from "./estilos.js";
6
6
  import { Icon } from "./icon.js";
7
7
  import { Card } from "./layout.js";
8
+ import { FilaRolante } from "./rolagem.js";
8
9
  import { Text } from "./text.js";
9
10
  import { useAureaStrings, useAureaTokens } from "./theme.js";
10
11
  const folha = criarFolha((t) => ({
@@ -317,10 +318,7 @@ export function Tabs({ tabs, value, onChange, label, style, ...rest }) {
317
318
  const s = folha(t);
318
319
  const strings = useAureaStrings();
319
320
  const aberta = tabs.find((it) => it.id === value);
320
- return (_jsxs(View, { style: style, ...rest, children: [_jsx(ScrollView, { horizontal: true, showsHorizontalScrollIndicator: false,
321
- // Sem isto o conteúdo estica até a largura do rolador e o `padding` de 3 da cápsula
322
- // aparece como faixa vazia à direita quando as abas cabem.
323
- contentContainerStyle: { flexGrow: 0 }, children: _jsx(View, { accessibilityRole: "tablist", accessibilityLabel: label ?? strings.tabsLabel, style: s.abas, children: tabs.map((it) => {
321
+ return (_jsxs(View, { style: style, ...rest, children: [_jsx(FilaRolante, { children: _jsx(View, { accessibilityRole: "tablist", accessibilityLabel: label ?? strings.tabsLabel, style: s.abas, children: tabs.map((it) => {
324
322
  const ativa = it.id === value;
325
323
  return (_jsx(Pressable, { onPress: it.disabled ? undefined : () => onChange?.(it.id), disabled: it.disabled, accessibilityRole: "tab", accessibilityState: { selected: ativa, disabled: !!it.disabled }, accessibilityLabel: typeof it.label === "string" ? it.label : undefined, style: [s.abaDeTab, ativa && s.abaDeTabAtiva, it.disabled && { opacity: 0.5 }], children: typeof it.label === "string"
326
324
  ? _jsx(Text, { size: "sm", weight: ativa ? 600 : 400, style: { color: ativa ? t.color.foreground : t.color.mutedForeground }, children: it.label })
package/dist/numero.js CHANGED
@@ -49,7 +49,7 @@ import { Platform, TextInput, View } from "react-native";
49
49
  import { IconButton } from "./actions.js";
50
50
  import { criarFolha } from "./estilos.js";
51
51
  import { useCampo } from "./inputs.js";
52
- import { useAureaStrings, useAureaTokens } from "./theme.js";
52
+ import { useAureaStrings, useAureaTokens, usePeleSobreAMarca } from "./theme.js";
53
53
  const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
54
54
  const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
55
55
  const folha = criarFolha((t) => ({
@@ -235,6 +235,7 @@ const paraEdicao = (n, locale) => {
235
235
  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
236
  const t = useAureaTokens();
237
237
  const s = folha(t);
238
+ const peleDaMarca = usePeleSobreAMarca();
238
239
  const strings = useAureaStrings();
239
240
  const campo = useCampo();
240
241
  const tam = size ?? campo?.size ?? "md";
@@ -310,7 +311,7 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
310
311
  // (`aurea.css:685`), com os dois botões FORA dela. O estado que faltava é o deles; a geometria
311
312
  // continua sendo a nossa.
312
313
  const [focado, setFocado] = React.useState(false);
313
- return (_jsxs(View, { testID: testID, style: [s.grupo, fullWidth && s.grupoLargo, inativo && s.desabilitado, style], children: [mostrarBotoes && (_jsx(IconButton, { name: icons.decrement, label: strings.decrement, appearance: "ghost", size: tam, disabled: inativo || noLimite(-1), onPress: () => empurrar(-1), testID: testID ? `${testID}-menos` : undefined })), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: mostrar, onChangeText: setEmEdicao, onFocus: () => { setFocado(true); setEmEdicao(paraEdicao(numero, locale)); }, onBlur: () => { setFocado(false); confirmar(); }, editable: !inativo && !readOnly, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, keyboardType: keyboardType ?? tecladoPadrao,
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,
314
315
  // O nome vem do `Field`, como em todo controle deste pacote — e `label` cobre quem usa o
315
316
  // campo solto. Sem um dos dois, é um nó anônimo para o leitor de tela.
316
317
  accessibilityLabel: label ?? campo?.label, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, "aria-invalid": campo?.invalido, style: [
@@ -323,5 +324,10 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
323
324
  // depois. Um campo inválido que está sendo corrigido tem de mostrar que está ativo —
324
325
  // invertendo, a pessoa digita sem pista nenhuma de onde o teclado está batendo.
325
326
  focado && { borderColor: t.color.focusStrong },
327
+ // 🔴 E DENTRO DO CARTÃO DA MARCA A TINTA VENCE OS DOIS — `usePeleSobreAMarca` tem os
328
+ // números: sobre o amarelo o inválido mede 1,86 e o foco do tema escuro mede 1,00.
329
+ // O foco continua existindo, por espessura.
330
+ peleDaMarca,
331
+ peleDaMarca && focado && { borderWidth: t.size.borderWidth * 2 },
326
332
  ] }), mostrarBotoes && (_jsx(IconButton, { name: icons.increment, label: strings.increment, appearance: "ghost", size: tam, disabled: inativo || noLimite(1), onPress: () => empurrar(1), testID: testID ? `${testID}-mais` : undefined }))] }));
327
333
  }
package/dist/overlays.js CHANGED
@@ -61,7 +61,7 @@ import { Button } from "./actions.js";
61
61
  import { criarFolha } from "./estilos.js";
62
62
  import { IconButton } from "./actions.js";
63
63
  import { Text } from "./text.js";
64
- import { useAureaStrings, useAureaTokens } from "./theme.js";
64
+ import { useAureaStrings, useAureaTokens, ForaDaMarca } from "./theme.js";
65
65
  import { useReduceMotion } from "./movimento.js";
66
66
  const folha = criarFolha((t) => ({
67
67
  fundo: { flex: 1, backgroundColor: t.color.overlay },
@@ -157,7 +157,7 @@ export function Dialog({ open, title, children, footer, onClose, scroll = true,
157
157
  const s = folha(t);
158
158
  const strings = useAureaStrings();
159
159
  const Corpo = scroll ? ScrollView : View;
160
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: [s.fundo, s.centro], children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(View, { ...naoAtravessa, style: [s.superficie, style], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children }), footer ? _jsx(View, { style: s.rodape, children: footer }) : null] })] }) }));
160
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: [s.fundo, s.centro], children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(View, { ...naoAtravessa, style: [s.superficie, style], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children }), footer ? _jsx(View, { style: s.rodape, children: footer }) : null] })] }) }) }));
161
161
  }
162
162
  /**
163
163
  * A decisão que não se desfaz.
@@ -185,9 +185,9 @@ export function ConfirmDialog({ open, title, description, confirmLabel, cancelLa
185
185
  const t = useAureaTokens();
186
186
  const s = folha(t);
187
187
  const strings = useAureaStrings();
188
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onCancel, testID: testID, children: _jsx(View, { style: [s.fundo, s.centro], children: _jsxs(View, { ...naoAtravessa, style: [s.superficie, s.confirmar], children: [_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) }), _jsx(View, { style: s.corpo, children: typeof description === "string"
189
- ? _jsx(Text, { size: "md", tone: "muted", children: description })
190
- : description }), _jsxs(View, { style: s.rodape, children: [_jsx(Button, { appearance: "outline", onPress: onCancel, testID: testID ? `${testID}-cancelar` : undefined, children: cancelLabel ?? strings.confirmCancel }), _jsx(Button, { appearance: "solid", tone: destructive ? "danger" : "brand", onPress: onConfirm, testID: testID ? `${testID}-confirmar` : undefined, children: confirmLabel ?? strings.confirmProceed })] })] }) }) }));
188
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: "fade", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onCancel, testID: testID, children: _jsx(View, { style: [s.fundo, s.centro], children: _jsxs(View, { ...naoAtravessa, style: [s.superficie, s.confirmar], children: [_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) }), _jsx(View, { style: s.corpo, children: typeof description === "string"
189
+ ? _jsx(Text, { size: "md", tone: "muted", children: description })
190
+ : description }), _jsxs(View, { style: s.rodape, children: [_jsx(Button, { appearance: "outline", onPress: onCancel, testID: testID ? `${testID}-cancelar` : undefined, children: cancelLabel ?? strings.confirmCancel }), _jsx(Button, { appearance: "solid", tone: destructive ? "danger" : "brand", onPress: onConfirm, testID: testID ? `${testID}-confirmar` : undefined, children: confirmLabel ?? strings.confirmProceed })] })] }) }) }) }));
191
191
  }
192
192
  /**
193
193
  * O painel que entra pela borda.
@@ -229,10 +229,10 @@ export function Drawer({ open, title, children, onClose, side = "right", scroll
229
229
  inputRange: [0, 1],
230
230
  outputRange: [side === "right" ? 480 : -480, 0],
231
231
  });
232
- return (_jsx(Modal, { visible: open, transparent: true, animationType: "none", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, style: [
233
- s.gaveta, side === "right" ? s.gavetaDireita : s.gavetaEsquerda,
234
- { transform: [{ translateX: desloca }] }, style,
235
- ], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }));
232
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: "none", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, style: [
233
+ s.gaveta, side === "right" ? s.gavetaDireita : s.gavetaEsquerda,
234
+ { transform: [{ translateX: desloca }] }, style,
235
+ ], children: [_jsxs(View, { style: s.cabecalho, children: [_jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }), _jsx(IconButton, { name: "close", label: strings.close, appearance: "ghost", size: "sm", onPress: onClose })] }), _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }) }));
236
236
  }
237
237
  /**
238
238
  * A folha que sobe de baixo. **NÃO existe na web** — é o único componente deste lote sem ficha.
@@ -287,5 +287,5 @@ export function BottomSheet({ open, title, children, onClose, grabber = true, dr
287
287
  }).start();
288
288
  },
289
289
  }), [draggable, onClose, arrasto, reduzir, t.easing.easeEmphasized]);
290
- return (_jsx(Modal, { visible: open, transparent: true, animationType: reduzir !== false ? "none" : "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, ...gestos.panHandlers, onLayout: (e) => { altura.current = e.nativeEvent.layout.height; }, style: [s.folhaBaixo, { transform: [{ translateY: arrasto }] }, style], children: [grabber ? (_jsx(View, { style: s.puxadorArea, accessible: false, importantForAccessibility: "no-hide-descendants", children: _jsx(View, { style: s.puxador }) })) : null, title ? (_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) })) : null, _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }));
290
+ return (_jsx(ForaDaMarca, { children: _jsx(Modal, { visible: open, transparent: true, animationType: reduzir !== false ? "none" : "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: onClose, testID: testID, children: _jsxs(View, { style: s.fundo, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: onClose, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(Animated.View, { ...naoAtravessa, ...gestos.panHandlers, onLayout: (e) => { altura.current = e.nativeEvent.layout.height; }, style: [s.folhaBaixo, { transform: [{ translateY: arrasto }] }, style], children: [grabber ? (_jsx(View, { style: s.puxadorArea, accessible: false, importantForAccessibility: "no-hide-descendants", children: _jsx(View, { style: s.puxador }) })) : null, title ? (_jsx(View, { style: s.cabecalho, children: _jsx(Text, { size: "lg", weight: 600, accessibilityRole: "header", style: s.titulo, children: title }) })) : null, _jsx(Corpo, { ...(scroll ? { contentContainerStyle: s.corpo } : { style: s.corpo }), children: children })] })] }) }) }));
291
291
  }
@@ -0,0 +1,16 @@
1
+ import * as React from "react";
2
+ import { type StyleProp, type ViewStyle } from "react-native";
3
+ export interface FilaRolanteProps {
4
+ /** O conteúdo da fila — normalmente a cápsula com os itens dentro. */
5
+ children: React.ReactNode;
6
+ /** Estilo do invólucro externo, não do conteúdo. */
7
+ style?: StyleProp<ViewStyle>;
8
+ testID?: string;
9
+ }
10
+ /**
11
+ * Uma linha que rola para o lado quando não cabe, e que sabe QUANDO não cabe.
12
+ *
13
+ * O componente fica com a informação em vez de adivinhar: `onLayout` dá a largura visível e
14
+ * `onContentSizeChange` dá a do conteúdo. Com as duas, `transbordou` é uma conta, não um palpite.
15
+ */
16
+ export declare function FilaRolante({ children, style, testID }: FilaRolanteProps): React.JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // A FILA QUE ROLA — e por que ela existe em vez de cada componente resolver o seu.
3
+ //
4
+ // 🔴 **ISTO NÃO É DESENHO NOVO: É TRADUÇÃO QUE FALTOU.** O consumidor pediu em 17/09/2026 um
5
+ // grupo de opções que coubesse com seis itens, e a primeira resposta que eu ia dar era criar um
6
+ // componente de "chips". **Medindo a web antes, o pedido já estava resolvido lá:**
7
+ //
8
+ // aurea.css:2127, em tela de até 640px:
9
+ // .tabs,.pagination,.segmented { max-width:100%; overflow-x:auto; }
10
+ //
11
+ // Ou seja: **na web o segmentado e as abas JÁ rolam no telefone.** O nativo é que não traduziu.
12
+ // Criar um terceiro jeito de escolher teria sido inventar o que já existia — e essa briga tem
13
+ // nome neste repositório: o `Sidebar`.
14
+ //
15
+ // ⚠ **E o sinal de "tem mais" foi pedido junto, com razão:** quem não sabe que dá para arrastar
16
+ // não acha a sexta opção. A saída óbvia do React Native **não serve** — `fadingEdgeLength` é
17
+ // `@platform android` (medido no tipo do RN), e usá-la daria o aviso no Android e **nada no
18
+ // iPhone**. É a armadilha de um alvo só que este projeto já pagou várias vezes.
19
+ // Gradiente também não: proibido desde a Fase 0.
20
+ //
21
+ // **O que fica, e é o mesmo da web:** a opção cortada pela borda é o sinal, e ele vem do recorte.
22
+ // O que este módulo acrescenta é MEDIR se há transbordo, para (1) mostrar a barra de rolagem só
23
+ // quando ela significa alguma coisa e (2) dar ao teste um número para conferir.
24
+ // ⚠ **Declarado: se o corte LÊ bem é pergunta de vidro, e nenhum aparelho deste projeto rodou
25
+ // iOS.** O que está provado aqui é a medição, não a leitura.
26
+ import * as React from "react";
27
+ import { ScrollView } from "react-native";
28
+ /**
29
+ * Uma linha que rola para o lado quando não cabe, e que sabe QUANDO não cabe.
30
+ *
31
+ * O componente fica com a informação em vez de adivinhar: `onLayout` dá a largura visível e
32
+ * `onContentSizeChange` dá a do conteúdo. Com as duas, `transbordou` é uma conta, não um palpite.
33
+ */
34
+ export function FilaRolante({ children, style, testID }) {
35
+ const [visivel, setVisivel] = React.useState(0);
36
+ const [conteudo, setConteudo] = React.useState(0);
37
+ const transbordou = conteudo > visivel && visivel > 0;
38
+ return (_jsx(ScrollView, { horizontal: true, testID: testID, onLayout: (e) => setVisivel(e.nativeEvent.layout.width), onContentSizeChange: (w) => setConteudo(w),
39
+ // A barra só aparece quando ela quer dizer alguma coisa. Com tudo cabendo ela seria ruído,
40
+ // e é assim que o `overflow-x:auto` da web se comporta: sem transbordo, sem barra.
41
+ showsHorizontalScrollIndicator: transbordou,
42
+ // Sem isto o conteúdo estica até a largura do rolador, e o respiro da cápsula aparece como
43
+ // faixa vazia à direita quando os itens cabem.
44
+ contentContainerStyle: { flexGrow: 0 }, style: style, children: children }));
45
+ }
package/dist/text.js CHANGED
@@ -22,7 +22,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
22
22
  import * as React from "react";
23
23
  import { Text as TextRN } from "react-native";
24
24
  import { criarFolha } from "./estilos.js";
25
- import { useAureaTokens } from "./theme.js";
25
+ import { useAureaTokens, useSobreAMarca } from "./theme.js";
26
26
  const TAMANHO = {
27
27
  xs: "textXs", sm: "textSm", md: "textMd", base: "textBase", lg: "textLg",
28
28
  xl: "textXl", "2xl": "text2xl", "3xl": "text3xl", "4xl": "text4xl", "5xl": "text5xl",
@@ -61,6 +61,7 @@ const folha = criarFolha((t) => {
61
61
  export function Text({ size = "md", weight = 400, font = "ui", tone = "default", leading = "normal", italic = false, align, tracking, style, ...rest }) {
62
62
  const t = useAureaTokens();
63
63
  const s = folha(t);
64
+ const sobreAMarca = useSobreAMarca();
64
65
  const proprio = React.useMemo(() => {
65
66
  const fontSize = t.size[TAMANHO[size]] ?? t.size.textMd;
66
67
  const escala = t.font[font];
@@ -79,5 +80,13 @@ export function Text({ size = "md", weight = 400, font = "ui", tone = "default",
79
80
  ...(align ? { textAlign: align } : null),
80
81
  };
81
82
  }, [t, size, weight, font, leading, italic, align, tracking]);
82
- return _jsx(TextRN, { style: [s[tone], proprio, style], ...rest });
83
+ // 🔴 SOBRE UMA SUPERFÍCIE DA MARCA A COR É FORÇADA, e ela vence até o tom explícito.
84
+ // Medido contra o `primary` nos dois temas: texto comum dá **1,83 no escuro**, esmaecido
85
+ // **1,35**, e `danger` menos ainda. **Nenhum tom alcança os 4,5 da norma sobre o amarelo** —
86
+ // só o `primary-foreground`, com 4,54. Respeitar um `tone` ali dentro seria obedecer a prop e
87
+ // entregar texto que não se lê.
88
+ // ⚠ **A consequência está declarada e não é neutra:** dentro de um cartão da marca não existe
89
+ // texto esmaecido. A hierarquia sai de PESO e TAMANHO.
90
+ // ⚠ E `style` continua por último de propósito: quem passa cor à mão assume a conta.
91
+ return _jsx(TextRN, { style: [s[tone], sobreAMarca && { color: sobreAMarca.tinta }, proprio, style], ...rest });
83
92
  }
package/dist/theme.d.ts CHANGED
@@ -73,3 +73,66 @@ export declare function useAureaTokens(): AureaTokens;
73
73
  * `strings.ts`.
74
74
  */
75
75
  export declare function useAureaStrings(): AureaStrings;
76
+ export interface SobreAMarcaValor {
77
+ /** A única cor que se pode desenhar sobre a marca: letra, contorno e glifo. 4,54 nos dois temas. */
78
+ tinta: string;
79
+ /** A cor do cartão por baixo. Serve de letra para quem se pinta com a tinta INTEIRA. */
80
+ fundo: string;
81
+ }
82
+ /** O provedor. Quem usa é o `Card variant="brand"`. */
83
+ export declare const SobreAMarca: React.Context<SobreAMarcaValor | null>;
84
+ /**
85
+ * O par de cores obrigatório quando se está sobre a marca, ou `null` quando não se está.
86
+ *
87
+ * ⚠ **Ele vence TODOS os tons, inclusive os explícitos.** Um `tone="danger"` sobre o amarelo
88
+ * mediria menos que os 4,5 da norma, então respeitá-lo entregaria texto ilegível em nome da
89
+ * obediência. **Sobre o amarelo existe UMA tinta** — a hierarquia sai de peso e tamanho.
90
+ *
91
+ * ⚠ **E o alcance dele é DECLARADO, não completo.** Leem o contexto: `Text`, a família de campo,
92
+ * o `Button` e o `IconButton`. **Componente fora dessa lista continua quebrado dentro do cartão
93
+ * amarelo**, e gate estático nenhum vê isso — o que o consumidor encaixa não está no nosso
94
+ * código, exatamente o ponto cego que o `check 43` já declara.
95
+ */
96
+ export declare function useSobreAMarca(): SobreAMarcaValor | null;
97
+ /**
98
+ * Corta a marca para dentro.
99
+ *
100
+ * 🔴 **Existe por uma armadilha do React Native que é invisível na tela e óbvia no código:** o
101
+ * `Modal` desenha numa camada POR CIMA de tudo, mas em React ele continua sendo FILHO de quem o
102
+ * escreveu. Um `Select` dentro de um `Card variant="brand"` abre a folha dele **fora** do cartão
103
+ * amarelo — e a folha herdaria a tinta do amarelo mesmo assim, pintando texto marrom sobre fundo
104
+ * normal.
105
+ *
106
+ * ⚠ **Camada visual e árvore de contexto são coisas diferentes**, e é só o segundo que manda na
107
+ * cor. Toda superfície que sai do fluxo — as duas folhas de escolha e os quatro sobrepostos —
108
+ * passa por aqui.
109
+ */
110
+ export declare function ForaDaMarca({ children }: {
111
+ children?: React.ReactNode;
112
+ }): React.JSX.Element;
113
+ /**
114
+ * A pele que um CAMPO tem de vestir quando está dentro de um `Card variant="brand"`, ou `null`
115
+ * quando não está.
116
+ *
117
+ * O campo perde o fundo e passa a ser um contorno da tinta: **4,54 contra o amarelo nos dois
118
+ * temas**, contra os 2,43/2,24 do `borderStrong` e os 1,61 do `fieldBg` no tema claro. A norma
119
+ * 1.4.11 pede 3:1 para o contorno que identifica um campo, e só a tinta chega lá.
120
+ *
121
+ * ⚠ **O fundo some porque NÃO HÁ fundo possível** — medidas as três superfícies neutras do
122
+ * sistema contra o amarelo no tema claro: `card` 1,91 · `background` 1,73 · `secondary` 1,61.
123
+ * Nenhuma separa. Um contorno forte separa.
124
+ *
125
+ * 🔴 **E a tinta vence o estado de INVÁLIDO, que é o contrário do que a intuição manda.** Medido:
126
+ * a borda de inválido (`danger-400`, `#9f2330`) mede **1,86** contra o amarelo — ela é MENOS
127
+ * visível que a tinta. Pintar de vermelho ali deixaria o campo errado mais difícil de achar que o
128
+ * campo certo. Dentro deste cartão o erro é dito pela MENSAGEM do `Field` e pelo estado que o
129
+ * leitor de tela anuncia, não pela cor — que é o que a norma 1.4.1 já exige de qualquer forma.
130
+ *
131
+ * ⚠ **O texto digitado e o marcador de dica ficam da MESMA cor**, porque só existe uma tinta. É
132
+ * perda real de hierarquia, declarada e não escondida: sobre o amarelo não há segunda cor.
133
+ */
134
+ export declare function usePeleSobreAMarca(): {
135
+ borderColor: string;
136
+ backgroundColor: string;
137
+ color: string;
138
+ } | null;
package/dist/theme.js CHANGED
@@ -109,3 +109,64 @@ export function useAureaTokens() {
109
109
  export function useAureaStrings() {
110
110
  return usar("useAureaStrings").strings;
111
111
  }
112
+ const SobreAMarcaCtx = React.createContext(null);
113
+ /** O provedor. Quem usa é o `Card variant="brand"`. */
114
+ export const SobreAMarca = SobreAMarcaCtx;
115
+ /**
116
+ * O par de cores obrigatório quando se está sobre a marca, ou `null` quando não se está.
117
+ *
118
+ * ⚠ **Ele vence TODOS os tons, inclusive os explícitos.** Um `tone="danger"` sobre o amarelo
119
+ * mediria menos que os 4,5 da norma, então respeitá-lo entregaria texto ilegível em nome da
120
+ * obediência. **Sobre o amarelo existe UMA tinta** — a hierarquia sai de peso e tamanho.
121
+ *
122
+ * ⚠ **E o alcance dele é DECLARADO, não completo.** Leem o contexto: `Text`, a família de campo,
123
+ * o `Button` e o `IconButton`. **Componente fora dessa lista continua quebrado dentro do cartão
124
+ * amarelo**, e gate estático nenhum vê isso — o que o consumidor encaixa não está no nosso
125
+ * código, exatamente o ponto cego que o `check 43` já declara.
126
+ */
127
+ export function useSobreAMarca() {
128
+ return React.useContext(SobreAMarcaCtx);
129
+ }
130
+ /**
131
+ * Corta a marca para dentro.
132
+ *
133
+ * 🔴 **Existe por uma armadilha do React Native que é invisível na tela e óbvia no código:** o
134
+ * `Modal` desenha numa camada POR CIMA de tudo, mas em React ele continua sendo FILHO de quem o
135
+ * escreveu. Um `Select` dentro de um `Card variant="brand"` abre a folha dele **fora** do cartão
136
+ * amarelo — e a folha herdaria a tinta do amarelo mesmo assim, pintando texto marrom sobre fundo
137
+ * normal.
138
+ *
139
+ * ⚠ **Camada visual e árvore de contexto são coisas diferentes**, e é só o segundo que manda na
140
+ * cor. Toda superfície que sai do fluxo — as duas folhas de escolha e os quatro sobrepostos —
141
+ * passa por aqui.
142
+ */
143
+ export function ForaDaMarca({ children }) {
144
+ return _jsx(SobreAMarcaCtx.Provider, { value: null, children: children });
145
+ }
146
+ /**
147
+ * A pele que um CAMPO tem de vestir quando está dentro de um `Card variant="brand"`, ou `null`
148
+ * quando não está.
149
+ *
150
+ * O campo perde o fundo e passa a ser um contorno da tinta: **4,54 contra o amarelo nos dois
151
+ * temas**, contra os 2,43/2,24 do `borderStrong` e os 1,61 do `fieldBg` no tema claro. A norma
152
+ * 1.4.11 pede 3:1 para o contorno que identifica um campo, e só a tinta chega lá.
153
+ *
154
+ * ⚠ **O fundo some porque NÃO HÁ fundo possível** — medidas as três superfícies neutras do
155
+ * sistema contra o amarelo no tema claro: `card` 1,91 · `background` 1,73 · `secondary` 1,61.
156
+ * Nenhuma separa. Um contorno forte separa.
157
+ *
158
+ * 🔴 **E a tinta vence o estado de INVÁLIDO, que é o contrário do que a intuição manda.** Medido:
159
+ * a borda de inválido (`danger-400`, `#9f2330`) mede **1,86** contra o amarelo — ela é MENOS
160
+ * visível que a tinta. Pintar de vermelho ali deixaria o campo errado mais difícil de achar que o
161
+ * campo certo. Dentro deste cartão o erro é dito pela MENSAGEM do `Field` e pelo estado que o
162
+ * leitor de tela anuncia, não pela cor — que é o que a norma 1.4.1 já exige de qualquer forma.
163
+ *
164
+ * ⚠ **O texto digitado e o marcador de dica ficam da MESMA cor**, porque só existe uma tinta. É
165
+ * perda real de hierarquia, declarada e não escondida: sobre o amarelo não há segunda cor.
166
+ */
167
+ export function usePeleSobreAMarca() {
168
+ const marca = useSobreAMarca();
169
+ return marca == null
170
+ ? null
171
+ : { borderColor: marca.tinta, backgroundColor: "transparent", color: marca.tinta };
172
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurea-uds/native",
3
- "version": "0.8.5",
3
+ "version": "0.8.7",
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.5"
50
+ "@aurea-uds/tokens": "^0.8.7"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@react-native-community/datetimepicker": ">=8",