@aurea-uds/native 0.7.0 → 0.8.1
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/README.md +135 -3
- package/dist/busca.d.ts +126 -0
- package/dist/busca.js +393 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +21 -0
- package/dist/inputs.d.ts +31 -1
- package/dist/inputs.js +19 -2
- package/dist/midia.d.ts +98 -0
- package/dist/midia.js +181 -0
- package/dist/numero.d.ts +108 -0
- package/dist/numero.js +327 -0
- package/dist/strings.d.ts +26 -0
- package/dist/strings.js +16 -0
- package/package.json +2 -2
package/dist/busca.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// Aurea nativo — a BUSCA: `Combobox` e `SearchField`.
|
|
3
|
+
//
|
|
4
|
+
// Lote 7 do `NATIVE.md` §8 — o primeiro lote que nasce de DEMANDA MEDIDA e não do plano: o alvo
|
|
5
|
+
// nativo fechou em 46 componentes em 11/09/2026, e o consumidor bateu em três lacunas na primeira
|
|
6
|
+
// tela que a pessoa toca. Esta é a primeira delas, e é a que bloqueia o cadastro.
|
|
7
|
+
//
|
|
8
|
+
// ── O QUE FALTAVA, LITERALMENTE ──────────────────────────────────────────────────────────────
|
|
9
|
+
// O `Select` (`inputs.tsx:576`) escreve a própria lacuna: *"O papel é `button` e não `combobox`.
|
|
10
|
+
// `combobox` promete um campo em que se DIGITA para filtrar; isto só abre uma lista."* Era
|
|
11
|
+
// honesto, e continua sendo — o `Select` não mudou. O que faltava era o componente que CUMPRE a
|
|
12
|
+
// promessa, e é este.
|
|
13
|
+
//
|
|
14
|
+
// E havia um agravante que a demanda não citava, medido aqui: a folha do `Select` usa
|
|
15
|
+
// `ScrollView` (`inputs.tsx:632`), que **monta todos os itens**. Um catálogo de milhares de
|
|
16
|
+
// linhas ali não fica lento — ele trava. Este usa `FlatList`, como o `DataList` do Lote 6 já
|
|
17
|
+
// fazia (`data.tsx:191`).
|
|
18
|
+
//
|
|
19
|
+
// ── A DIVERGÊNCIA DA WEB, E ELA É DELIBERADA ─────────────────────────────────────────────────
|
|
20
|
+
// **A demanda é busca REMOTA de seleção única, e essa combinação não existe na web.** Medido em
|
|
21
|
+
// `inputs-client.tsx`:
|
|
22
|
+
//
|
|
23
|
+
// Combobox (seleção única) -> `items` e filtro do Base UI, do lado do cliente. Sem `onInputChange`.
|
|
24
|
+
// MultiCombobox (múltipla) -> tem `onInputChange` + `loading`, e desliga o filtro (`filter={null}`)
|
|
25
|
+
//
|
|
26
|
+
// Ou seja: quem quer buscar no servidor E escolher UM item não tem componente na web. Copiar o
|
|
27
|
+
// contrato de lá deixaria o consumidor com um filtro de cliente sobre milhares de linhas que ele
|
|
28
|
+
// nem baixou.
|
|
29
|
+
//
|
|
30
|
+
// Então este componente soma `onSearchChange` + `loading` + `onEndReached` ao contrato de seleção
|
|
31
|
+
// única, e **a regra é uma só e vale para os dois lados**: se `onSearchChange` existe, a Aurea
|
|
32
|
+
// NÃO filtra — a lista que chega é a lista que aparece, e quem busca é o app. Sem ele, a Aurea
|
|
33
|
+
// filtra o que recebeu. É a mesma chave do `filter={null}` do `MultiCombobox`, escrita como
|
|
34
|
+
// presença de prop em vez de configuração de motor.
|
|
35
|
+
//
|
|
36
|
+
// É a mesma classe de divergência que a `Table` do Lote 6 assumiu (`columns`+`rows` em vez de
|
|
37
|
+
// `children`): quando a plataforma muda o problema, o contrato muda com ele — declarado, não
|
|
38
|
+
// escondido.
|
|
39
|
+
//
|
|
40
|
+
// ── ONDE SE DIGITA, E POR QUE NÃO É NO GATILHO ───────────────────────────────────────────────
|
|
41
|
+
// Na web o `<input>` É o combobox: digita-se nele e a lista desce ancorada. Aqui **o gatilho
|
|
42
|
+
// continua sendo um botão, e o campo de digitar mora DENTRO da folha**. Três razões medidas, não
|
|
43
|
+
// preferidas:
|
|
44
|
+
//
|
|
45
|
+
// 1. o teclado do telefone ocupa metade da tela. Um campo ancorado no meio de um formulário
|
|
46
|
+
// abre o teclado por cima da própria lista — a folha sobe do rodapé e fica acima dele;
|
|
47
|
+
// 2. o gatilho precisa mostrar o ESCOLHIDO com a geometria do `.input` (mesma altura, mesma
|
|
48
|
+
// borda, a mesma seta) — é o que faz o campo parecer um campo no formulário. Um campo de
|
|
49
|
+
// texto ali mostraria o que se digitou, não o que se escolheu;
|
|
50
|
+
// 3. é o idioma dos seletores do próprio sistema, nos dois lados.
|
|
51
|
+
//
|
|
52
|
+
// ── A ACESSIBILIDADE, MEDIDA NO FONTE DO RN 0.87.1 (e não presumida) ─────────────────────────
|
|
53
|
+
// O Lote 5 ensinou que o RN ACEITA papel que não mapeia (`dialog` compila e vira `null`). Então
|
|
54
|
+
// os dois papéis daqui foram conferidos no fonte do `react-native@0.87.1` antes de usados:
|
|
55
|
+
//
|
|
56
|
+
// `role="search"` -> `accessibilityPropsConversions.h:64-65` traduz para
|
|
57
|
+
// `AccessibilityTraits::SearchField`, que o `RCTConversions.h:120-121`
|
|
58
|
+
// vira `UIAccessibilityTraitSearchField` no iOS; no Android o
|
|
59
|
+
// `ReactAccessibilityDelegate.kt:461` dá a `className`
|
|
60
|
+
// `android.widget.EditText`. **Mapeia nos dois.**
|
|
61
|
+
// `role="combobox"` -> no Android o `ReactAccessibilityDelegate.kt:680-682` põe
|
|
62
|
+
// `roleDescription` de `R.string.combobox_description` (o TalkBack diz
|
|
63
|
+
// "caixa de combinação"); **no iOS não há trait equivalente** — não está
|
|
64
|
+
// na tabela do `RCTConversions.h`. Mapeia em UM lado.
|
|
65
|
+
//
|
|
66
|
+
// Por isso o gatilho é `button` com `expanded`, como o do `Select`: ele abre uma folha e não
|
|
67
|
+
// aceita digitação, então `button` é o que ele É nos dois sistemas. Quem digita é o campo da
|
|
68
|
+
// folha, e ele leva `search` — que mapeia nos dois.
|
|
69
|
+
import * as React from "react";
|
|
70
|
+
import { Animated, Easing, FlatList, Modal, PanResponder, Pressable, TextInput, View, } from "react-native";
|
|
71
|
+
import { IconButton } from "./actions.js";
|
|
72
|
+
import { criarFolha } from "./estilos.js";
|
|
73
|
+
import { Spinner } from "./feedback.js";
|
|
74
|
+
import { Icon } from "./icon.js";
|
|
75
|
+
import { KeyboardAvoiding, useCampo } from "./inputs.js";
|
|
76
|
+
import { useReduceMotion } from "./movimento.js";
|
|
77
|
+
import { Text } from "./text.js";
|
|
78
|
+
import { useAureaStrings, useAureaTokens } from "./theme.js";
|
|
79
|
+
// As mesmas contas do `inputs.tsx`, e elas são repetidas AQUI de propósito: exportá-las de lá
|
|
80
|
+
// tornaria pública uma conta interna do outro módulo, e um `import` de função privada entre
|
|
81
|
+
// módulos irmãos é o começo de um acoplamento que ninguém declara. São três linhas.
|
|
82
|
+
const alturaDoTamanho = (t, s) => s === "sm" ? t.size.controlHSm : s === "lg" ? t.size.controlHLg : t.size.controlHMd;
|
|
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.
|
|
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;
|
|
101
|
+
const folha = criarFolha((t) => ({
|
|
102
|
+
// O gatilho é o `.input` da web (`aurea.css:685`), igual ao do `Select` — mesma altura, mesma
|
|
103
|
+
// borda, o mesmo raio de controle.
|
|
104
|
+
caixa: {
|
|
105
|
+
width: "100%", minWidth: 0,
|
|
106
|
+
borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
|
|
107
|
+
borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
|
|
108
|
+
flexDirection: "row", alignItems: "center",
|
|
109
|
+
},
|
|
110
|
+
invalido: { borderColor: t.color.danger400 ?? t.color.destructive },
|
|
111
|
+
desabilitado: { opacity: 0.5 },
|
|
112
|
+
// As ações do gatilho — limpar e a seta. O `.combobox-actions` da web (`aurea.css:1673`) é
|
|
113
|
+
// absoluto sobre o campo; aqui é um irmão na linha, porque não há campo por baixo.
|
|
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 },
|
|
117
|
+
// A folha, com a mesma anatomia da do `Select` (`inputs.tsx:98-113`) — e é de propósito que
|
|
118
|
+
// sejam iguais: são o mesmo gesto, e duas folhas diferentes para o mesmo gesto é como uma
|
|
119
|
+
// biblioteca deixa de ter linguagem.
|
|
120
|
+
fundoDaLista: { flex: 1, justifyContent: "flex-end", backgroundColor: "#00000080" },
|
|
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.
|
|
127
|
+
lista: {
|
|
128
|
+
maxHeight: "90%",
|
|
129
|
+
borderTopLeftRadius: t.size.radiusCard, borderTopRightRadius: t.size.radiusCard,
|
|
130
|
+
backgroundColor: t.color.popover, borderWidth: t.size.borderWidth, borderColor: t.color.border,
|
|
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 },
|
|
136
|
+
// O campo de digitar, no topo da folha. É o `.input-group` da web (`aurea.css:741`): a borda
|
|
137
|
+
// mora no GRUPO e o campo dentro dele é transparente e sem borda.
|
|
138
|
+
grupo: {
|
|
139
|
+
flexDirection: "row", alignItems: "center", gap: t.size.space1,
|
|
140
|
+
margin: t.size.space3,
|
|
141
|
+
paddingHorizontal: t.size.space3,
|
|
142
|
+
borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
|
|
143
|
+
borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
|
|
144
|
+
},
|
|
145
|
+
// ⚠ `paddingVertical: 0` e `textAlignVertical: "center"` — a MESMA correção que o `Input` do
|
|
146
|
+
// Lote 4 levou depois do vidro (`inputs.tsx:280-296`): sem elas o Android injeta o padding do
|
|
147
|
+
// tema dele no nó do Yoga e o texto sai cortado dentro de uma caixa de altura fixa. A regra do
|
|
148
|
+
// `CLAUDE.md` ("correção local é proibida sem responder quem mais tem esse problema") é o que
|
|
149
|
+
// traz as duas linhas para cá, e este campo é exatamente "quem mais".
|
|
150
|
+
campoDeTexto: { flex: 1, minWidth: 0, paddingVertical: 0, textAlignVertical: "center" },
|
|
151
|
+
opcao: {
|
|
152
|
+
minHeight: t.size.controlHLg, justifyContent: "center",
|
|
153
|
+
paddingHorizontal: t.size.space4, paddingVertical: t.size.space2,
|
|
154
|
+
},
|
|
155
|
+
opcaoEscolhida: { backgroundColor: t.color.surface3 ?? t.color.muted },
|
|
156
|
+
vazio: { paddingHorizontal: t.size.space4, paddingVertical: t.size.space5, alignItems: "center" },
|
|
157
|
+
carregando: { paddingVertical: t.size.space4, alignItems: "center" },
|
|
158
|
+
}));
|
|
159
|
+
/**
|
|
160
|
+
* O campo em que se DIGITA para achar um item.
|
|
161
|
+
*
|
|
162
|
+
* ```tsx
|
|
163
|
+
* // catálogo remoto — a Aurea não filtra, o app busca
|
|
164
|
+
* <Field label="Item">
|
|
165
|
+
* <Combobox
|
|
166
|
+
* items={resultados}
|
|
167
|
+
* value={escolhido}
|
|
168
|
+
* onValueChange={setEscolhido}
|
|
169
|
+
* onSearchChange={buscarNoServidor}
|
|
170
|
+
* loading={buscando}
|
|
171
|
+
* onEndReached={proximaPagina}
|
|
172
|
+
* />
|
|
173
|
+
* </Field>
|
|
174
|
+
*
|
|
175
|
+
* // lista na mão — a Aurea filtra
|
|
176
|
+
* <Combobox items={unidades} value={unidade} onValueChange={setUnidade} />
|
|
177
|
+
* ```
|
|
178
|
+
*
|
|
179
|
+
* ⚠ **O gatilho é `button`, não `combobox`** — e desta vez a razão é medida, não herdada do
|
|
180
|
+
* `Select`: `role="combobox"` mapeia no Android (`roleDescription`) e **não mapeia no iOS**; e o
|
|
181
|
+
* gatilho, de qualquer forma, não aceita digitação. Quem digita é o campo da folha, que leva
|
|
182
|
+
* `role="search"` — esse mapeia nos dois. A tabela com arquivo e linha está no topo deste módulo.
|
|
183
|
+
*
|
|
184
|
+
* ⚠ **Ele não guarda a escolha nem o texto buscado.** `value` e o que se digita são do app, como
|
|
185
|
+
* na web e pela mesma razão do achado I1 da auditoria: um componente com a sua própria cópia do
|
|
186
|
+
* estado é a segunda fonte de verdade que ninguém sabe que existe.
|
|
187
|
+
*/
|
|
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, }) {
|
|
189
|
+
const t = useAureaTokens();
|
|
190
|
+
const s = folha(t);
|
|
191
|
+
const strings = useAureaStrings();
|
|
192
|
+
const campo = useCampo();
|
|
193
|
+
const tam = size ?? campo?.size ?? "md";
|
|
194
|
+
const inativo = disabled ?? campo?.disabled;
|
|
195
|
+
const [aberto, setAberto] = React.useState(false);
|
|
196
|
+
const [texto, setTexto] = React.useState("");
|
|
197
|
+
const reduzir = useReduceMotion();
|
|
198
|
+
// ── A ESPERA ────────────────────────────────────────────────────────────────────────────────
|
|
199
|
+
// O timer vive num `ref` e é limpo em três lugares: à digitação seguinte, ao fechar a folha e
|
|
200
|
+
// ao desmontar. O terceiro é o que importa e o que mais se esquece — sem ele, sair da tela com
|
|
201
|
+
// uma busca pendente dispara `onSearchChange` sobre um componente que já não existe, e o app
|
|
202
|
+
// leva um `setState` em árvore desmontada.
|
|
203
|
+
const relogio = React.useRef(null);
|
|
204
|
+
const cancelar = React.useCallback(() => {
|
|
205
|
+
if (relogio.current != null) {
|
|
206
|
+
clearTimeout(relogio.current);
|
|
207
|
+
relogio.current = null;
|
|
208
|
+
}
|
|
209
|
+
}, []);
|
|
210
|
+
React.useEffect(() => cancelar, [cancelar]);
|
|
211
|
+
// A referência mais recente da função de busca, para o timer não disparar a de um render
|
|
212
|
+
// anterior. É o padrão de `ref` para callback — e existe porque o app costuma passar uma
|
|
213
|
+
// arrow nova a cada render.
|
|
214
|
+
const buscaAtual = React.useRef(onSearchChange);
|
|
215
|
+
React.useEffect(() => { buscaAtual.current = onSearchChange; }, [onSearchChange]);
|
|
216
|
+
const digitar = React.useCallback((v) => {
|
|
217
|
+
setTexto(v);
|
|
218
|
+
if (!buscaAtual.current)
|
|
219
|
+
return;
|
|
220
|
+
cancelar();
|
|
221
|
+
if (searchDelay <= 0) {
|
|
222
|
+
buscaAtual.current(v);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
relogio.current = setTimeout(() => {
|
|
226
|
+
relogio.current = null;
|
|
227
|
+
buscaAtual.current?.(v);
|
|
228
|
+
}, searchDelay);
|
|
229
|
+
}, [cancelar, searchDelay]);
|
|
230
|
+
const fechar = React.useCallback(() => {
|
|
231
|
+
cancelar();
|
|
232
|
+
setAberto(false);
|
|
233
|
+
setTexto("");
|
|
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]);
|
|
264
|
+
// ── O FILTRO, QUANDO ELE É NOSSO ────────────────────────────────────────────────────────────
|
|
265
|
+
const filtrados = React.useMemo(() => {
|
|
266
|
+
if (onSearchChange)
|
|
267
|
+
return items; // busca remota: a lista que chegou é a lista.
|
|
268
|
+
const alvo = dobrar(texto.trim());
|
|
269
|
+
if (!alvo)
|
|
270
|
+
return items;
|
|
271
|
+
return items.filter((i) => dobrar(i.label).includes(alvo));
|
|
272
|
+
}, [items, texto, onSearchChange]);
|
|
273
|
+
const nada = !loading && filtrados.length === 0;
|
|
274
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Pressable, { testID: testID, onPress: inativo ? undefined : () => setAberto(true), disabled: inativo, accessibilityRole: "button", accessibilityLabel: campo?.label, accessibilityHint: campo?.hint, accessibilityValue: { text: value?.label }, accessibilityState: { disabled: !!inativo, expanded: aberto }, style: [
|
|
275
|
+
s.caixa,
|
|
276
|
+
{ height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) },
|
|
277
|
+
campo?.invalido && s.invalido,
|
|
278
|
+
inativo && s.desabilitado,
|
|
279
|
+
style,
|
|
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,
|
|
281
|
+
// ⚠ `autoFocus` é o que faz a folha valer a pena: abrir um campo de busca e
|
|
282
|
+
// exigir um segundo toque para o teclado subir é um toque a mais em cada
|
|
283
|
+
// cadastro. O foco entra com a folha; o teclado vem junto.
|
|
284
|
+
autoFocus: true, autoCorrect: false, autoCapitalize: "none",
|
|
285
|
+
// Mapeia nos dois sistemas — conferido no fonte do RN, tabela no topo do módulo.
|
|
286
|
+
accessibilityRole: "search", accessibilityLabel: searchPlaceholder ?? strings.comboboxSearch,
|
|
287
|
+
// `returnKeyType="search"` troca o "enter" do teclado pela lupa. É pista de
|
|
288
|
+
// plataforma, não decoração: diz à pessoa que aquele campo é de busca antes de
|
|
289
|
+
// ela digitar a primeira letra.
|
|
290
|
+
returnKeyType: "search", style: [
|
|
291
|
+
s.campoDeTexto,
|
|
292
|
+
{ fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
|
|
293
|
+
color: t.color.foreground },
|
|
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
|
|
295
|
+
? _jsx(Text, { size: "sm", tone: "muted", children: empty ?? strings.comboboxEmpty })
|
|
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: {
|
|
297
|
+
selected: item.value === value?.value, disabled: !!item.disabled,
|
|
298
|
+
}, style: [
|
|
299
|
+
s.opcao,
|
|
300
|
+
item.value === value?.value && s.opcaoEscolhida,
|
|
301
|
+
item.disabled && s.desabilitado,
|
|
302
|
+
], children: _jsx(Text, { size: "md", weight: item.value === value?.value ? 600 : 400, children: item.label }) })) })] })] }) })] }));
|
|
303
|
+
}
|
|
304
|
+
// ── A DOBRA DE ACENTO, e por que ela é sondada em vez de presumida ───────────────────────────
|
|
305
|
+
// Buscar "acucar" tem de achar "açúcar" — num catálogo em português, exigir o acento certo é
|
|
306
|
+
// exigir que a pessoa saiba escrever o que está procurando. A dobra é `NFD` + remoção dos
|
|
307
|
+
// diacríticos combinantes, que é a forma padrão.
|
|
308
|
+
//
|
|
309
|
+
// **A sonda existe porque o motor é o motor, não um navegador.** `String.prototype.normalize` é
|
|
310
|
+
// ES2015 e está no motor, mas este pacote roda em qualquer motor que o consumidor escolha — e
|
|
311
|
+
// um `normalize` ausente derrubaria a busca inteira com `TypeError` em vez de degradá-la. A
|
|
312
|
+
// sonda roda UMA vez, no carregamento do módulo, e custa uma string de dois caracteres.
|
|
313
|
+
const TEM_NORMALIZE = (() => {
|
|
314
|
+
try {
|
|
315
|
+
return "é".normalize("NFD").length === 2;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
})();
|
|
321
|
+
const dobrar = (v) => {
|
|
322
|
+
const minusculo = v.toLowerCase();
|
|
323
|
+
return TEM_NORMALIZE
|
|
324
|
+
? minusculo.normalize("NFD").replace(/[̀-ͯ]/g, "")
|
|
325
|
+
: minusculo;
|
|
326
|
+
};
|
|
327
|
+
/**
|
|
328
|
+
* A lupa + o campo, numa peça só.
|
|
329
|
+
*
|
|
330
|
+
* ⚠ **Não é o mesmo papel do `Combobox`, e confundir os dois é o erro comum:** o `Combobox`
|
|
331
|
+
* ESCOLHE um item de um catálogo e devolve a escolha; este FILTRA o que já está na tela e devolve
|
|
332
|
+
* texto. Um cadastro pede o primeiro; uma lista com muitas linhas pede o segundo.
|
|
333
|
+
*
|
|
334
|
+
* ⚠ **`role="search"`, e ele mapeia nos dois sistemas** — conferido no fonte do RN 0.87.1, com
|
|
335
|
+
* arquivo e linha no topo deste módulo. É a diferença entre este componente e o `Combobox`, cujo
|
|
336
|
+
* gatilho não pôde levar `combobox` porque o iOS não tem o trait.
|
|
337
|
+
*
|
|
338
|
+
* ⚠ **A borda mora no GRUPO, não no campo** — é o `.input-group` da web (`aurea.css:741`), e é o
|
|
339
|
+
* que deixa lupa, campo e botão de limpar dentro de uma caixa só em vez de três.
|
|
340
|
+
*/
|
|
341
|
+
export function SearchField({ value, onChangeText, onSearchChange, searchDelay = ESPERA_PADRAO, placeholder, disabled, size, icon = "search", clearable = true, onSubmit, style, testID, }) {
|
|
342
|
+
const t = useAureaTokens();
|
|
343
|
+
const s = folha(t);
|
|
344
|
+
const strings = useAureaStrings();
|
|
345
|
+
const campo = useCampo();
|
|
346
|
+
const tam = size ?? campo?.size ?? "md";
|
|
347
|
+
const inativo = disabled ?? campo?.disabled;
|
|
348
|
+
const [focado, setFocado] = React.useState(false);
|
|
349
|
+
// O campo pode ser controlado (`value`) ou não; o texto interno cobre o segundo caso, e é o que
|
|
350
|
+
// o botão de limpar precisa para saber se tem o que limpar.
|
|
351
|
+
const [interno, setInterno] = React.useState("");
|
|
352
|
+
const texto = value ?? interno;
|
|
353
|
+
const relogio = React.useRef(null);
|
|
354
|
+
const cancelar = React.useCallback(() => {
|
|
355
|
+
if (relogio.current != null) {
|
|
356
|
+
clearTimeout(relogio.current);
|
|
357
|
+
relogio.current = null;
|
|
358
|
+
}
|
|
359
|
+
}, []);
|
|
360
|
+
React.useEffect(() => cancelar, [cancelar]);
|
|
361
|
+
const buscaAtual = React.useRef(onSearchChange);
|
|
362
|
+
React.useEffect(() => { buscaAtual.current = onSearchChange; }, [onSearchChange]);
|
|
363
|
+
const digitar = React.useCallback((v) => {
|
|
364
|
+
setInterno(v);
|
|
365
|
+
onChangeText?.(v);
|
|
366
|
+
if (!buscaAtual.current)
|
|
367
|
+
return;
|
|
368
|
+
cancelar();
|
|
369
|
+
if (searchDelay <= 0) {
|
|
370
|
+
buscaAtual.current(v);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
relogio.current = setTimeout(() => {
|
|
374
|
+
relogio.current = null;
|
|
375
|
+
buscaAtual.current?.(v);
|
|
376
|
+
}, searchDelay);
|
|
377
|
+
}, [cancelar, onChangeText, searchDelay]);
|
|
378
|
+
return (_jsxs(View, { testID: testID, style: [
|
|
379
|
+
s.caixa,
|
|
380
|
+
{ height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam),
|
|
381
|
+
gap: t.size.space1 },
|
|
382
|
+
campo?.invalido && s.invalido,
|
|
383
|
+
// O foco é a única pista de campo ativo que sobra no telefone — mesma decisão do `Input`
|
|
384
|
+
// do Lote 4, e ela vale igual aqui.
|
|
385
|
+
focado && { borderColor: t.color.focusStrong },
|
|
386
|
+
inativo && s.desabilitado,
|
|
387
|
+
style,
|
|
388
|
+
], children: [icon && _jsx(Icon, { name: icon, size: "sm", color: t.color.subtleForeground }), _jsx(TextInput, { testID: testID ? `${testID}-campo` : undefined, value: value, onChangeText: digitar, placeholder: placeholder, placeholderTextColor: t.color.subtleForeground, editable: !inativo, autoCorrect: false, autoCapitalize: "none", returnKeyType: "search", onSubmitEditing: onSubmit, onFocus: () => setFocado(true), onBlur: () => setFocado(false), accessibilityRole: "search", accessibilityLabel: campo?.label ?? placeholder, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, style: [
|
|
389
|
+
s.campoDeTexto,
|
|
390
|
+
{ fontSize: fonteDoTamanho(t, tam), fontFamily: t.font.ui[400],
|
|
391
|
+
color: t.color.foreground },
|
|
392
|
+
] }), clearable && texto.length > 0 && !inativo && (_jsx(IconButton, { name: "close", label: strings.searchClear, appearance: "ghost", size: "sm", onPress: () => digitar(""), testID: testID ? `${testID}-limpar` : undefined }))] }));
|
|
393
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -33,3 +33,9 @@ export { Timeline, DataList, Table } from "./data.js";
|
|
|
33
33
|
export type { TimelineProps, DataListProps, TableProps, AureaColumn, AureaTimelineItem, AureaDataListItem, } from "./data.js";
|
|
34
34
|
export { Chart, ChartLegend } from "./chart.js";
|
|
35
35
|
export type { ChartProps, ChartLegendProps, AureaSeries, AureaChartMark } from "./chart.js";
|
|
36
|
+
export { Combobox, SearchField } from "./busca.js";
|
|
37
|
+
export type { ComboboxProps, SearchFieldProps, AureaComboboxItem } from "./busca.js";
|
|
38
|
+
export { NumberField, formatarNumero, lerNumero, separadoresDoLocale } from "./numero.js";
|
|
39
|
+
export type { NumberFieldProps, AureaSeparadores } from "./numero.js";
|
|
40
|
+
export { Image, Gallery } from "./midia.js";
|
|
41
|
+
export type { ImageProps, GalleryProps, AureaGalleryItem, AureaImageSource, } from "./midia.js";
|
package/dist/index.js
CHANGED
|
@@ -86,3 +86,24 @@ export { Timeline, DataList, Table } from "./data.js";
|
|
|
86
86
|
// ⚠ **Com duas ou mais séries a legenda NÃO desliga**: a paleta `--chart-*` é uma rampa de um
|
|
87
87
|
// azul só, e duas séries vizinhas nela são indistinguíveis (ΔE 5.9, medido).
|
|
88
88
|
export { Chart, ChartLegend } from "./chart.js";
|
|
89
|
+
// ── Lote 7 — as três lacunas que o CONSUMIDOR mediu, e não o plano ─────────────────────────
|
|
90
|
+
// ⚠ Este é o primeiro lote do alvo nativo que **não sai do `NATIVE.md` §5**: o plano fechou em
|
|
91
|
+
// 11/09/2026 com 46 componentes e "não sobrou nada". Sobrou — só que a fila passou a ser a do
|
|
92
|
+
// app, não a do documento. As três estão no §8, com a demanda de cada uma.
|
|
93
|
+
//
|
|
94
|
+
// ⚠ **O `Combobox` DIVERGE da web, e é deliberado:** ele soma `onSearchChange`, `loading` e
|
|
95
|
+
// `onEndReached` ao contrato de seleção única, porque busca remota + escolha de UM item não
|
|
96
|
+
// existe lá (o `Combobox` da web filtra no cliente; quem tem `onInputChange` é o
|
|
97
|
+
// `MultiCombobox`, que é de seleção múltipla). A regra: **`onSearchChange` presente = a Aurea
|
|
98
|
+
// não filtra.**
|
|
99
|
+
export { Combobox, SearchField } from "./busca.js";
|
|
100
|
+
// ⚠ O `NumberField` formata no BLUR, nunca enquanto se digita — a [ADR-0024] atravessa inteira.
|
|
101
|
+
// O que muda no nativo é quem chama o `Intl` (não há Base UI) e a VOLTA, que a web não precisou
|
|
102
|
+
// escrever: `lerNumero` converte de novo o que a pessoa digitou. Os dois auxiliares são públicos
|
|
103
|
+
// porque o app tem o mesmo problema em toda tela de lançamento.
|
|
104
|
+
export { NumberField, formatarNumero, lerNumero, separadoresDoLocale } from "./numero.js";
|
|
105
|
+
// ⚠ O `Image` NÃO é o `Image` do React Native com outra pele: ele **reserva a caixa** antes dos
|
|
106
|
+
// bytes e **cai para um substituto** quando eles não vêm — as duas coisas que a ficha da web
|
|
107
|
+
// promete e que o primitivo cru não faz. Foi por não fazê-las que o `Avatar` precisou de
|
|
108
|
+
// conserto em 31/07/2026.
|
|
109
|
+
export { Image, Gallery } from "./midia.js";
|
package/dist/inputs.d.ts
CHANGED
|
@@ -61,10 +61,34 @@ export interface InputProps {
|
|
|
61
61
|
/**
|
|
62
62
|
* O teclado que sobe. **O plano do consumidor pede `numeric` nos campos de medida e moeda**, e
|
|
63
63
|
* é a prop que faz a diferença entre digitar 12,4 em dois toques ou em oito.
|
|
64
|
+
*
|
|
65
|
+
* ⚠ **Teclado não é formato, e esta prop resolve só a metade fácil** — foi a segunda lacuna
|
|
66
|
+
* que o consumidor mediu. Moeda, medida decimal e contador são o `NumberField` (Lote 7,
|
|
67
|
+
* `numero.tsx`), que formata no blur e devolve `number`. Este campo continua devolvendo texto.
|
|
64
68
|
*/
|
|
65
69
|
keyboardType?: KeyboardTypeOptions;
|
|
66
70
|
secureTextEntry?: boolean;
|
|
67
71
|
autoCapitalize?: "none" | "sentences" | "words" | "characters";
|
|
72
|
+
/**
|
|
73
|
+
* Normaliza o texto **quando o foco sai** — placa, documento, telefone.
|
|
74
|
+
*
|
|
75
|
+
* ⚠ **A Aurea entrega o MOMENTO, não o formato**, e isso é a [ADR-0024] inteira: formatar
|
|
76
|
+
* enquanto se digita é o defeito que o USWDS publicou com reprovação WCAG registrada e que o
|
|
77
|
+
* MUI abandonou na v6. O formato é conhecimento de DOMÍNIO — a Aurea não sabe o que é um
|
|
78
|
+
* documento válido em lugar nenhum, e uma tabela de formatos por país dentro de uma biblioteca
|
|
79
|
+
* de interface envelhece sem ninguém perceber.
|
|
80
|
+
*
|
|
81
|
+
* <Input value={placa} onChangeText={setPlaca} formatOnBlur={(v) => v.toUpperCase()} />
|
|
82
|
+
*
|
|
83
|
+
* ⚠ **Exige o campo CONTROLADO** (`value` + `onChangeText`), e a diferença com a web é honesta:
|
|
84
|
+
* lá o componente escreve em `e.currentTarget.value` e o `<input>` não controlado obedece. Aqui
|
|
85
|
+
* o texto do `TextInput` não controlado vive dentro do nó nativo, e escrevê-lo de fora exigiria
|
|
86
|
+
* `setNativeProps` — API que a Nova Arquitetura desencoraja. Então o que sai daqui é o valor
|
|
87
|
+
* formatado por `onChangeText`; quem guarda o estado desenha.
|
|
88
|
+
*
|
|
89
|
+
* Para MOEDA e MEDIDA não use isto: use o `NumberField`, que já faz a conta e devolve `number`.
|
|
90
|
+
*/
|
|
91
|
+
formatOnBlur?: (value: string) => string;
|
|
68
92
|
onBlur?: () => void;
|
|
69
93
|
onFocus?: () => void;
|
|
70
94
|
/** Várias linhas. É o que o `Textarea` liga — raramente se põe à mão. */
|
|
@@ -79,7 +103,7 @@ export interface InputProps {
|
|
|
79
103
|
* (`aurea.css:686`); **não há ponteiro no telefone**. O foco continua existindo e é o que marca o
|
|
80
104
|
* campo ativo.
|
|
81
105
|
*/
|
|
82
|
-
export declare function Input({ value, defaultValue, onChangeText, placeholder, disabled, size, keyboardType, secureTextEntry, autoCapitalize, onBlur, onFocus, multiline, style, testID, }: InputProps): React.JSX.Element;
|
|
106
|
+
export declare function Input({ value, defaultValue, onChangeText, placeholder, disabled, size, keyboardType, secureTextEntry, autoCapitalize, formatOnBlur, onBlur, onFocus, multiline, style, testID, }: InputProps): React.JSX.Element;
|
|
83
107
|
export interface TextareaProps extends InputProps {
|
|
84
108
|
/** Linhas visíveis. Vira altura mínima, porque no RN não há `rows`. */
|
|
85
109
|
rows?: number;
|
|
@@ -169,6 +193,12 @@ export interface SelectProps {
|
|
|
169
193
|
* ⚠ **O papel é `button` e não `combobox`.** `combobox` promete um campo em que se DIGITA para
|
|
170
194
|
* filtrar; isto só abre uma lista. Prometer o que não se faz é o mesmo defeito do `tablist` no
|
|
171
195
|
* Lote 3.
|
|
196
|
+
*
|
|
197
|
+
* ⚠ **Quem DIGITA para filtrar é o `Combobox`** (Lote 7, `busca.tsx`), e ele nasceu desta
|
|
198
|
+
* lacuna — o consumidor bateu nela no cadastro. **A escolha entre os dois é de tamanho de
|
|
199
|
+
* lista, e a linha é dura:** este monta todos os itens num `ScrollView` (abaixo), o que serve
|
|
200
|
+
* para unidades, estados e tipos; um catálogo de milhares de linhas aqui não fica lento, trava.
|
|
201
|
+
* O `Combobox` usa `FlatList` e aceita busca remota.
|
|
172
202
|
*/
|
|
173
203
|
export declare function Select({ items, value, onChange, placeholder, disabled, size, chevron, style, testID, }: SelectProps): React.JSX.Element;
|
|
174
204
|
export interface FormProps extends ViewProps {
|
package/dist/inputs.js
CHANGED
|
@@ -147,14 +147,25 @@ export function Label({ children, trailing, style, ...rest }) {
|
|
|
147
147
|
* (`aurea.css:686`); **não há ponteiro no telefone**. O foco continua existindo e é o que marca o
|
|
148
148
|
* campo ativo.
|
|
149
149
|
*/
|
|
150
|
-
export function Input({ value, defaultValue, onChangeText, placeholder, disabled, size, keyboardType, secureTextEntry, autoCapitalize, onBlur, onFocus, multiline, style, testID, }) {
|
|
150
|
+
export function Input({ value, defaultValue, onChangeText, placeholder, disabled, size, keyboardType, secureTextEntry, autoCapitalize, formatOnBlur, onBlur, onFocus, multiline, style, testID, }) {
|
|
151
151
|
const t = useAureaTokens();
|
|
152
152
|
const s = folha(t);
|
|
153
153
|
const campo = useCampo();
|
|
154
154
|
const tam = size ?? campo?.size ?? "md";
|
|
155
155
|
const inativo = disabled ?? campo?.disabled;
|
|
156
156
|
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: () => {
|
|
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: () => {
|
|
158
|
+
setFocado(false);
|
|
159
|
+
// Só avisa quando o texto MUDOU: emitir o mesmo valor a cada saída de foco faria o app
|
|
160
|
+
// re-renderizar sem motivo, e num formulário de dez campos isso é dez renders por
|
|
161
|
+
// preenchimento. Mesma guarda da web (`inputs-client.tsx:194`).
|
|
162
|
+
if (formatOnBlur && value != null) {
|
|
163
|
+
const formatado = formatOnBlur(value);
|
|
164
|
+
if (formatado !== value)
|
|
165
|
+
onChangeText?.(formatado);
|
|
166
|
+
}
|
|
167
|
+
onBlur?.();
|
|
168
|
+
},
|
|
158
169
|
// O nome e a dica vêm do `Field` — ver o bloco acima sobre por que a ligação da web não
|
|
159
170
|
// atravessa. Sem isto, o campo é um nó anônimo para o leitor de tela.
|
|
160
171
|
accessibilityLabel: campo?.label, accessibilityHint: campo?.hint, accessibilityState: { disabled: !!inativo }, "aria-invalid": campo?.invalido, style: [
|
|
@@ -319,6 +330,12 @@ export function SegmentedControl({ items, value, onChange, label, disabled, styl
|
|
|
319
330
|
* ⚠ **O papel é `button` e não `combobox`.** `combobox` promete um campo em que se DIGITA para
|
|
320
331
|
* filtrar; isto só abre uma lista. Prometer o que não se faz é o mesmo defeito do `tablist` no
|
|
321
332
|
* Lote 3.
|
|
333
|
+
*
|
|
334
|
+
* ⚠ **Quem DIGITA para filtrar é o `Combobox`** (Lote 7, `busca.tsx`), e ele nasceu desta
|
|
335
|
+
* lacuna — o consumidor bateu nela no cadastro. **A escolha entre os dois é de tamanho de
|
|
336
|
+
* lista, e a linha é dura:** este monta todos os itens num `ScrollView` (abaixo), o que serve
|
|
337
|
+
* para unidades, estados e tipos; um catálogo de milhares de linhas aqui não fica lento, trava.
|
|
338
|
+
* O `Combobox` usa `FlatList` e aceita busca remota.
|
|
322
339
|
*/
|
|
323
340
|
export function Select({ items, value, onChange, placeholder, disabled, size, chevron = "chevron--down", style, testID, }) {
|
|
324
341
|
const t = useAureaTokens();
|
package/dist/midia.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { type ImageSourcePropType, type StyleProp, type ViewStyle } from "react-native";
|
|
3
|
+
import { type IconName } from "./icon.js";
|
|
4
|
+
/** URL ou `require()` de um asset local — as duas formas do `Image` do RN, como no `Avatar`. */
|
|
5
|
+
export type AureaImageSource = ImageSourcePropType | string;
|
|
6
|
+
export interface ImageProps {
|
|
7
|
+
source: AureaImageSource;
|
|
8
|
+
/**
|
|
9
|
+
* O nome acessível. **É o `alt` da web, e é obrigatório pelo mesmo motivo que o `label` do
|
|
10
|
+
* `IconButton`:** uma foto sem texto alternativo não diz nada a quem não a vê. Decorativa de
|
|
11
|
+
* verdade se escreve `alt=""` — explícito, como manda a WAI.
|
|
12
|
+
*/
|
|
13
|
+
alt: string;
|
|
14
|
+
/** `16/9`, `"16/9"`, `"16:9"` ou `1.777…`. Sem ela, a imagem ocupa a altura que o `style` der. */
|
|
15
|
+
ratio?: number | string;
|
|
16
|
+
fit?: "cover" | "contain";
|
|
17
|
+
/** O que aparece no lugar quando o bitmap não vem. Sem ele, o glifo `image` sobre a caixa reservada. */
|
|
18
|
+
fallback?: React.ReactNode;
|
|
19
|
+
/** O glifo do substituto. Registre-o, ou passe `false`. */
|
|
20
|
+
fallbackIcon?: IconName | false;
|
|
21
|
+
/**
|
|
22
|
+
* Desenha OUTRO componente de imagem com a nossa pele — é o caso do consumidor que já usa
|
|
23
|
+
* `expo-image` por cache de disco.
|
|
24
|
+
*
|
|
25
|
+
* <Image render={<ExpoImage contentFit="cover" transition={150} />} source={u} alt="…" />
|
|
26
|
+
*
|
|
27
|
+
* Recebe `source`, `style`, `onError` e o nome acessível. **`fit` NÃO é traduzido para ele** —
|
|
28
|
+
* `resizeMode` é do `Image` do RN, e o `expo-image` chama a mesma coisa de `contentFit`; quem
|
|
29
|
+
* passa o elemento escreve a prop dele.
|
|
30
|
+
*/
|
|
31
|
+
render?: React.ReactElement;
|
|
32
|
+
onError?: () => void;
|
|
33
|
+
style?: StyleProp<ViewStyle>;
|
|
34
|
+
testID?: string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A foto, com a caixa reservada e um substituto quando ela não vem.
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* <Image source={foto.uri} alt="Frente do item" ratio="4/3" />
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* ⚠ **A caixa nasce reservada e pintada** com a mesma superfície do `Skeleton` — sem `ratio`, o
|
|
44
|
+
* layout salta quando o bitmap chega, e num telefone esse salto acontece com o dedo já a caminho
|
|
45
|
+
* do botão.
|
|
46
|
+
*
|
|
47
|
+
* ⚠ **O substituto continua sendo a imagem para quem usa leitor de tela**, com o mesmo `alt`.
|
|
48
|
+
* Trocar o bitmap por uma caixa muda o que se vê, não o que a foto É — e o `role="image"` mapeia
|
|
49
|
+
* nos dois sistemas (`ReactAccessibilityDelegate.kt:461`, `RCTConversions.h:96`).
|
|
50
|
+
*/
|
|
51
|
+
export declare function Image({ source, alt, ratio, fit, fallback, fallbackIcon, render, onError, style, testID, }: ImageProps): React.JSX.Element;
|
|
52
|
+
export interface AureaGalleryItem {
|
|
53
|
+
id: string;
|
|
54
|
+
source: AureaImageSource;
|
|
55
|
+
alt: string;
|
|
56
|
+
caption?: React.ReactNode;
|
|
57
|
+
}
|
|
58
|
+
export interface GalleryProps {
|
|
59
|
+
items: AureaGalleryItem[];
|
|
60
|
+
/** O nome da grade para o leitor de tela. Sem ele, a frase `galleryLabel` do provider. */
|
|
61
|
+
label?: string;
|
|
62
|
+
/** O `id` escolhido. **A galeria não guarda escolha** — é constante do app, como na web. */
|
|
63
|
+
selected?: string;
|
|
64
|
+
onSelect?: (id: string) => void;
|
|
65
|
+
/** Tocar abre a foto grande no `Dialog` que já existe. */
|
|
66
|
+
zoom?: boolean;
|
|
67
|
+
/** Proporção dos ladrilhos. Padrão **1** (quadrado), como o `ratio="1/1"` da web. */
|
|
68
|
+
ratio?: number | string;
|
|
69
|
+
/**
|
|
70
|
+
* Largura mínima de cada ladrilho, em dp. O `--gallery-min` da web é `8rem` — e `1rem = 16dp`,
|
|
71
|
+
* medido, então **128**.
|
|
72
|
+
*/
|
|
73
|
+
minTileWidth?: number;
|
|
74
|
+
style?: StyleProp<ViewStyle>;
|
|
75
|
+
testID?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* A grade de fotos, e a foto grande quando se toca.
|
|
79
|
+
*
|
|
80
|
+
* ```tsx
|
|
81
|
+
* <Gallery items={fotos} zoom selected={atual} onSelect={setAtual} />
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* ⚠ **Ampliar é DIÁLOGO, e diálogo já existe.** É a trava que a web escreveu no item L2 e ela
|
|
85
|
+
* atravessa inteira: a foto grande abre no `Dialog` do Lote 5, com o confinamento de foco que o
|
|
86
|
+
* `Modal` do RN dá pelo sistema e a saída pelo botão VOLTAR do Android. Uma segunda superfície
|
|
87
|
+
* flutuante aqui seria uma segunda linguagem.
|
|
88
|
+
*
|
|
89
|
+
* ⚠ **Legenda visível torna a miniatura decorativa**, e quem exigiu isso na web foi o axe, não a
|
|
90
|
+
* teoria: com `alt` e legenda dizendo a mesma coisa, o leitor de tela anuncia duas vezes seguidas.
|
|
91
|
+
* Aqui a tradução é outra (não há `alt=""` no RN) mas a regra é a mesma — **com legenda, quem
|
|
92
|
+
* carrega o nome é o LADRILHO, e a imagem sai da árvore**. O `alt` não se perde: ele continua
|
|
93
|
+
* nomeando a foto ampliada, que é onde não há legenda ao lado.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ **Sem `onSelect` e sem `zoom` os ladrilhos não são alvos.** Um `Pressable` que não faz nada é
|
|
96
|
+
* um alvo que engana quem navega por leitor de tela — mesma decisão da web, mesma razão.
|
|
97
|
+
*/
|
|
98
|
+
export declare function Gallery({ items, label, selected, onSelect, zoom, ratio, minTileWidth, style, testID, }: GalleryProps): React.JSX.Element;
|