@aurea-uds/native 0.14.1 → 0.15.0

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 CHANGED
@@ -73,17 +73,19 @@ Para o texto sair na Atkinson Hyperlegible (a fonte da Aurea desde a ADR-0053),
73
73
  O último costuma **já estar instalado**: `react-navigation` e `expo-router` o arrastam. Declará-lo
74
74
  como peer não adiciona peso ao app — só diz o que já ia estar lá.
75
75
 
76
- E **dois peers OPCIONAIS**, marcados como tal em `peerDependenciesMeta` — quem não usa data nem
77
- câmera não instala nada e não vê aviso:
76
+ E **três peers OPCIONAIS**, marcados como tal em `peerDependenciesMeta` — quem não usa data,
77
+ câmera nem arquivo não instala nada e não vê aviso. Instale só o da peça que usar:
78
78
 
79
79
  ```sh
80
80
  pnpm add @react-native-community/datetimepicker expo-image-picker
81
+ pnpm add expo-document-picker
81
82
  ```
82
83
 
83
84
  | | | |
84
85
  |---|---|---|
85
86
  | `@react-native-community/datetimepicker` | ≥8 | MIT · só para o `DatePicker` |
86
87
  | `expo-image-picker` | ≥16 | MIT · só para o `PhotoInput` |
88
+ | `expo-document-picker` | ≥12 | MIT · só para o `FileInput`, que sai por `@aurea-uds/native/system/file` |
87
89
 
88
90
  ## Tema × densidade
89
91
 
@@ -408,8 +410,13 @@ nega. Cada pergunta ficou com um padrão, e **cada padrão tem uma saída**:
408
410
  | e se a pessoa **negar**? | pergunta de novo na próxima vez, enquanto o sistema deixar | `onPermissionDenied` |
409
411
  | e se negar **de vez** (`canAskAgain: false`)? | um `Alert` de aviso + botão para as configurações | `offerSettings={false}` |
410
412
  | câmera ou galeria? | **câmera** | `source="library"` |
411
- | como remover? | um `IconButton` no canto de cada miniatura | `removeIcon` |
413
+ | como remover? | um X com fundo, **todo fora** da foto, no canto de cima à direita (R-22) | `removeIcon` |
412
414
  | quantas cabem? | **uma** | `max` |
415
+ | como **olhar** a foto? | tocar na miniatura abre a foto grande, no mesmo zoom da `Gallery` (R-22) | — |
416
+
417
+ **Desde a `0.15.0` (R-22)**, a miniatura é quadrada de 64 (o `Avatar` `lg` do HeroUI Native), do
418
+ tamanho do botão de pôr foto. O leitor de tela ouve *"Foto 2 de 3"* e *"Abre a foto"*; o X,
419
+ *"Remover foto 2"*. Com o campo inativo, a foto ainda abre e o X não remove.
413
420
 
414
421
  ⚠ **O gatilho some ao atingir o limite**, em vez de ficar aceso e não fazer nada — botão que
415
422
  existe e não responde é o defeito, não a proteção.
@@ -420,6 +427,39 @@ errada é um diálogo assustador que o app não precisava mostrar.
420
427
  As frases (`cameraDenied`, `openSettings`, `photoRemove`, `datePlaceholder`) saem da tabela de
421
428
  **Frases** — ver a seção abaixo.
422
429
 
430
+ ### `FileInput` — escolher arquivo (R-21, `0.15.0`)
431
+
432
+ Ele mora num caminho **só dele**, e não no `/system`:
433
+
434
+ ```tsx
435
+ import {FileInput} from "@aurea-uds/native/system/file";
436
+
437
+ <Field label="Documento">
438
+ <FileInput value={arquivos} onChange={setArquivos} accept="application/pdf"
439
+ maxSize={10 * 1024 * 1024} />
440
+ </Field>
441
+ ```
442
+
443
+ ⚠ **O caminho próprio é o que faz "opcional" ser verdade.** Se ele morasse no `/system`, todo app
444
+ que usa o `DatePicker` teria de instalar o `expo-document-picker`, ou a montagem falharia. Só quem
445
+ importa `/system/file` precisa dele.
446
+
447
+ | a pergunta | o que ficou | como mudar |
448
+ |---|---|---|
449
+ | o que volta? | `{uri, name, size, mimeType}` — o endereço local, **não o conteúdo** | — |
450
+ | a pessoa **cancelou**? | nada muda | — |
451
+ | passou do **tamanho**? | recusado, com *"Arquivo maior que o limite: nome"* em vermelho embaixo | `maxSize` |
452
+ | **tipo** errado? | o seletor filtra; a peça confere de novo e recusa | `accept` |
453
+ | o tamanho **não veio**? | aceito: não há o que medir, e o servidor confere | — |
454
+ | **quantos** cabem? | um; no limite o gatilho some, como no `PhotoInput` | `max` |
455
+ | **nome comprido**? | corta no meio (`relat…2026.pdf`): a extensão continua à vista | — |
456
+
457
+ Para o leitor de tela: o gatilho é um botão com o nome do `Field` e o texto *"Escolher arquivo"*;
458
+ cada arquivo lê *"nome, tamanho"*; o X lê *"Remover nome"*; a recusa é anunciada.
459
+
460
+ As frases `fileChoose`, `fileRemove`, `fileTooLarge` e `fileWrongType` saem da tabela de **Frases**;
461
+ as três últimas são as mesmas da web.
462
+
423
463
  ## Gráfico
424
464
 
425
465
  ```tsx
@@ -555,6 +595,14 @@ botão e não faria nada.
555
595
  ```tsx
556
596
  <Timeline items={[{title: "Criado", description: "pelo app", time: "08/09 14:20"}]} />
557
597
 
