@aurea-uds/native 0.8.0 → 0.8.2

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.d.ts CHANGED
@@ -3,7 +3,7 @@ import { type PressableProps } from "react-native";
3
3
  import { type AureaIconRegistry, type IconName } from "./icon.js";
4
4
  /** Quanto peso a caixa tem. */
5
5
  export type AureaButtonAppearance = "solid" | "outline" | "ghost";
6
- /** O que a cor significa. Mesmos nomes da web (ADR-0016). */
6
+ /** O que a cor significa. Mesmos nomes da web (ADR-0044). */
7
7
  export type AureaButtonTone = "neutral" | "brand" | "danger" | "success" | "warning" | "info";
8
8
  /** As cinco alturas de controle, todas de token de densidade. */
9
9
  export type AureaButtonSize = "xs" | "sm" | "md" | "lg" | "xl";
@@ -14,6 +14,26 @@ export interface ButtonProps extends Omit<PressableProps, "children" | "style">
14
14
  size?: AureaButtonSize;
15
15
  leadingIcon?: IconName;
16
16
  trailingIcon?: IconName;
17
+ /**
18
+ * Um desenho QUALQUER na frente do texto, para quando a marca não pode ser um ícone nosso.
19
+ *
20
+ * ⚠ **Ele existe por causa de MARCA REGISTRADA, não por conveniência.** O botão "Entrar com
21
+ * Google" e o "Entrar com Apple" exigem o desenho oficial de cada um, e nenhum dos dois pode
22
+ * viver dentro desta biblioteca: o Google proíbe redesenhar e manda usar o arquivo do pacote
23
+ * dele; a Apple proíbe usar o logo sem licença escrita. **Então a marca entra pelo app**, e o
24
+ * que a Aurea dá é a cápsula em volta.
25
+ *
26
+ * <Button appearance="outline" leading={<RNImage source={logoGoogle} style={{width: 18, height: 18}} />}>
27
+ * Entrar com Google
28
+ * </Button>
29
+ *
30
+ * ⚠ **A cor do texto NÃO atravessa para cá** — o que entra desenha a própria cor, e é assim
31
+ * que tem de ser: a marca do Google tem cor fixa, e tingi-la seria justamente o que a regra
32
+ * dele proíbe. Um `leadingIcon` nosso continua herdando a cor do botão.
33
+ */
34
+ leading?: React.ReactNode;
35
+ /** O mesmo, do outro lado. */
36
+ trailing?: React.ReactNode;
17
37
  /** Registro de ícones, quando não há um em contexto. Ver `criarRegistroDeIcones`. */
18
38
  icons?: AureaIconRegistry;
19
39
  fullWidth?: boolean;
@@ -29,7 +49,7 @@ export interface ButtonProps extends Omit<PressableProps, "children" | "style">
29
49
  * descreve: lá, um botão desabilitado some da ordem de foco e a explicação pendurada nele não é
30
50
  * lida por ninguém. Aqui não há o dilema, então não há o par `aria-disabled` — um só basta.
31
51
  */
