@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 +22 -2
- package/dist/actions.js +4 -4
- package/dist/busca.d.ts +8 -1
- package/dist/busca.js +66 -11
- package/dist/display.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/dist/inputs.d.ts +55 -0
- package/dist/inputs.js +104 -26
- package/dist/numero.d.ts +9 -1
- package/dist/numero.js +44 -8
- package/dist/strings.d.ts +8 -0
- package/dist/strings.js +4 -0
- package/dist/text.d.ts +1 -1
- package/package.json +2 -2
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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:
|
|
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 =
|
|
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-
|
|
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
|
|
155
|
-
const
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
//
|
|
57
|
-
|
|
58
|
-
// O `.number-field-
|
|
59
|
-
//
|
|
60
|
-
// um campo
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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.
|
|
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.
|
|
50
|
+
"@aurea-uds/tokens": "^0.8.2"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"@react-native-community/datetimepicker": ">=8",
|