598
+ // R-18 (0.15.0): ícone em moldura, cor por tom, valor à direita e o que houve entre dois itens.
599
+ <Timeline items={[
600
+ {title: "Pedido enviado", description: "28/09", icon: "truck", tone: "success",
601
+ trailing: "R$ 216,00", between: "6 dias depois"},
602
+ {title: "Pagamento", description: "22/09", icon: "storefront", tone: "primary",
603
+ trailing: "R$ 460,00"},
604
+ ]} />
605
+
558
606
  <DataList items={[{term: "Quilometragem", value: "12,4 km/l"}]} />
559
607
  ```
560
608
 
@@ -567,6 +615,10 @@ num painel de oito pares perde qual valor era de qual termo.
567
615
  que a regra evita: a 320px a coluna do valor resolvia em **0px** e jogava o texto para fora da
568
616
  página. `layout="inline"` / `"stacked"` força, contra a largura.
569
617
 
618
+ ⚠ **Basta um item com `icon` para a coluna inteira ter a largura da moldura** (40): os itens sem
619
+ ícone ficam com o ponto, no centro dela, e a linha passa pelo centro das molduras. O `between` não
620
+ aparece depois do último item, porque não há próximo.
621
+
570
622
  ⚠ **No `Timeline` a contagem e a posição NÃO são anunciadas**, e é escolha. Na web ele é um
571
623
  `<ol>`, e a ficha diz que o leitor anuncia "1 de 40" de graça; aqui esse "de graça" não existe e
572
624
  teria de ser escrito item a item. Pendurar isso em cada evento de um histórico é ruído a cada
package/dist/actions.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { type PressableProps } from "react-native";
3
- import { type AureaIconRegistry, type IconName } from "./icon.js";
3
+ import { type AureaIconRegistry, type AureaIcon } from "./icon.js";
4
4
  /** Quanto peso a caixa tem. */
5
5
  export type AureaButtonAppearance = "solid" | "outline" | "ghost";
6
6
  /** O que a cor significa. Mesmos nomes da web (ADR-0044). */
@@ -12,8 +12,8 @@ export interface ButtonProps extends Omit<PressableProps, "children" | "style">
12
12
  appearance?: AureaButtonAppearance;
13
13
  tone?: AureaButtonTone;
14
14
  size?: AureaButtonSize;
15
- leadingIcon?: IconName;
16
- trailingIcon?: IconName;
15
+ leadingIcon?: AureaIcon;
16
+ trailingIcon?: AureaIcon;
17
17
  /**
18
18
  * Um desenho QUALQUER na frente do texto, para quando a marca não pode ser um ícone nosso.
19
19
  *
@@ -55,7 +55,7 @@ export interface LinkButtonProps extends Omit<ButtonProps, "appearance" | "fullW
55
55
  /** Botão-texto sem recuo nem caixa, alinhado com o texto em volta. O `LinkButton` do HeroUI. */
56
56
  export declare function LinkButton(props: LinkButtonProps): React.JSX.Element;
57
57
  export interface IconButtonProps extends Omit<ButtonProps, "children" | "leadingIcon" | "trailingIcon" | "fullWidth"> {
58
- name: IconName;
58
+ name: AureaIcon;
59
59
  /** **Obrigatório**: um botão que só tem glifo não tem texto para o leitor de tela anunciar. */
60
60
  label: string;
61
61
  }
@@ -0,0 +1,53 @@
1
+ import * as React from "react";
2
+ import { type StyleProp, type ViewStyle } from "react-native";
3
+ import { type AureaIcon } from "./icon.js";
4
+ /** O arquivo escolhido: o que o `expo-document-picker` devolve, sem o conteúdo. */
5
+ export type AureaFile = {
6
+ /** O endereço local do arquivo — é ele que o app envia. */
7
+ uri: string;
8
+ name: string;
9
+ /** Em bytes. Pode faltar: nem todo provedor de arquivos do Android informa. */
10
+ size?: number;
11
+ mimeType?: string;
12
+ };
13
+ export interface FileInputProps {
14
+ value?: AureaFile[];
15
+ onChange?: (arquivos: AureaFile[]) => void;
16
+ /**
17
+ * Os tipos aceitos, em MIME: `"application/pdf"`, `["image/*", "application/pdf"]`. É o filtro
18
+ * do seletor do sistema, e a peça confere de novo na volta. Padrão: qualquer arquivo.
19
+ */
20
+ accept?: string | string[];
21
+ /** Quantos arquivos cabem. Padrão: 1. Com o limite atingido, o gatilho some. */
22
+ max?: number;
23
+ /** O tamanho máximo, em bytes. O arquivo maior é recusado, com aviso embaixo do campo. */
24
+ maxSize?: number;
25
+ disabled?: boolean;
26
+ /** O glifo do gatilho. Padrão `paperclip`; `false` tira. */
27
+ addIcon?: AureaIcon | false;
28
+ /** O glifo de cada arquivo escolhido. Padrão `file-text`, o mesmo da web; `false` tira. */
29
+ fileIcon?: AureaIcon | false;
30
+ removeIcon?: AureaIcon;
31
+ style?: StyleProp<ViewStyle>;
32
+ testID?: string;
33
+ }
34
+ /** O tamanho para ler: a mesma conta do `formatSize` da web (`file-input.tsx`). */
35
+ export declare function formatarTamanho(bytes: number): string;
36
+ /** O arquivo bate com o `accept`? Sem `accept`, ou sem tipo informado, a resposta é sim. */
37
+ export declare function aceitaTipo(mimeType: string | undefined, accept: string | string[] | undefined): boolean;
38
+ /**
39
+ * Escolher arquivo do aparelho.
40
+ *
41
+ * | a pergunta | o que ficou |
42
+ * |---|---|
43
+ * | a pessoa **cancelou** o seletor? | nada muda: nem a lista, nem o aviso |
44
+ * | o arquivo passa do **`maxSize`**? | é recusado, e o aviso aparece embaixo, em vermelho |
45
+ * | o tipo não bate com o **`accept`**? | idem — o seletor do sistema filtra, a peça confere |
46
+ * | o tamanho **não veio**? | aceito: não há o que medir, e o servidor confere |
47
+ * | **quantos** cabem? | `max`, padrão 1; no limite o gatilho some, como no `PhotoInput` |
48
+ * | **nome comprido**? | corta no MEIO (`relat…2026.pdf`), para a extensão continuar à vista |
49
+ *
50
+ * Para o leitor de tela: o gatilho é um botão com o nome do `Field` e o texto dele; cada
51
+ * arquivo lê *"nome, tamanho"*; o X lê *"Remover nome"*; o aviso de recusa é anunciado.
52
+ */
53
+ export declare function FileInput({ value, onChange, accept, max, maxSize, disabled, addIcon, fileIcon, removeIcon, style, testID, }: FileInputProps): React.JSX.Element;
@@ -0,0 +1,134 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // Aurea nativo — o `FileInput`: escolher um arquivo do aparelho. R-21, 02/10/2026.
3
+ //
4
+ // Pedido do app: mandar um documento (PDF) ao servidor dele. A trava de importação do app proíbe
5
+ // o `expo-document-picker` nas telas, então a peça tem de ser da Aurea, no molde do `PhotoInput`.
6
+ // Proposta aprovada pelo Victor em 02/10/2026 (*"3 sim"*).
7
+ //
8
+ // ── AS REFERÊNCIAS ───────────────────────────────────────────────────────────────────────────
9
+ // HeroUI não tem: nem o `heroui-native` 1.0.10, nem o `@heroui/styles` 3.2.6 (medido nos pacotes).
10
+ // ReUI tem (`reui.io/docs/components/base/file-upload`): `accept`, `maxSize`, `maxFiles`,
11
+ // `multiple`, `onFilesChange`. Daqui vêm `accept` e `maxSize`, que são também os nomes
12
+ // do `FileInput` da web. O limite é `max`, e não `maxFiles`: é o nome do `PhotoInput`,
13
+ // o irmão que mora ao lado.
14
+ // web o `FileInput` do `@aurea-uds/react`: as frases (`fileTooLarge`, `fileWrongType`,
15
+ // `fileRemove`) e a conta do tamanho (`formatSize`) são as de lá.
16
+ //
17
+ // ── A DEPENDÊNCIA ────────────────────────────────────────────────────────────────────────────
18
+ // expo-document-picker MIT · ~57.0.1 fixado pelo Expo SDK 57 · peer OPCIONAL, `>=12`
19
+ // O formato de `getDocumentAsync` que este arquivo usa (`canceled`, `assets`, `multiple`, `type`
20
+ // como texto ou lista) é o mesmo da 12.0.1 à 57.0.3 — medido nos `types.d.ts` das duas.
21
+ //
22
+ // ── ⚠ UM CAMINHO SÓ DELE: `@aurea-uds/native/system/file` ─────────────────────────────────────
23
+ // Não no `/system` junto do `DatePicker` e do `PhotoInput`, e é a mesma razão da ADR-0038: o
24
+ // Metro resolve todo `import` do arquivo que o app importa. Se o `FileInput` morasse no
25
+ // `sistema.tsx`, **todo app que usa o `DatePicker` teria de instalar o `expo-document-picker`**,
26
+ // ou a montagem falharia — o README do pacote já descreve esse defeito. Caminho próprio, e só
27
+ // quem escolhe arquivo instala.
28
+ //
29
+ // ── O QUE A PEÇA NÃO FAZ ─────────────────────────────────────────────────────────────────────
30
+ // Não lê o arquivo e não envia nada: devolve o endereço local (`uri`), o nome, o tamanho e o
31
+ // tipo. Quem envia é o app, e quem confere o conteúdo é o servidor dele.
32
+ import * as React from "react";
33
+ import { Pressable, View } from "react-native";
34
+ import * as DocumentPicker from "expo-document-picker";
35
+ import { IconButton } from "./actions.js";
36
+ import { criarFolha, estadoAcessivel } from "./estilos.js";
37
+ import { Icon } from "./icon.js";
38
+ import { alturaDoTamanho, respiroDoTamanho, useCampo } from "./inputs.js";
39
+ import { Text } from "./text.js";
40
+ import { useAureaStrings, useAureaTokens } from "./theme.js";
41
+ const folha = criarFolha((t) => ({
42
+ pilha: { gap: t.size.space2 },
43
+ // O gatilho e a linha do arquivo têm a pele do campo: mesma borda, mesmo fundo, cápsula.
44
+ caixa: {
45
+ flexDirection: "row", alignItems: "center", gap: t.size.space2,
46
+ width: "100%", minWidth: 0, borderWidth: t.size.borderWidth, borderColor: t.color.borderStrong,
47
+ borderRadius: t.size.radiusControl, backgroundColor: t.color.fieldBg,
48
+ },
49
+ invalido: { borderColor: t.color.danger400 ?? t.color.destructive },
50
+ desabilitado: { opacity: t.size.opacityDisabled },
51
+ nomeETamanho: { flex: 1, minWidth: 0, flexDirection: "row", alignItems: "center", gap: t.size.space2 },
52
+ nome: { flexShrink: 1 },
53
+ }));
54
+ /** O tamanho para ler: a mesma conta do `formatSize` da web (`file-input.tsx`). */
55
+ export function formatarTamanho(bytes) {
56
+ if (bytes < 1024)
57
+ return `${bytes} B`;
58
+ const unidades = ["KB", "MB", "GB"];
59
+ let n = bytes / 1024, i = 0;
60
+ while (n >= 1024 && i < unidades.length - 1) {
61
+ n /= 1024;
62
+ i++;
63
+ }
64
+ return `${n < 10 ? n.toFixed(1) : Math.round(n)} ${unidades[i]}`;
65
+ }
66
+ /** O arquivo bate com o `accept`? Sem `accept`, ou sem tipo informado, a resposta é sim. */
67
+ export function aceitaTipo(mimeType, accept) {
68
+ if (accept == null || !mimeType)
69
+ return true;
70
+ const tipo = mimeType.toLowerCase();
71
+ return (Array.isArray(accept) ? accept : [accept]).some((padrao) => {
72
+ const p = padrao.trim().toLowerCase();
73
+ if (p === "*/*" || p === tipo)
74
+ return true;
75
+ return p.endsWith("/*") && tipo.startsWith(p.slice(0, -1));
76
+ });
77
+ }
78
+ /**
79
+ * Escolher arquivo do aparelho.
80
+ *
81
+ * | a pergunta | o que ficou |
82
+ * |---|---|
83
+ * | a pessoa **cancelou** o seletor? | nada muda: nem a lista, nem o aviso |
84
+ * | o arquivo passa do **`maxSize`**? | é recusado, e o aviso aparece embaixo, em vermelho |
85
+ * | o tipo não bate com o **`accept`**? | idem — o seletor do sistema filtra, a peça confere |
86
+ * | o tamanho **não veio**? | aceito: não há o que medir, e o servidor confere |
87
+ * | **quantos** cabem? | `max`, padrão 1; no limite o gatilho some, como no `PhotoInput` |
88
+ * | **nome comprido**? | corta no MEIO (`relat…2026.pdf`), para a extensão continuar à vista |
89
+ *
90
+ * Para o leitor de tela: o gatilho é um botão com o nome do `Field` e o texto dele; cada
91
+ * arquivo lê *"nome, tamanho"*; o X lê *"Remover nome"*; o aviso de recusa é anunciado.
92
+ */
93
+ export function FileInput({ value = [], onChange, accept, max = 1, maxSize, disabled, addIcon = "paperclip", fileIcon = "file-text", removeIcon = "x", style, testID, }) {
94
+ const t = useAureaTokens();
95
+ const s = folha(t);
96
+ const strings = useAureaStrings();
97
+ const campo = useCampo();
98
+ const tam = campo?.size ?? "md";
99
+ const inativo = disabled ?? campo?.disabled;
100
+ const [recusados, setRecusados] = React.useState([]);
101
+ const cheio = value.length >= max;
102
+ const medida = { height: alturaDoTamanho(t, tam), paddingHorizontal: respiroDoTamanho(t, tam) };
103
+ const escolher = React.useCallback(async () => {
104
+ const r = await DocumentPicker.getDocumentAsync({
105
+ ...(accept != null ? { type: accept } : {}),
106
+ multiple: max - value.length > 1,
107
+ });
108
+ // Cancelar não muda nada — nem apaga o aviso de uma recusa anterior.
109
+ if (r.canceled || !r.assets?.length)
110
+ return;
111
+ const aceitos = [];
112
+ const fora = [];
113
+ for (const a of r.assets) {
114
+ if (!aceitaTipo(a.mimeType, accept)) {
115
+ fora.push({ nome: a.name, motivo: "tipo" });
116
+ continue;
117
+ }
118
+ if (maxSize != null && a.size != null && a.size > maxSize) {
119
+ fora.push({ nome: a.name, motivo: "tamanho" });
120
+ continue;
121
+ }
122
+ aceitos.push({ uri: a.uri, name: a.name, size: a.size, mimeType: a.mimeType });
123
+ }
124
+ setRecusados(fora);
125
+ const cabem = aceitos.slice(0, max - value.length);
126
+ if (cabem.length)
127
+ onChange?.([...value, ...cabem]);
128
+ }, [accept, max, maxSize, value, onChange]);
129
+ return (_jsxs(View, { testID: testID, style: [s.pilha, style], children: [value.map((arquivo, n) => {
130
+ const tamanho = arquivo.size != null ? formatarTamanho(arquivo.size) : null;
131
+ return (_jsxs(View, { style: [s.caixa, medida, { paddingRight: 0 }], children: [_jsxs(View, { style: s.nomeETamanho, accessible: true, accessibilityLabel: tamanho ? `${arquivo.name}, ${tamanho}` : arquivo.name, children: [fileIcon && _jsx(Icon, { name: fileIcon, size: "sm", color: t.color.subtleForeground }), _jsx(Text, { size: "sm", numberOfLines: 1, ellipsizeMode: "middle", style: s.nome, children: arquivo.name }), tamanho && _jsx(Text, { size: "xs", tone: "muted", children: tamanho })] }), _jsx(IconButton, { appearance: "ghost", size: "sm", name: removeIcon, label: `${strings.fileRemove} ${arquivo.name}`, disabled: inativo, onPress: () => onChange?.(value.filter((_, i) => i !== n)) })] }, `${arquivo.uri}-${n}`));
132
+ }), !cheio && (_jsxs(Pressable, { onPress: inativo ? undefined : escolher, disabled: inativo, accessibilityRole: "button", accessibilityLabel: campo?.label ? `${campo.label}, ${strings.fileChoose}` : strings.fileChoose, accessibilityHint: campo?.hint, ...estadoAcessivel({ disabled: !!inativo }), style: [s.caixa, medida, (campo?.invalido || recusados.length > 0) && s.invalido,
133
+ inativo && s.desabilitado], children: [addIcon && _jsx(Icon, { name: addIcon, size: "sm", color: t.color.mutedForeground }), _jsx(Text, { size: tam === "sm" ? "xs" : tam === "lg" ? "base" : "md", tone: "muted", numberOfLines: 1, children: strings.fileChoose })] })), recusados.length > 0 && (_jsx(View, { accessibilityLiveRegion: "polite", children: recusados.map((r, n) => (_jsx(Text, { size: "xs", tone: "danger", children: `${r.motivo === "tamanho" ? strings.fileTooLarge : strings.fileWrongType}: ${r.nome}` }, n))) }))] }));
134
+ }
package/dist/busca.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { type TextInputProps, type StyleProp, type ViewStyle } from "react-native";
3
- import { type IconName } from "./icon.js";
3
+ import { type AureaIcon } from "./icon.js";
4
4
  import { type AureaFieldSize } from "./inputs.js";
5
5
  /** Uma linha do catálogo. **Mesma forma da web** (`ComboboxOption`), mais o `disabled` que o `Select` daqui já tinha. */
6
6
  export interface AureaComboboxItem {
@@ -62,9 +62,9 @@ export interface ComboboxProps {
62
62
  disabled?: boolean;
63
63
  size?: AureaFieldSize;
64
64
  /** O glifo da seta. Registre-o, ou passe `false`. */
65
- chevron?: IconName | false;
65
+ chevron?: AureaIcon | false;
66
66
  /** O glifo da lupa no campo da folha. Registre-o, ou passe `false`. */
67
- searchIcon?: IconName | false;
67
+ searchIcon?: AureaIcon | false;
68
68
  /**
69
69
  * O teclado do campo de busca da folha — E6, 25/09/2026. Para buscar um ANO ou um código, passe
70
70
  * `"number-pad"`: sem isto abre o teclado de letras. É o `keyboardType` do `TextInput`, com o
@@ -115,7 +115,7 @@ export interface SearchFieldProps {
115
115
  disabled?: boolean;
116
116
  size?: AureaFieldSize;
117
117
  /** O glifo da lupa. Padrão `search`; `false` tira. */
118
- icon?: IconName | false;
118
+ icon?: AureaIcon | false;
119
119
  /** Mostra o botão de limpar quando há texto. Padrão **true**. */
120
120
  clearable?: boolean;
121
121
  onSubmit?: () => void;
package/dist/busca.js CHANGED
@@ -73,7 +73,7 @@ import { criarFolha, estadoAcessivel } from "./estilos.js";
73
73
  import { Spinner } from "./feedback.js";
74
74
  import { Icon } from "./icon.js";
75
75
  import { KeyboardAvoiding, useCampo } from "./inputs.js";
76
- import { useReduceMotion } from "./movimento.js";
76
+ import { useReduceMotion, driverNativo } from "./movimento.js";
77
77
  import { RecuoDaFolha } from "./screen.js";
78
78
  import { Text } from "./text.js";
79
79
  import { useAureaStrings, useAureaTokens, ForaDaMarca, usePeleSobreAMarca } from "./theme.js";
@@ -283,7 +283,7 @@ export function Combobox({ items, value, onValueChange, onSearchChange, searchDe
283
283
  return;
284
284
  }
285
285
  Animated.timing(arrasto, {
286
- toValue: 0, duration: t.size.durationSlow, useNativeDriver: true,
286
+ toValue: 0, duration: t.size.durationSlow, useNativeDriver: driverNativo(),
287
287
  easing: Easing.bezier(...t.easing.easeEmphasized),
288
288
  }).start();
289
289
  },
package/dist/data.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type StyleProp, type ViewStyle } from "react-native";
3
+ import type { AureaBadgeTone } from "./display.js";
4
+ import type { AureaIcon } from "./icon.js";
3
5
  type Virtualizavel = {
4
6
  /** Vira `FlatList`. ⚠ Então este componente é o rolador — não o ponha dentro de `<Screen scroll>`. */
5
7
  virtualized?: boolean;
@@ -9,6 +11,22 @@ export type AureaTimelineItem = {
9
11
  description?: React.ReactNode;
10
12
  /** Nó, não string: uma data absoluta e uma relativa são igualmente bem-vindas — como na web. */
11
13
  time?: React.ReactNode;
14
+ /**
15
+ * R-18, 02/10/2026: o glifo numa moldura redonda, no lugar do ponto. Moldura de 40 (o `Avatar`
16
+ * `sm` do HeroUI Native) e glifo de 20. Basta um item com `icon` para a coluna inteira ter a
17
+ * largura da moldura — os itens sem ícone ficam com o ponto, no centro dela.
18
+ */
19
+ icon?: AureaIcon;
20
+ /** A cor da moldura, com o vocabulário do `Badge`. Sem `icon`, não faz nada. Padrão: `neutral`. */
21
+ tone?: AureaBadgeTone;
22
+ /** O que vai à DIREITA do título — um valor, um selo. */
23
+ trailing?: React.ReactNode;
24
+ /**
25
+ * O que aconteceu ENTRE este item e o próximo, escrito ao lado da linha: *"6 dias depois"*,
26
+ * *"3 h depois"*. Proposta aprovada na prancha de 02/10/2026: a linha deixa de ser enfeite e
27
+ * passa a dizer alguma coisa. No último item não aparece, porque não há próximo.
28
+ */
29
+ between?: React.ReactNode;
12
30
  };
13
31
  export interface TimelineProps extends Virtualizavel {
14
32
  items: readonly AureaTimelineItem[];
package/dist/data.js CHANGED
@@ -57,6 +57,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
57
57
  import * as React from "react";
58
58
  import { FlatList, Pressable, View, useWindowDimensions, } from "react-native";
59
59
  import { criarFolha } from "./estilos.js";
60
+ import { IconeEmMoldura } from "./moldura.js";
60
61
  import { Text } from "./text.js";
61
62
  import { useAureaStrings, useAureaTokens } from "./theme.js";
62
63
  const folha = criarFolha((t) => ({
@@ -79,6 +80,14 @@ const folha = criarFolha((t) => ({
79
80
  borderWidth: t.size.space05, borderColor: t.color.surface1,
80
81
  },
81
82
  colunaDoEvento: { flex: 1, minWidth: 0, gap: t.size.space05 },
83
+ // R-18: o ponto centrado na coluna da moldura, quando a lista tem molduras.
84
+ colunaDoPonto: { width: t.size.space10, alignItems: "center", flexGrow: 0, flexShrink: 0 },
85
+ linhaDoTitulo: {
86
+ flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between",
87
+ gap: t.size.space2,
88
+ },
89
+ tituloQueEncolhe: { flexShrink: 1 },
90
+ entre: { flexDirection: "row", alignItems: "center" },
82
91
  // ── DataList ──
83
92
  pares: { gap: t.size.space2 },
84
93
  parEmpilhado: { gap: t.size.space05 },
@@ -123,23 +132,39 @@ const EMPILHA_ABAIXO_DE = 640;
123
132
  export function Timeline({ items, virtualized = false, style, testID }) {
124
133
  const t = useAureaTokens();
125
134
  const s = folha(t);
126
- const desenhar = React.useCallback((item) => (_jsxs(View, { style: s.evento, children: [_jsx(View, { style: s.ponto }), _jsxs(View, { style: s.colunaDoEvento, children: [typeof item.title === "string"
127
- ? _jsx(Text, { size: "md", weight: 600, children: item.title }) : item.title, item.description != null
128
- ? (typeof item.description === "string"
129
- ? _jsx(Text, { size: "sm", tone: "muted", children: item.description }) : item.description)
130
- : null, item.time != null
131
- ? (typeof item.time === "string"
132
- ? _jsx(Text, { size: "xs", tone: "muted", children: item.time }) : item.time)
133
- : null] })] })), [s]);
135
+ // A coluna do marcador tem a largura da moldura quando ALGUM item tem ícone: senão os títulos
136
+ // ficariam em colunas diferentes conforme o item. O trilho vai ao centro dela.
137
+ const comMoldura = items.some((i) => i.icon != null);
138
+ const marcador = comMoldura ? t.size.space10 : t.size.space3;
139
+ const desenhar = React.useCallback((item, n) => {
140
+ const titulo = typeof item.title === "string"
141
+ ? _jsx(Text, { size: "md", weight: 600, style: s.tituloQueEncolhe, children: item.title }) : item.title;
142
+ const ponto = item.icon != null
143
+ ? _jsx(IconeEmMoldura, { icon: item.icon, tone: item.tone, moldura: t.size.space10, glifo: t.size.iconMd, sobre: t.color.surface1 })
144
+ : comMoldura ? _jsx(View, { style: s.colunaDoPonto, children: _jsx(View, { style: s.ponto }) })
145
+ : _jsx(View, { style: s.ponto });
146
+ const entre = item.between != null && n < items.length - 1 ? (_jsx(View, { style: [s.entre, { paddingLeft: marcador + t.size.space3 }], children: typeof item.between === "string"
147
+ ? _jsx(Text, { size: "xs", tone: "muted", children: item.between }) : item.between })) : null;
148
+ return (_jsxs(_Fragment, { children: [_jsxs(View, { style: s.evento, children: [ponto, _jsxs(View, { style: s.colunaDoEvento, children: [item.trailing != null ? (_jsxs(View, { style: s.linhaDoTitulo, children: [titulo, typeof item.trailing === "string"
149
+ ? _jsx(Text, { size: "md", weight: 600, children: item.trailing }) : item.trailing] })) : titulo, item.description != null
150
+ ? (typeof item.description === "string"
151
+ ? _jsx(Text, { size: "sm", tone: "muted", children: item.description }) : item.description)
152
+ : null, item.time != null
153
+ ? (typeof item.time === "string"
154
+ ? _jsx(Text, { size: "xs", tone: "muted", children: item.time }) : item.time)
155
+ : null] })] }), entre] }));
156
+ }, [s, t, comMoldura, marcador, items.length]);
134
157
  if (virtualized) {
135
- return (_jsx(FlatList, { accessibilityRole: "list", data: items, keyExtractor: (_, n) => String(n), renderItem: ({ item }) => desenhar(item), ItemSeparatorComponent: () => _jsx(View, { style: { height: t.size.space5 } }),
158
+ return (_jsx(FlatList, { accessibilityRole: "list", data: items, keyExtractor: (_, n) => String(n),
159
+ // O `between` mora dentro da célula, então leva o mesmo vão que o separador dá entre elas.
160
+ renderItem: ({ item, index }) => _jsx(View, { style: s.trilha, children: desenhar(item, index) }), ItemSeparatorComponent: () => _jsx(View, { style: { height: t.size.space5 } }),
136
161
  // ⚠ O trilho NÃO entra no modo virtualizado, e isso é honesto em vez de quebrado: ele é
137
162
  // uma linha absoluta do topo ao pé da lista INTEIRA, e no `FlatList` a lista inteira não
138
163
  // existe — só a janela. Desenhá-lo daria um trilho que começa e termina no lugar errado
139
164
  // conforme se rola. Os pontos continuam, e são eles que marcam os eventos.
140
165
  style: style, testID: testID }));
141
166
  }
142
- return (_jsxs(View, { accessibilityRole: "list", style: [s.trilha, style], testID: testID, children: [items.length > 0 ? _jsx(View, { style: s.trilho }) : null, items.map((item, n) => _jsx(React.Fragment, { children: desenhar(item) }, n))] }));
167
+ return (_jsxs(View, { accessibilityRole: "list", style: [s.trilha, style], testID: testID, children: [items.length > 0 ? _jsx(View, { style: [s.trilho, comMoldura && { left: marcador / 2 }] }) : null, items.map((item, n) => _jsx(React.Fragment, { children: desenhar(item, n) }, n))] }));
143
168
  }
144
169
  /**
145
170
  * Os pares termo → valor.
package/dist/display.js CHANGED
@@ -23,7 +23,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
23
  // perderia o cartão, que é metade do que ela é.
24
24
  import * as React from "react";
25
25
  import { Image, View } from "react-native";
26
- import { criarFolha } from "./estilos.js";
26
+ import { acentoDoTom, criarFolha, fundoDoTom } from "./estilos.js";
27
27
  import { Card } from "./layout.js";
28
28
  import { gravidadeDoEstado } from "./strings.js";
29
29
  import { Text } from "./text.js";
@@ -103,24 +103,15 @@ const LETRA_DO_SELO = (t, size) => size === "lg" ? { fontSize: t.size.textBase,
103
103
  export function Badge({ tone = "neutral", emphasis = "soft", size = "md", dot, count, max = 99, showZero, leading, trailing, fit = "auto", anchor, badgeContent, invisible, children, style, ...rest }) {
104
104
  const t = useAureaTokens();
105
105
  const s = folha(t);
106
- const acento = tone === "primary" ? t.color.primaryEmphasis
107
- : tone === "info" ? t.color.info400 ?? t.color.info
108
- : tone === "success" ? t.color.success400 ?? t.color.success
109
- : tone === "warning" ? t.color.warning400 ?? t.color.warning
110
- : tone === "danger" ? t.color.danger400 ?? t.color.destructive
111
- : t.color.secondaryForeground;
112
- const fundoDoTom = tone === "info" ? t.color.infoBg
113
- : tone === "success" ? t.color.successBg
114
- : tone === "warning" ? t.color.warningBg
115
- : tone === "danger" ? t.color.dangerBg
116
- : undefined;
106
+ const acento = acentoDoTom(t, tone);
107
+ const fundo = fundoDoTom(t, tone);
117
108
  const pele = emphasis === "solid"
118
109
  ? { backgroundColor: acento, borderColor: "transparent" }
119
110
  : emphasis === "outline"
120
111
  ? { backgroundColor: "transparent", borderColor: acento }
121
112
  : tone === "neutral"
122
113
  ? {}
123
- : { backgroundColor: fundoDoTom, borderColor: acento };
114
+ : { backgroundColor: fundo, borderColor: acento };
124
115
  const corDoTexto = emphasis === "solid" ? t.color.background
125
116
  : tone === "neutral" ? t.color.secondaryForeground : acento;
126
117
  const numero = count != null ? formatarContagem(count, max) : undefined;
package/dist/estilos.d.ts CHANGED
@@ -33,6 +33,18 @@ export declare function criarFolha<T extends NamedStyles<T> | NamedStyles<Record
33
33
  * um ícone) não tinha como escrevê-lo sem número à mão. O `check 39` cobra o teste dele.
34
34
  */
35
35
  export declare const comOpacidade: (cor: string, pct: number) => string;
36
+ /** O tom de cor com significado — o vocabulário do `Badge` (`AureaBadgeTone`). */
37
+ export type TomDeCor = "neutral" | "primary" | "info" | "success" | "warning" | "danger";
38
+ /**
39
+ * A cor FORTE de um tom: o texto do selo, o ícone da moldura.
40
+ *
41
+ * ⚠ **Morava dentro do `Badge` e mudou de casa em 02/10/2026**, quando a moldura do ícone (R-15 e
42
+ * R-18) passou a precisar da mesma cor por tom. Duas cópias do mapa é o defeito que o `CLAUDE.md`
43
+ * nomeia — e o dia em que um tom novo entra numa só.
44
+ */
45
+ export declare const acentoDoTom: (t: AureaTokens, tom: TomDeCor) => string;
46
+ /** O fundo SUAVE de um tom (`--<tom>-bg`). `primary` e `neutral` não têm token: `undefined`. */
47
+ export declare const fundoDoTom: (t: AureaTokens, tom: TomDeCor) => string | undefined;
36
48
  /**
37
49
  * 🔴 O FIO AMARELO DO ESCOLHIDO — o sinal único da casa para "este é o escolhido".
38
50
  *
package/dist/estilos.js CHANGED
@@ -49,6 +49,25 @@ export function criarFolha(descrever) {
49
49
  export const comOpacidade = (cor, pct) => /^#[0-9a-fA-F]{6}$/.test(cor)
50
50
  ? cor + Math.round(Math.max(0, Math.min(1, pct)) * 255).toString(16).padStart(2, "0")
51
51
  : cor;
52
+ /**
53
+ * A cor FORTE de um tom: o texto do selo, o ícone da moldura.
54
+ *
55
+ * ⚠ **Morava dentro do `Badge` e mudou de casa em 02/10/2026**, quando a moldura do ícone (R-15 e
56
+ * R-18) passou a precisar da mesma cor por tom. Duas cópias do mapa é o defeito que o `CLAUDE.md`
57
+ * nomeia — e o dia em que um tom novo entra numa só.
58
+ */
59
+ export const acentoDoTom = (t, tom) => tom === "primary" ? t.color.primaryEmphasis
60
+ : tom === "info" ? t.color.info400 ?? t.color.info
61
+ : tom === "success" ? t.color.success400 ?? t.color.success
62
+ : tom === "warning" ? t.color.warning400 ?? t.color.warning
63
+ : tom === "danger" ? t.color.danger400 ?? t.color.destructive
64
+ : t.color.secondaryForeground;
65
+ /** O fundo SUAVE de um tom (`--<tom>-bg`). `primary` e `neutral` não têm token: `undefined`. */
66
+ export const fundoDoTom = (t, tom) => tom === "info" ? t.color.infoBg
67
+ : tom === "success" ? t.color.successBg
68
+ : tom === "warning" ? t.color.warningBg
69
+ : tom === "danger" ? t.color.dangerBg
70
+ : undefined;
52
71
  /**
53
72
  * 🔴 O FIO AMARELO DO ESCOLHIDO — o sinal único da casa para "este é o escolhido".
54
73
  *
@@ -1,6 +1,6 @@
1
1
  import * as React from "react";
2
2
  import { type StyleProp, type ViewProps, type ViewStyle } from "react-native";
3
- import { type IconName } from "./icon.js";
3
+ import { type IconName, type AureaIcon } from "./icon.js";
4
4
  import { type AureaUniversalState } from "./strings.js";
5
5
  export type AureaSpinnerSize = "sm" | "md" | "lg";
6
6
  export interface SpinnerProps {
@@ -65,7 +65,7 @@ export interface AlertProps extends ViewProps {
65
65
  /** Um dos sete estados universais. Ele **escolhe a variante** e escreve o texto padrão. */
66
66
  state?: AureaUniversalState;
67
67
  title?: React.ReactNode;
68
- icon?: IconName;
68
+ icon?: AureaIcon;
69
69
  onDismiss?: () => void;
70
70
  children?: React.ReactNode;
71
71
  }
@@ -86,7 +86,12 @@ export interface AlertProps extends ViewProps {
86
86
  */
87
87
  export declare function Alert({ variant, state, title, icon, onDismiss, children, style, accessibilityLabel, accessibilityHint, ...rest }: AlertProps): React.JSX.Element;
88
88
  export interface EmptyStateProps {
89
- icon?: IconName;
89
+ /**
90
+ * O glifo, dentro de uma moldura redonda — R-15, 02/10/2026, escolhida pelo Victor na prancha
91
+ * (*"1 c"*). Moldura de 64 (o `Avatar` `lg` do HeroUI) e glifo de 32, o de antes. O desenho
92
+ * do próprio app entra aqui também (`icon={Logo}`, R-11).
93
+ */
94
+ icon?: AureaIcon;
90
95
  title: React.ReactNode;
91
96
  description?: React.ReactNode;
92
97
  action?: React.ReactNode;
@@ -110,7 +115,7 @@ export interface DataStateProps extends Omit<ViewProps, "children"> {
110
115
  /** Substitui o esqueleto padrão de `loading`. */
111
116
  skeleton?: React.ReactNode;
112
117
  emptyTitle?: React.ReactNode;
113
- emptyIcon?: IconName;
118
+ emptyIcon?: AureaIcon;
114
119
  action?: React.ReactNode;
115
120
  /** Função para o conteúdo caro não ser construído enquanto o estado o esconde. */
116
121
  children: React.ReactNode | (() => React.ReactNode);
package/dist/feedback.js CHANGED
@@ -24,7 +24,8 @@ import { Animated, Easing, View } from "react-native";
24
24
  import { IconButton } from "./actions.js";
25
25
  import { criarFolha, estadoAcessivel } from "./estilos.js";
26
26
  import { Icon } from "./icon.js";
27
- import { useReduceMotion } from "./movimento.js";
27
+ import { IconeEmMoldura } from "./moldura.js";
28
+ import { useReduceMotion, driverNativo } from "./movimento.js";
28
29
  import { gravidadeDoEstado } from "./strings.js";
29
30
  import { Text } from "./text.js";
30
31
  import { useAureaStrings, useAureaTokens } from "./theme.js";
@@ -96,7 +97,7 @@ export function Spinner({ size = "sm", label, decorative, style, testID }) {
96
97
  if (reduzir !== false)
97
98
  return;
98
99
  const laco = Animated.loop(Animated.timing(giro, {
99
- toValue: 1, duration: 700, easing: Easing.linear, useNativeDriver: true,
100
+ toValue: 1, duration: 700, easing: Easing.linear, useNativeDriver: driverNativo(),
100
101
  }));
101
102
  laco.start();
102
103
  // Sem o `stop` o laço sobrevive à desmontagem e continua acordando o JS — o vazamento
@@ -137,8 +138,8 @@ export function Skeleton({ width, height, radius, style, testID }) {
137
138
  // 1,35 s no total, com o vale em .55 na METADE — é o `@keyframes skeleton-pulse` do
138
139
  // `aurea.css:1112` lido literalmente: 675 ms para descer e 675 para voltar.
139
140
  const laco = Animated.loop(Animated.sequence([
140
- Animated.timing(opacidade, { toValue: 0.55, duration: 675, easing: Easing.inOut(Easing.ease), useNativeDriver: true }),
141
- Animated.timing(opacidade, { toValue: 1, duration: 675, easing: Easing.inOut(Easing.ease), useNativeDriver: true }),
141
+ Animated.timing(opacidade, { toValue: 0.55, duration: 675, easing: Easing.inOut(Easing.ease), useNativeDriver: driverNativo() }),
142
+ Animated.timing(opacidade, { toValue: 1, duration: 675, easing: Easing.inOut(Easing.ease), useNativeDriver: driverNativo() }),
142
143
  ]));
143
144
  laco.start();
144
145
  return () => laco.stop();
@@ -220,7 +221,7 @@ export function EmptyState({ icon = "file", title, description, action, state, s
220
221
  const s = folha(t);
221
222
  const strings = useAureaStrings();
222
223
  const desc = description ?? (state ? strings.universalState[state] : null);
223
- return (_jsxs(View, { testID: testID, style: [s.vazio, style], children: [_jsx(Icon, { name: icon, size: "xl", color: t.color.subtleForeground }), typeof title === "string"
224
+ return (_jsxs(View, { testID: testID, style: [s.vazio, style], children: [_jsx(IconeEmMoldura, { icon: icon, moldura: t.size.space16, glifo: t.size.iconXl }), typeof title === "string"
224
225
  ? _jsx(Text, { size: "base", weight: 600, leading: "tight", align: "center", children: title })
225
226
  : title, desc != null && (typeof desc === "string"
226
227
  ? _jsx(Text, { size: "sm", tone: "muted", align: "center", children: desc })