32
- export declare function Button({ children, appearance, tone, size, leadingIcon, trailingIcon, icons, fullWidth, pressed, disabled, accessibilityLabel, ...rest }: ButtonProps): React.JSX.Element;
52
+ export declare function Button({ children, appearance, tone, size, leadingIcon, trailingIcon, leading, trailing, icons, fullWidth, pressed, disabled, accessibilityLabel, ...rest }: ButtonProps): React.JSX.Element;
33
53
  export interface IconButtonProps extends Omit<ButtonProps, "children" | "leadingIcon" | "trailingIcon" | "fullWidth"> {
34
54
  name: IconName;
35
55
  /** **Obrigatório**: um botão que só tem glifo não tem texto para o leitor de tela anunciar. */
package/dist/actions.js CHANGED
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  // Medida a ficha da web antes de escrever (passo 1 do BUILDING.md). Lá existem TRÊS props que
6
6
  // pintam a mesma coisa, e a própria ficha diz qual é qual: *"`variant` é atalho para o par
7
7
  // (appearance, tone). Os treze nomes continuam valendo e pintam exatamente o mesmo; a API de dois
8
- // eixos é appearance + tone"* — a ADR-0016.
8
+ // eixos é appearance + tone"* — a ADR-0044.
9
9
  //
10
10
  // O `variant` existe lá porque a Aurea **está publicada** e aqueles treze nomes são contrato com
11
11
  // quem instalou. **Aqui não há nada publicado**, e nascer com o atalho seria nascer com a dívida:
@@ -88,7 +88,7 @@ const folha = criarFolha((t) => ({
88
88
  * descreve: lá, um botão desabilitado some da ordem de foco e a explicação pendurada nele não é
89
89
  * lida por ninguém. Aqui não há o dilema, então não há o par `aria-disabled` — um só basta.
90
90
  */
91
- export function Button({ children, appearance = "solid", tone = "neutral", size = "md", leadingIcon, trailingIcon, icons, fullWidth = false, pressed, disabled, accessibilityLabel, ...rest }) {
91
+ export function Button({ children, appearance = "solid", tone = "neutral", size = "md", leadingIcon, trailingIcon, leading, trailing, icons, fullWidth = false, pressed, disabled, accessibilityLabel, ...rest }) {
92
92
  const t = useAureaTokens();
93
93
  const s = folha(t);
94
94
  const cor = pintar(t, tone);
@@ -104,9 +104,9 @@ export function Button({ children, appearance = "solid", tone = "neutral", size
104
104
  return (_jsx(Pressable, { disabled: disabled, accessibilityRole: "button", accessibilityLabel: accessibilityLabel, accessibilityState: { disabled: !!disabled, ...(pressed === undefined ? null : { checked: pressed }) }, style: ({ pressed: tocando }) => [
105
105
  s.alvo, fullWidth && s.alvoLargura,
106
106
  tocando && s.pressionado, disabled && s.inerte,
107
- ], ...rest, children: _jsxs(View, { style: [s.caixa, caixa], children: [leadingIcon ? _jsx(Icon, { name: leadingIcon, size: ICONE[size], color: corDoTexto, icons: icons }) : null, typeof children === "string"
107
+ ], ...rest, children: _jsxs(View, { style: [s.caixa, caixa], children: [leading ?? null, leadingIcon ? _jsx(Icon, { name: leadingIcon, size: ICONE[size], color: corDoTexto, icons: icons }) : null, typeof children === "string"
108
108
  ? _jsx(Text, { size: FONTE[size], weight: 500, leading: "none", style: { color: corDoTexto }, children: children })
109
- : children, trailingIcon ? _jsx(Icon, { name: trailingIcon, size: ICONE[size], color: corDoTexto, icons: icons }) : null] }) }));
109
+ : children, trailingIcon ? _jsx(Icon, { name: trailingIcon, size: ICONE[size], color: corDoTexto, icons: icons }) : null, trailing ?? null] }) }));
110
110
  }
111
111
  /**
112
112
  * Botão quadrado, só glifo.
package/dist/busca.d.ts CHANGED
@@ -46,6 +46,13 @@ export interface ComboboxProps {
46
46
  empty?: React.ReactNode;
47
47
  /** Mostra o botão de limpar no gatilho quando há escolha. Padrão **true**. */
48
48
  clearable?: boolean;
49
+ /**
50
+ * Arrastar o cabeçalho da folha para baixo fecha. Ligado.
51
+ *
52
+ * ⚠ **É a ÚNICA saída por gesto no iOS** — lá não há botão VOLTAR, então sem isto restaria só
53
+ * tocar no fundo. Mesmo gesto, mesmo limiar e mesma curva do `BottomSheet` do Lote 5.
54
+ */
55
+ draggable?: boolean;
49
56
  disabled?: boolean;
50
57
  size?: AureaFieldSize;
51
58
  /** O glifo da seta. Registre-o, ou passe `false`. */
@@ -84,7 +91,7 @@ export interface ComboboxProps {
84
91
  * na web e pela mesma razão do achado I1 da auditoria: um componente com a sua própria cópia do
85
92
  * estado é a segunda fonte de verdade que ninguém sabe que existe.
86
93
  */
87
- export declare function Combobox({ items, value, onValueChange, onSearchChange, searchDelay, loading, onEndReached, placeholder, searchPlaceholder, empty, clearable, disabled, size, chevron, searchIcon, style, testID, }: ComboboxProps): React.JSX.Element;
94
+ export declare function Combobox({ items, value, onValueChange, onSearchChange, searchDelay, loading, onEndReached, placeholder, searchPlaceholder, empty, clearable, draggable, disabled, size, chevron, searchIcon, style, testID, }: ComboboxProps): React.JSX.Element;
88
95
  export interface SearchFieldProps {
89
96
  value?: string;
90
97
  onChangeText?: (v: string) => void;
package/dist/busca.js CHANGED
@@ -67,12 +67,13 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
67
67
  // aceita digitação, então `button` é o que ele É nos dois sistemas. Quem digita é o campo da
68
68
  // folha, e ele leva `search` — que mapeia nos dois.
69
69
  import * as React from "react";
70
- import { FlatList, Modal, Pressable, TextInput, View, } from "react-native";
70
+ import { Animated, Easing, FlatList, Modal, PanResponder, Pressable, TextInput, View, } from "react-native";
71
71
  import { IconButton } from "./actions.js";
72
72
  import { criarFolha } from "./estilos.js";
73
73
  import { Spinner } from "./feedback.js";
74
74
  import { Icon } from "./icon.js";
75
- import { useCampo } from "./inputs.js";
75
+ import { KeyboardAvoiding, useCampo } from "./inputs.js";
76
+ import { useReduceMotion } from "./movimento.js";
76
77
  import { Text } from "./text.js";
77
78
  import { useAureaStrings, useAureaTokens } from "./theme.js";
78
79
  // As mesmas contas do `inputs.tsx`, e elas são repetidas AQUI de propósito: exportá-las de lá
@@ -80,7 +81,23 @@ import { useAureaStrings, useAureaTokens } from "./theme.js";
80
81
  // módulos irmãos é o começo de um acoplamento que ninguém declara. São três linhas.
81
82
  const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
82
83
  const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
84
+ // ⚠ O `13` do tamanho `md` NÃO é número inventado: é o `padding-inline` literal do `.input`
85
+ // (`aurea.css:685`), e é o mesmo valor que o `inputs.tsx:60` usa. Não há token de 13 — a escala
86
+ // tem `space3`=12 e `space4`=16 —, e é por isso que o CSS o escreve cru dos dois lados.
83
87
  const respiroDoTamanho = (t, s) => s === "sm" ? t.size.space3 : s === "lg" ? t.size.space4 : 13;
88
+ // ── AS DUAS CONSTANTES DE INTERAÇÃO, e elas NÃO são geometria ────────────────────────────────
89
+ // Ficam nomeadas e no topo porque número solto no meio do componente é o que o `CLAUDE.md`
90
+ // passou a proibir em 12/09/2026. **Nenhuma das duas tem linha de `aurea.css` atrás, e não
91
+ // deveria ter:** uma é tempo de digitação, a outra é fração de viewport do motor de lista.
92
+ //
93
+ // 250 ms é o intervalo entre teclas de quem digita corrido. Abaixo disso a espera não junta
94
+ // letra nenhuma e cada tecla vira uma ida ao servidor; muito acima, a lista demora a reagir e a
95
+ // pessoa acha que o campo travou. `searchDelay` existe para o app discordar.
96
+ const ESPERA_PADRAO = 250;
97
+ // Fração da altura visível que sobra abaixo antes de pedir a próxima página. Meia tela de
98
+ // antecedência: menos que isso e a lista chega ao fim antes da resposta; mais, e o app pagina
99
+ // páginas que ninguém vai ver. É prop do `FlatList`, não medida de desenho.
100
+ const ANTECEDENCIA_DE_PAGINA = 0.5;
84
101
  const folha = criarFolha((t) => ({
85
102
  // O gatilho é o `.input` da web (`aurea.css:685`), igual ao do `Select` — mesma altura, mesma
86
103
  // borda, o mesmo raio de controle.
@@ -94,20 +111,28 @@ const folha = criarFolha((t) => ({
94
111
  desabilitado: { opacity: 0.5 },
95
112
  // As ações do gatilho — limpar e a seta. O `.combobox-actions` da web (`aurea.css:1673`) é
96
113
  // absoluto sobre o campo; aqui é um irmão na linha, porque não há campo por baixo.
97
- acoes: { flexDirection: "row", alignItems: "center", gap: 2 },
114
+ // `space05` mede 2 — o mesmo valor, agora com nome. O `.combobox-actions` da web
115
+ // (`aurea.css:1673`) usa `gap:2px` literal; aqui o token existe, então é ele que entra.
116
+ acoes: { flexDirection: "row", alignItems: "center", gap: t.size.space05 },
98
117
  // A folha, com a mesma anatomia da do `Select` (`inputs.tsx:98-113`) — e é de propósito que
99
118
  // sejam iguais: são o mesmo gesto, e duas folhas diferentes para o mesmo gesto é como uma
100
119
  // biblioteca deixa de ter linguagem.
101
120
  fundoDaLista: { flex: 1, justifyContent: "flex-end", backgroundColor: "#00000080" },
102
121
  fundoDeToque: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0 },
122
+ // 🔴 OS 85% E OS 45% ERAM INVENTADOS, e saíram em 12/09/2026. **A casa já tinha a medida:**
123
+ // o `folhaBaixo` do `BottomSheet` do Lote 5 (`overlays.tsx:104`) usa `maxHeight: "90%"`, e
124
+ // duas folhas que sobem do rodapé com alturas diferentes é como uma biblioteca deixa de ter
125
+ // linguagem. O `minHeight` sumiu: folha abraça o conteúdo, e uma altura mínima só servia para
126
+ // desenhar vazio.
103
127
  lista: {
104
- // `maxHeight` MAIOR que a do `Select` (60%), e a razão é o teclado: aqui ele sobe junto, e
105
- // uma folha de 60% com teclado aberto deixa três linhas visíveis. O `KeyboardAvoidingView`
106
- // não entra — dentro de um `Modal` do RN ele não recebe as métricas do teclado no Android.
107
- maxHeight: "85%", minHeight: "45%",
128
+ maxHeight: "90%",
108
129
  borderTopLeftRadius: t.size.radiusCard, borderTopRightRadius: t.size.radiusCard,
109
130
  backgroundColor: t.color.popover, borderWidth: t.size.borderWidth, borderColor: t.color.border,
110
131
  },
132
+ // O puxador, copiado do `BottomSheet` (`overlays.tsx:111-112`) — mesma medida, mesmo token.
133
+ puxadorArea: { alignItems: "center", paddingVertical: t.size.space2 },
134
+ puxador: { width: 40, height: 4, borderRadius: t.size.radiusFull,
135
+ backgroundColor: t.color.borderStrong },
111
136
  // O campo de digitar, no topo da folha. É o `.input-group` da web (`aurea.css:741`): a borda
112
137
  // mora no GRUPO e o campo dentro dele é transparente e sem borda.
113
138
  grupo: {
@@ -160,7 +185,7 @@ const folha = criarFolha((t) => ({
160
185
  * na web e pela mesma razão do achado I1 da auditoria: um componente com a sua própria cópia do
161
186
  * estado é a segunda fonte de verdade que ninguém sabe que existe.
162
187
  */
163
- export function Combobox({ items, value, onValueChange, onSearchChange, searchDelay = 250, loading, onEndReached, placeholder, searchPlaceholder, empty, clearable = true, disabled, size, chevron = "chevron--down", searchIcon = "search", style, testID, }) {
188
+ 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, }) {
164
189
  const t = useAureaTokens();
165
190
  const s = folha(t);
166
191
  const strings = useAureaStrings();
@@ -169,6 +194,7 @@ export function Combobox({ items, value, onValueChange, onSearchChange, searchDe
169
194
  const inativo = disabled ?? campo?.disabled;
170
195
  const [aberto, setAberto] = React.useState(false);
171
196
  const [texto, setTexto] = React.useState("");
197
+ const reduzir = useReduceMotion();
172
198
  // ── A ESPERA ────────────────────────────────────────────────────────────────────────────────
173
199
  // O timer vive num `ref` e é limpo em três lugares: à digitação seguinte, ao fechar a folha e
174
200
  // ao desmontar. O terceiro é o que importa e o que mais se esquece — sem ele, sair da tela com
@@ -206,6 +232,35 @@ export function Combobox({ items, value, onValueChange, onSearchChange, searchDe
206
232
  setAberto(false);
207
233
  setTexto("");
208
234
  }, [cancelar]);
235
+ // ── O ARRASTO, copiado do `BottomSheet` do Lote 5 (`overlays.tsx:427-451`) ──────────────────
236
+ // Os números são os DE LÁ, não novos: 6dp para reivindicar, limiar de 80dp ou um terço da
237
+ // altura, velocidade 1.2. Copiar em vez de reinventar é o que mantém as duas folhas com o
238
+ // mesmo tato — e é a regra do `BUILDING.md` sobre não criar escala nova no meio de um
239
+ // componente.
240
+ const arrasto = React.useRef(new Animated.Value(0)).current;
241
+ const altura = React.useRef(0);
242
+ React.useEffect(() => { if (!aberto)
243
+ arrasto.setValue(0); }, [aberto, arrasto]);
244
+ const gestos = React.useMemo(() => PanResponder.create({
245
+ onMoveShouldSetPanResponder: (_e, g) => draggable && g.dy > 6,
246
+ onPanResponderMove: (_e, g) => { if (g.dy > 0)
247
+ arrasto.setValue(g.dy); },
248
+ onPanResponderRelease: (_e, g) => {
249
+ const limiar = Math.max(80, altura.current / 3);
250
+ if (g.dy > limiar || g.vy > 1.2) {
251
+ fechar();
252
+ return;
253
+ }
254
+ if (reduzir !== false) {
255
+ arrasto.setValue(0);
256
+ return;
257
+ }
258
+ Animated.timing(arrasto, {
259
+ toValue: 0, duration: t.size.durationSlow, useNativeDriver: true,
260
+ easing: Easing.bezier(...t.easing.easeEmphasized),
261
+ }).start();
262
+ },
263
+ }), [draggable, fechar, arrasto, reduzir, t.size.durationSlow, t.easing.easeEmphasized]);
209
264
  // ── O FILTRO, QUANDO ELE É NOSSO ────────────────────────────────────────────────────────────
210
265
  const filtrados = React.useMemo(() => {
211
266
  if (onSearchChange)
@@ -222,7 +277,7 @@ export function Combobox({ items, value, onValueChange, onSearchChange, searchDe
222
277
  campo?.invalido && s.invalido,
223
278
  inativo && s.desabilitado,
224
279
  style,
225
- ], 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(Icon, { name: chevron, size: "sm", color: t.color.subtleForeground })] })] }), _jsx(Modal, { visible: aberto, transparent: true, animationType: "slide", statusBarTranslucent: true, navigationBarTranslucent: true, onRequestClose: fechar, children: _jsxs(View, { style: s.fundoDaLista, children: [_jsx(Pressable, { style: s.fundoDeToque, onPress: fechar, accessible: false, testID: testID ? `${testID}-fundo` : undefined }), _jsxs(View, { style: s.lista, onStartShouldSetResponder: () => true, children: [_jsxs(View, { style: [s.grupo, { height: alturaDoTamanho(t, tam) }], 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,
280
+ ], 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(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,
226
281
  // ⚠ `autoFocus` é o que faz a folha valer a pena: abrir um campo de busca e
227
282
  // exigir um segundo toque para o teclado subir é um toque a mais em cada
228
283
  // cadastro. O foco entra com a folha; o teclado vem junto.
@@ -238,7 +293,7 @@ export function Combobox({ items, value, onValueChange, onSearchChange, searchDe
238
293
  color: t.color.foreground },
239
294
  ] }), 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
240
295
  ? _jsx(Text, { size: "sm", tone: "muted", children: empty ?? strings.comboboxEmpty })
241
- : empty })), _jsx(FlatList, { data: filtrados, keyExtractor: (i) => i.value, keyboardShouldPersistTaps: "handled", onEndReached: onEndReached, onEndReachedThreshold: 0.5, renderItem: ({ item }) => (_jsx(Pressable, { disabled: item.disabled, onPress: () => { onValueChange?.(item); fechar(); }, accessibilityRole: "menuitem", accessibilityState: {
296
+ : 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: {
242
297
  selected: item.value === value?.value, disabled: !!item.disabled,
243
298
  }, style: [
244
299
  s.opcao,
@@ -283,7 +338,7 @@ const dobrar = (v) => {
283
338
  * ⚠ **A borda mora no GRUPO, não no campo** — é o `.input-group` da web (`aurea.css:741`), e é o
284
339
  * que deixa lupa, campo e botão de limpar dentro de uma caixa só em vez de três.
285
340
  */
286
- export function SearchField({ value, onChangeText, onSearchChange, searchDelay = 250, placeholder, disabled, size, icon = "search", clearable = true, onSubmit, style, testID, }) {
341
+ export function SearchField({ value, onChangeText, onSearchChange, searchDelay = ESPERA_PADRAO, placeholder, disabled, size, icon = "search", clearable = true, onSubmit, style, testID, }) {
287
342
  const t = useAureaTokens();
288
343
  const s = folha(t);
289
344
  const strings = useAureaStrings();
package/dist/display.d.ts CHANGED
@@ -10,7 +10,7 @@ export declare const formatarContagem: (count: number, max?: number) => string;
10
10
  export interface BadgeProps extends ViewProps {
11
11
  /**
12
12
  * O tom. Chama-se `tone` e não `variant`, como no `Button` do Lote 1 — o vocabulário de cor
13
- * com significado é `tone` em todo o alvo nativo (ADR-0016 é quem separa os dois na web).
13
+ * com significado é `tone` em todo o alvo nativo (ADR-0044 é quem separa os dois na web).
14
14
  */
15
15
  tone?: AureaBadgeTone;
16
16
  emphasis?: AureaBadgeEmphasis;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ export { BottomNav, Topbar, NavList, Stepper } from "./navigation.js";
25
25
  export type { BottomNavProps, TopbarProps, NavListProps, StepperProps, AureaNavItem, AureaNavListItem, AureaStepItem, AureaStepState, AureaBottomNavVariant, AureaBottomNavIndicator, AureaTopbarVariant, } from "./navigation.js";
26
26
  export { Field, Label, Input, Textarea, Select, Switch, Checkbox, Radio, SegmentedControl, Form, KeyboardAvoiding, useCampo, } from "./inputs.js";
27
27
  export type { FieldProps, LabelProps, InputProps, TextareaProps, SelectProps, SwitchProps, CheckboxProps, RadioProps, SegmentedControlProps, FormProps, KeyboardAvoidingProps, AureaFieldSize, } from "./inputs.js";
28
+ export { InputGroup, InputGroupAddon, PasswordField, useGrupoDeCampo } from "./inputs.js";
29
+ export type { InputGroupProps, InputGroupAddonProps, PasswordFieldProps } from "./inputs.js";
28
30
  export { Dialog, ConfirmDialog, Drawer, BottomSheet } from "./overlays.js";
29
31
  export type { DialogProps, ConfirmDialogProps, DrawerProps, BottomSheetProps, AureaDrawerSide, } from "./overlays.js";
30
32
  export { ToastHost, Toast, useToast } from "./toast.js";
package/dist/index.js CHANGED
@@ -54,6 +54,11 @@ export { BottomNav, Topbar, NavList, Stepper } from "./navigation.js";
54
54
  // nome, a dica e o estado de inválido para o controle por CONTEXTO. É a tradução que decidiu o
55
55
  // lote — o RN não tem `id` nem `htmlFor`.
56
56
  export { Field, Label, Input, Textarea, Select, Switch, Checkbox, Radio, SegmentedControl, Form, KeyboardAvoiding, useCampo, } from "./inputs.js";
57
+ // ⚠ O GRUPO e o campo de SENHA entraram depois do Lote 4, medidos pelo app na tela de entrar
58
+ // (15/09/2026): o campo não tinha onde encaixar glifo, e `secureTextEntry` esconde a senha sem
59
+ // dar botão para mostrá-la. **Os dois já existiam na web desde 29/08/2026** — a falta era só
60
+ // deste lado, e o desenho daqui é o de lá traduzido, não um desenho novo.
61
+ export { InputGroup, InputGroupAddon, PasswordField, useGrupoDeCampo } from "./inputs.js";
57
62
  // ⚠ `DatePicker` e `PhotoInput` NÃO saem daqui, e é de propósito: eles vivem em
58
63
  // `@aurea-uds/native/system`, pela mesma razão que os ícones vivem em `./icons/*` (ADR-0038).
59
64
  // Sair por este barril traria os dois módulos NATIVOS ao grafo de todo app — inclusive de quem
package/dist/inputs.d.ts CHANGED
@@ -11,6 +11,14 @@ type ContextoDeCampo = {
11
11
  };
12
12
  /** O que os controles leem do `Field` acima deles. Público porque um controle novo vai precisar. */
13
13
  export declare function useCampo(): ContextoDeCampo | null;
14
+ interface ContextoDeGrupo {
15
+ size: AureaFieldSize;
16
+ disabled: boolean;
17
+ /** O campo avisa o grupo quando ganha e perde o foco — no RN não existe `:focus-within`. */
18
+ aoFocar: (v: boolean) => void;
19
+ }
20
+ /** O grupo em volta do campo, quando existe um. `null` quando o campo está sozinho. */
21
+ export declare function useGrupoDeCampo(): ContextoDeGrupo | null;
14
22
  export interface FieldProps extends ViewProps {
15
23
  label?: string;
16
24
  hint?: React.ReactNode;
@@ -120,6 +128,53 @@ interface ControleProps {
120
128
  style?: StyleProp<ViewStyle>;
121
129
  testID?: string;
122
130
  }
131
+ export interface InputGroupProps extends ViewProps {
132
+ /** O degrau do sistema. Cai no `Field` em volta quando não vem, como no `Input`. */
133
+ size?: AureaFieldSize;
134
+ disabled?: boolean;
135
+ testID?: string;
136
+ }
137
+ /**
138
+ * A caixa de campo que aceita coisas dentro — glifo na frente, botão atrás, os dois.
139
+ *
140
+ * <InputGroup>
141
+ * <InputGroupAddon><Icon name="email" size="sm" /></InputGroupAddon>
142
+ * <Input value={email} onChangeText={setEmail} keyboardType="email-address" />
143
+ * </InputGroup>
144
+ *
145
+ * ⚠ **O `Input` lá dentro para de desenhar a própria caixa** — quem desenha borda, fundo, altura,
146
+ * foco e inválido passa a ser o grupo. Isso é automático: o campo enxerga o grupo por contexto.
147
+ */
148
+ export declare function InputGroup({ size, disabled, children, style, testID, ...rest }: InputGroupProps): React.JSX.Element;
149
+ export interface InputGroupAddonProps extends ViewProps {
150
+ testID?: string;
151
+ }
152
+ /**
153
+ * Um encaixe dentro do `InputGroup`. **A posição é a do JSX** — antes do campo fica na frente,
154
+ * depois fica atrás. Não há prop de lado, e o porquê está no bloco do grupo lá em cima.
155
+ */
156
+ export declare function InputGroupAddon({ children, style, testID, ...rest }: InputGroupAddonProps): React.JSX.Element;
157
+ /**
158
+ * O campo de senha, com o botão que mostra e esconde.
159
+ *
160
+ * <Field label="Senha"><PasswordField value={senha} onChangeText={setSenha} /></Field>
161
+ *
162
+ * ⚠ **`secureTextEntry` não entra**: um campo de senha que aceitasse essa prop deixaria de ser
163
+ * um. É a mesma razão pela qual o `type` não atravessa no `PasswordField` da web.
164
+ *
165
+ * ⚠ **O rótulo do botão é o anúncio do estado** — ele troca entre "Mostrar senha" e "Ocultar
166
+ * senha". Não há estado de pressionado junto, e isso é decisão copiada da web: com os dois, o
167
+ * leitor de tela lê a mesma informação duas vezes.
168
+ *
169
+ * ⚠ **`autoCapitalize` nasce em `none`.** No Android o teclado maiúsculiza a primeira letra por
170
+ * padrão, e numa senha isso é o erro de digitação mais caro que existe: o campo está escondido,
171
+ * então ninguém vê o que deu errado.
172
+ */
173
+ export interface PasswordFieldProps extends Omit<InputProps, "secureTextEntry" | "multiline" | "keyboardType"> {
174
+ /** Começa visível. Padrão: escondida. */
175
+ defaultVisible?: boolean;
176
+ }
177
+ export declare function PasswordField({ defaultVisible, size, disabled, autoCapitalize, testID, style, ...rest }: PasswordFieldProps): React.JSX.Element;
123
178
  export type CheckboxProps = ControleProps;
124
179
  export type RadioProps = ControleProps;
125
180
  /** A caixa que marca. `accessibilityRole="checkbox"` + `state.checked`. */
package/dist/inputs.js CHANGED
@@ -40,10 +40,11 @@ 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 { criarFolha } from "./estilos.js";
43
+ import { IconButton } from "./actions.js";
43
44
  import { Icon } from "./icon.js";
44
45
  import { useReduceMotion } from "./movimento.js";
45
46
  import { Text } from "./text.js";
46
- import { useAureaTokens } from "./theme.js";
47
+ import { useAureaStrings, useAureaTokens } from "./theme.js";
47
48
  const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
48
49
  const fonteDoTamanho = (t, s) => s === "sm" ? t.size.textXs : s === "lg" ? t.size.textBase : t.size.textMd;
49
50
  const respiroDoTamanho = (t, s) => s === "sm" ? t.size.space3 : s === "lg" ? t.size.space4 : 13;
@@ -61,6 +62,21 @@ const folha = criarFolha((t) => ({
61
62
  areaDeTexto: { minHeight: 104, paddingVertical: t.size.space3, borderRadius: t.size.radiusLg },
62
63
  invalido: { borderColor: t.color.danger400 ?? t.color.destructive },
63
64
  desabilitado: { opacity: 0.5 },
65
+ // O GRUPO é a mesma caixa do campo virada em LINHA, e os números são os mesmos: mesma borda,
66
+ // mesmo raio de controle, mesmo fundo. A altura e o respiro horizontal entram por tamanho, como
67
+ // no `Input`, porque é o grupo que passa a desenhar a caixa quando existe um.
68
+ grupo: {
69
+ flexDirection: "row", alignItems: "center", width: "100%", minWidth: 0,
70
+ borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
71
+ borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
72
+ gap: t.size.space1,
73
+ },
74
+ // Dentro do grupo o campo perde a caixa e vira só o texto — a borda passou a ser do grupo.
75
+ // ⚠ As DUAS últimas linhas continuam obrigatórias: são o conserto de Android de 09/09/2026
76
+ // (`paddingVertical: 0` desliga a injeção do tema, `textAlignVertical` tira o texto do topo).
77
+ // É o mesmo par que o `campoDeTexto` do `busca.tsx` carrega pela mesma razão.
78
+ campoNoGrupo: { flex: 1, minWidth: 0, paddingVertical: 0, textAlignVertical: "center" },
79
+ encaixe: { flexDirection: "row", alignItems: "center", flexShrink: 0, gap: t.size.space1 },
64
80
  marcaBase: {
65
81
  alignItems: "center", justifyContent: "center", marginTop: 1,
66
82
  borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
@@ -96,6 +112,11 @@ const CampoCtx = React.createContext(null);
96
112
  export function useCampo() {
97
113
  return React.useContext(CampoCtx);
98
114
  }
115
+ const GrupoDeCampo = React.createContext(null);
116
+ /** O grupo em volta do campo, quando existe um. `null` quando o campo está sozinho. */
117
+ export function useGrupoDeCampo() {
118
+ return React.useContext(GrupoDeCampo);
119
+ }
99
120
  /**
100
121
  * Rótulo, controle, dica e erro — na ordem, com o respiro do CSS.
101
122
  *
@@ -151,11 +172,13 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
151
172
  const t = useAureaTokens();
152
173
  const s = folha(t);
153
174
  const campo = useCampo();
154
- const tam = size ?? campo?.size ?? "md";
155
- const inativo = disabled ?? campo?.disabled;
175
+ const grupo = useGrupoDeCampo();
176
+ const tam = size ?? grupo?.size ?? campo?.size ?? "md";
177
+ const inativo = disabled ?? grupo?.disabled ?? campo?.disabled;
156
178
  const [focado, setFocado] = React.useState(false);
157
- return (_jsx(TextInput, { 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); onFocus?.(); }, onBlur: () => {
179
+ return (_jsx(TextInput, { 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: () => {
158
180
  setFocado(false);
181
+ grupo?.aoFocar(false);
159
182
  // Só avisa quando o texto MUDOU: emitir o mesmo valor a cada saída de foco faria o app
160
183
  // re-renderizar sem motivo, e num formulário de dez campos isso é dez renders por
161
184
  // preenchimento. Mesma guarda da web (`inputs-client.tsx:194`).
@@ -169,30 +192,36 @@ export function Input({ value, defaultValue, onChangeText, placeholder, disabled
169
192
  // O nome e a dica vêm do `Field` — ver o bloco acima sobre por que a ligação da web não
170
193
  // atravessa. Sem isto, o campo é um nó anônimo para o leitor de tela.
171
194
  accessibilityLabel: campo?.label, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, "aria-invalid": campo?.invalido, style: [
172
- s.caixa,
173
- { height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam),
174
- fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400], color: t.color.foreground,
175
- // ⚠ AS DUAS LINHAS ABAIXO NÃO SÃO ENFEITE — sem elas o texto sobe e é CORTADO no
176
- // Android, e foi assim que este campo chegou ao vidro em 09/09/2026.
177
- //
178
- // `padding-block:0` é LITERAL na web (`aurea.css:685`) e não atravessou. No RN a falta
179
- // dele não é "sem padding": o `AndroidTextInputComponentDescriptor.h:101-107` INJETA o
180
- // padding do tema do Android no nó do Yoga sempre que as props não trazem `padding`,
181
- // `paddingTop`/`paddingBottom` nem `paddingVertical` — e o `hasPaddingVertical` é
182
- // exatamente o que desliga a injeção. Com `height` fixa, esse padding come a altura por
183
- // dentro e a caixa de texto fica menor que a linha: daí o corte.
184
- //
185
- // O `textAlignVertical` é o outro lado da mesma conta. Na web, um campo com
186
- // `block-size` centraliza a linha sozinho; no Android o `setTextAlignVertical` do
187
- // `ReactTextInputManager.kt:571` traduz `null`/`auto` para `Gravity.NO_GRAVITY`, que
188
- // deixa o `EditText` no topo. `center` é o que reproduz a web — e o `Textarea` o
189
- // sobrescreve para `top`, que é o que uma caixa alta faz nos dois lados.
190
- paddingVertical: 0, textAlignVertical: "center" },
191
- campo?.invalido && s.invalido,
195
+ // DENTRO DE UM GRUPO o campo não desenha caixa nenhuma: borda, fundo, altura, respiro,
196
+ // inválido, foco e desabilitado passam a ser do grupo. Manter qualquer um dos dois
197
+ // desenhando daria borda dentro de borda — que é exatamente o defeito que o `.input-group
198
+ // > .input` da web zera à mão (`aurea.css:751`).
199
+ grupo ? s.campoNoGrupo : s.caixa,
200
+ grupo
201
+ ? { fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400], color: t.color.foreground }
202
+ : { height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam),
203
+ fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400], color: t.color.foreground,
204
+ // ⚠ AS DUAS LINHAS ABAIXO NÃO SÃO ENFEITE — sem elas o texto sobe e é CORTADO no
205
+ // Android, e foi assim que este campo chegou ao vidro em 09/09/2026.
206
+ //
207
+ // `padding-block:0` é LITERAL na web (`aurea.css:685`) e não atravessou. No RN a falta
208
+ // dele não é "sem padding": o `AndroidTextInputComponentDescriptor.h:101-107` INJETA o
209
+ // padding do tema do Android no nó do Yoga sempre que as props não trazem `padding`,
210
+ // `paddingTop`/`paddingBottom` nem `paddingVertical` — e o `hasPaddingVertical` é
211
+ // exatamente o que desliga a injeção. Com `height` fixa, esse padding come a altura por
212
+ // dentro e a caixa de texto fica menor que a linha: daí o corte.
213
+ //
214
+ // O `textAlignVertical` é o outro lado da mesma conta. Na web, um campo com
215
+ // `block-size` centraliza a linha sozinho; no Android o `setTextAlignVertical` do
216
+ // `ReactTextInputManager.kt:571` traduz `null`/`auto` para `Gravity.NO_GRAVITY`, que
217
+ // deixa o `EditText` no topo. `center` é o que reproduz a web — e o `Textarea` o
218
+ // sobrescreve para `top`, que é o que uma caixa alta faz nos dois lados.
219
+ paddingVertical: 0, textAlignVertical: "center" },
220
+ !grupo && campo?.invalido && s.invalido,
192
221
  // O foco é o único estado que sobra do trio da web (hover/focus/active), e ele importa
193
222
  // mais aqui: no telefone é a única pista de qual campo o teclado está alimentando.
194
- focado && { borderColor: t.color.focusStrong },
195
- inativo && s.desabilitado,
223
+ !grupo && focado && { borderColor: t.color.focusStrong },
224
+ !grupo && inativo && s.desabilitado,
196
225
  style,
197
226
  ] }));
198
227
  }
@@ -211,6 +240,55 @@ export function Textarea({ rows, size, style, ...rest }) {
211
240
  style,
212
241
  ], multiline: true }));
213
242
  }
243
+ /**
244
+ * A caixa de campo que aceita coisas dentro — glifo na frente, botão atrás, os dois.
245
+ *
246
+ * <InputGroup>
247
+ * <InputGroupAddon><Icon name="email" size="sm" /></InputGroupAddon>
248
+ * <Input value={email} onChangeText={setEmail} keyboardType="email-address" />
249
+ * </InputGroup>
250
+ *
251
+ * ⚠ **O `Input` lá dentro para de desenhar a própria caixa** — quem desenha borda, fundo, altura,
252
+ * foco e inválido passa a ser o grupo. Isso é automático: o campo enxerga o grupo por contexto.
253
+ */
254
+ export function InputGroup({ size, disabled, children, style, testID, ...rest }) {
255
+ const t = useAureaTokens();
256
+ const s = folha(t);
257
+ const campo = useCampo();
258
+ const tam = size ?? campo?.size ?? "md";
259
+ const inativo = disabled ?? campo?.disabled ?? false;
260
+ const [focado, setFocado] = React.useState(false);
261
+ // O contexto muda de identidade a cada render se for objeto literal, e isso re-renderiza todo
262
+ // campo de todo formulário a cada tecla. Mesma memoização do `Field`.
263
+ const ctx = React.useMemo(() => ({ size: tam, disabled: inativo, aoFocar: setFocado }), [tam, inativo]);
264
+ return (_jsx(View, { testID: testID, style: [
265
+ s.grupo,
266
+ { height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) },
267
+ campo?.invalido && s.invalido,
268
+ // O foco vem do campo lá dentro, por contexto: no React Native não existe `:focus-within`,
269
+ // e sem isto a única pista de qual campo o teclado alimenta sumiria ao entrar num grupo.
270
+ focado && { borderColor: t.color.focusStrong },
271
+ inativo && s.desabilitado,
272
+ style,
273
+ ], ...rest, children: _jsx(GrupoDeCampo.Provider, { value: ctx, children: children }) }));
274
+ }
275
+ /**
276
+ * Um encaixe dentro do `InputGroup`. **A posição é a do JSX** — antes do campo fica na frente,
277
+ * depois fica atrás. Não há prop de lado, e o porquê está no bloco do grupo lá em cima.
278
+ */
279
+ export function InputGroupAddon({ children, style, testID, ...rest }) {
280
+ const t = useAureaTokens();
281
+ const s = folha(t);
282
+ return _jsx(View, { testID: testID, style: [s.encaixe, style], ...rest, children: children });
283
+ }
284
+ export function PasswordField({ defaultVisible = false, size, disabled, autoCapitalize = "none", testID, style, ...rest }) {
285
+ const strings = useAureaStrings();
286
+ const campo = useCampo();
287
+ const [visivel, setVisivel] = React.useState(defaultVisible);
288
+ const tam = size ?? campo?.size ?? "md";
289
+ const inativo = disabled ?? campo?.disabled;
290
+ return (_jsxs(InputGroup, { size: tam, disabled: inativo, testID: testID, children: [_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, onPress: () => setVisivel(v => !v), testID: testID ? `${testID}-olho` : undefined }) })] }));
291
+ }
214
292
  function ControleMarcado({ papel, label, description, checked, onChange, disabled, size, style, testID, }) {
215
293
  const t = useAureaTokens();
216
294
  const s = folha(t);
package/dist/numero.d.ts CHANGED
@@ -52,6 +52,14 @@ export interface NumberFieldProps {
52
52
  /** Mostra o valor e não deixa editar — os botões também somem. */
53
53
  readOnly?: boolean;
54
54
  size?: AureaFieldSize;
55
+ /**
56
+ * Ocupa a largura disponível em vez de abraçar o conteúdo. Padrão **false**.
57
+ *
58
+ * O padrão segue o `.number-field` da web, que é `inline-flex` (`aurea.css:704`) — e o mesmo
59
+ * eixo existe na HeroUI (`fullWidth`, medido no inventário). **Ligue para moeda:** o campo em
60
+ * repouso tem `--space-16` (64dp), que cabe um contador e não cabe `R$ 1.234,50`.
61
+ */
62
+ fullWidth?: boolean;
55
63
  /** O nome para o leitor de tela quando não há `Field` em volta. */
56
64
  label?: string;
57
65
  placeholder?: string;
@@ -96,5 +104,5 @@ type IconNameLocal = string;
96
104
  * ⚠ **Formata no blur, e só no blur.** Enquanto o campo tem foco, ele mostra exatamente o que foi
97
105
  * digitado — essa é a decisão inteira da ADR-0024, e o teste que a cobra está no lote.
98
106
  */
99
- export declare function NumberField({ value, defaultValue, onValueChange, min, max, step, format, locale, disabled, readOnly, size, label, placeholder, keyboardType, icons, style, testID, }: NumberFieldProps): React.JSX.Element;
107
+ export declare function NumberField({ value, defaultValue, onValueChange, min, max, step, format, locale, disabled, readOnly, size, fullWidth, label, placeholder, keyboardType, icons, style, testID, }: NumberFieldProps): React.JSX.Element;
100
108
  export {};
package/dist/numero.js CHANGED
@@ -53,18 +53,32 @@ import { useAureaStrings, useAureaTokens } 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) => ({
56
- // `.number-field-group` da web (`aurea.css:705`): linha, centro, `--space-1` entre as peças.
57
- grupo: { flexDirection: "row", alignItems: "center", gap: t.size.space1 },
58
- // O `.number-field-input` (`aurea.css:706`) é `--space-16` de largura e CENTRALIZADO. A largura
59
- // fixa atravessa como mínimo e não como fixa: num telefone de 360dp, com dois botões ao lado,
60
- // um campo rígido estoura a linha em `spacious`.
56
+ // 🔴 ELE ESTICAVA, E NÃO DEVIA — achado em 12/09/2026, olhando a primeira imagem da vitrine.
57
+ //
58
+ // O `.number-field` da web é `inline-flex` (`aurea.css:704`) e o `.number-field-group` também
59
+ // (`:705`): os dois **ABRAÇAM o conteúdo**. A primeira versão daqui punha `flex: 1` no campo,
60
+ // que PREENCHE — e o resultado é um campo enorme com o `−` e o `+` jogados nas pontas. Era
61
+ // divergência do nosso próprio CSS, entregue sem declarar, e ela só apareceu quando houve
62
+ // imagem para olhar.
63
+ //
64
+ // ⚠ **A saída é o eixo da HeroUI, e não uma invenção minha:** o `number-field` deles publica
65
+ // `fullWidth: [base, false, group, true]` (`INVENTORY-HEROUI.json`, medido em 22/08/2026).
66
+ // Esticar ou abraçar é DECISÃO DE USO, então vira prop — com o padrão em abraçar, que é o que
67
+ // o nosso CSS já dizia.
68
+ grupo: { flexDirection: "row", alignItems: "center", gap: t.size.space1,
69
+ alignSelf: "flex-start" },
70
+ grupoLargo: { alignSelf: "stretch" },
71
+ // `--space-16` = 64dp, medido. É a largura do `.number-field-input` (`aurea.css:706`), e ela
72
+ // serve ao caso que o componente foi feito para: contador e medida curta. **Moeda formatada
73
+ // não cabe em 64dp — nem aqui nem na web**, e é para isso que existe o `fullWidth`.
61
74
  campo: {
62
- flex: 1, minWidth: t.size.space16 ?? 64, textAlign: "center",
75
+ width: t.size.space16, textAlign: "center",
63
76
  borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
64
77
  borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
65
78
  // A mesma correção de Android do `Input` do Lote 4 — ver `inputs.tsx:280-296`.
66
79
  paddingVertical: 0, textAlignVertical: "center",
67
80
  },
81
+ campoLargo: { width: undefined, flex: 1, minWidth: t.size.space16 },
68
82
  invalido: { borderColor: t.color.danger400 ?? t.color.destructive },
69
83
  desabilitado: { opacity: 0.5 },
70
84
  }));
@@ -218,7 +232,7 @@ const paraEdicao = (n, locale) => {
218
232
  * ⚠ **Formata no blur, e só no blur.** Enquanto o campo tem foco, ele mostra exatamente o que foi
219
233
  * digitado — essa é a decisão inteira da ADR-0024, e o teste que a cobra está no lote.
220
234
  */
221
- export function NumberField({ value, defaultValue, onValueChange, min, max, step = 1, format, locale, disabled, readOnly, size, label, placeholder, keyboardType, icons = { increment: "add", decrement: "subtract" }, style, testID, }) {
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, }) {
222
236
  const t = useAureaTokens();
223
237
  const s = folha(t);
224
238
  const strings = useAureaStrings();
@@ -279,13 +293,35 @@ export function NumberField({ value, defaultValue, onValueChange, min, max, step
279
293
  return direcao === 1 ? (max != null && base >= max) : (min != null && base <= min);
280
294
  };
281
295
  const mostrarBotoes = icons !== false && !readOnly;
282
- return (_jsxs(View, { testID: testID, style: [s.grupo, 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: () => setEmEdicao(paraEdicao(numero, locale)), onBlur: confirmar, editable: !inativo && !readOnly, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, keyboardType: keyboardType ?? tecladoPadrao,
296
+ // 🔴 ACHADO PELA REFERÊNCIA, e não por mim — 12/09/2026. O inventário da HeroUI que já morava
297
+ // no repositório (`audit/activity-2/INVENTORY-HEROUI.json`, medido em 22/08 sobre
298
+ // `@heroui/react@3.2.4`) lista os estados do `number-field` deles:
299
+ //
300
+ // disabled · focus-visible · FOCUS-WITHIN · hovered · invalid · pressed
301
+ //
302
+ // Este componente nasceu **sem estado de foco nenhum** — e era o ÚNICO campo de texto do pacote
303
+ // sem ele: o `Input` do Lote 4 marca (`inputs.tsx:344`), o `Textarea` herda, o `SearchField`
304
+ // marca (`busca.tsx:520`). Inconsistência dentro da própria biblioteca, e invisível em teste
305
+ // até alguém comparar com uma referência.
306
+ //
307
+ // ⚠ **A BORDA vai no CAMPO, e não no grupo — e aqui a Aurea diverge da HeroUI de propósito.**
308
+ // Lá o `Group` carrega a caixa e o `Input` fica nu dentro dela. Aqui não: o `.number-field-group`
309
+ // do nosso CSS (`aurea.css:705`) é só `inline-flex` + `gap`, e quem tem borda é o `.input`
310
+ // (`aurea.css:685`), com os dois botões FORA dela. O estado que faltava é o deles; a geometria
311
+ // continua sendo a nossa.
312
+ 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,
283
314
  // O nome vem do `Field`, como em todo controle deste pacote — e `label` cobre quem usa o
284
315
  // campo solto. Sem um dos dois, é um nó anônimo para o leitor de tela.
285
316
  accessibilityLabel: label ?? campo?.label, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, "aria-invalid": campo?.invalido, style: [
286
317
  s.campo,
318
+ fullWidth && s.campoLargo,
287
319
  { height: alturaDoTamanho(t, tam), fontSize: fonteDoTamanho(t, tam),
288
320
  fontFamily: t.font.ui[400], color: t.color.foreground },
289
321
  campo?.invalido && s.invalido,
322
+ // A ORDEM IMPORTA e é a mesma do `Input` do Lote 4: o inválido vem antes, o foco
323
+ // depois. Um campo inválido que está sendo corrigido tem de mostrar que está ativo —
324
+ // invertendo, a pessoa digita sem pista nenhuma de onde o teclado está batendo.
325
+ focado && { borderColor: t.color.focusStrong },
290
326
  ] }), 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 }))] }));
291
327
  }
package/dist/strings.d.ts CHANGED
@@ -32,6 +32,14 @@ export interface AureaStrings {
32
32
  openSettings: string;
33
33
  /** O rótulo do botão que tira uma foto anexada. */
34
34
  photoRemove: string;
35
+ /**
36
+ * O botão que revela a senha no `PasswordField`, e o que a esconde de volta. Mesmos nomes da
37
+ * web (`pure.tsx`), porque é a mesma peça com a mesma decisão: **o rótulo é que anuncia o
38
+ * estado**, e não um `accessibilityState` de pressionado — com os dois, o leitor de tela diz
39
+ * a mesma coisa duas vezes.
40
+ */
41
+ passwordShow: string;
42
+ passwordHide: string;
35
43
  /** O botão de fechar do `Dialog` e do `Drawer`. Mesmo nome da web. */
36
44
  close: string;
37
45
  /** A saída do `ConfirmDialog`. Mesmo nome da web. */
package/dist/strings.js CHANGED
@@ -37,6 +37,8 @@ export const defaultStrings = {
37
37
  cameraDenied: "Camera access is off. Turn it on in Settings to attach a photo.",
38
38
  openSettings: "Open Settings",
39
39
  photoRemove: "Remove photo",
40
+ passwordShow: "Show password",
41
+ passwordHide: "Hide password",
40
42
  close: "Close",
41
43
  confirmCancel: "Cancel",
42
44
  confirmProceed: "Continue",
@@ -72,6 +74,8 @@ export const ptBR = {
72
74
  cameraDenied: "O acesso à câmera está desligado. Ligue nas configurações para anexar uma foto.",
73
75
  openSettings: "Abrir configurações",
74
76
  photoRemove: "Remover foto",
77
+ passwordShow: "Mostrar senha",
78
+ passwordHide: "Ocultar senha",
75
79
  close: "Fechar",
76
80
  confirmCancel: "Cancelar",
77
81
  confirmProceed: "Continuar",
package/dist/text.d.ts CHANGED
@@ -6,7 +6,7 @@ export type AureaTextSize = "xs" | "sm" | "md" | "base" | "lg" | "xl" | "2xl" |
6
6
  export type AureaTextWeight = 400 | 500 | 600 | 700;
7
7
  /** Papel de tipografia — os três `--font-*` da Aurea. */
8
8
  export type AureaTextFont = "ui" | "editorial" | "code";
9
- /** O que a cor SIGNIFICA. Mesmo vocabulário do `tone` do Button (ADR-0016). */
9
+ /** O que a cor SIGNIFICA. Mesmo vocabulário do `tone` do Button (ADR-0044). */
10
10
  export type AureaTextTone = "default" | "muted" | "subtle" | "primary" | "danger" | "success" | "warning" | "info";
11
11
  export type AureaTextLeading = "none" | "tight" | "normal" | "relaxed";
12
12
  export interface TextProps extends TextPropsRN {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurea-uds/native",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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.0"
50
+ "@aurea-uds/tokens": "^0.8.2"
51
51
  },
52
52
  "peerDependencies": {
53
53
  "@react-native-community/datetimepicker": ">=8",