@aurea-uds/native 0.16.0 → 0.17.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.
@@ -14,7 +14,7 @@ export interface AureaNavItem {
14
14
  export type AureaBottomNavVariant = "floating" | "edge";
15
15
  /** Quanto a pílula ocupa: a tela menos a margem (`full`, o de sempre) ou só as abas (`content`). */
16
16
  export type AureaBottomNavWidth = "full" | "content";
17
- export type AureaBottomNavIndicator = "none" | "subtle" | "pill" | "circle" | "circle-raised" | "circle-outline" | "circle-bold";
17
+ export type AureaBottomNavIndicator = "none" | "subtle" | "pill" | "circle" | "circle-raised" | "circle-outline" | "circle-bold" | "capsule" | "expand";
18
18
  export interface BottomNavProps extends ViewProps {
19
19
  items: AureaNavItem[];
20
20
  /** O `id` do item corrente. */
@@ -80,7 +80,18 @@ const folha = criarFolha((t) => ({
80
80
  flex: 1, minWidth: 0, alignItems: "center", justifyContent: "center",
81
81
  // `space05` e não `space1` — a barra mais baixa (30/09/2026, pedido do Victor, aprovado pela
82
82
  // imagem). Com a moldura de 32 e o vão de 4 eram 9 entre o ícone e o rótulo; ver `marca`.
83
- gap: t.size.space05, minHeight: t.size.controlHLg, padding: t.size.space1,
83
+ // O NOME COLADO NO ÍCONE — 03/10/2026, pedido do Victor (*"o texto pode ficar mais próximo do
84
+ // ícone"*): vão ZERO, como no Telegram, em que o nome começa onde a caixa do ícone acaba. O
85
+ // `space05` daqui (30/09/2026) somava 2 aos 2 de folga da própria linha do nome; medido na
86
+ // bancada, eram 9 de tinta a tinta no `pill` e 13 no `circle-bold`.
87
+ gap: 0, minHeight: t.size.controlHLg,
88
+ // 🔴 A ALTURA DO TELEGRAM — 03/10/2026, pedido do Victor: *"ainda acho ele muito largo
89
+ // comparado a bottomnav como do telegram"*, e "largo" é a grossura. Medido no fonte do
90
+ // Telegram para Android 12.10.6 (`DialogsActivity.MAIN_TABS_HEIGHT = 56`, `GlassTabView`): a
91
+ // pílula tem 56; o botão, 48; o ícone, 24; o nome, 12. A nossa media 73 no `circle-bold`. O
92
+ // recheio de cima e de baixo cai de `space1` para `space05`, e o nome sai em 12 com a
93
+ // entrelinha justa (ver o `Text` do rótulo). Igual na web (`aurea.css`).
94
+ paddingHorizontal: t.size.space1, paddingVertical: t.size.space05,
84
95
  borderRadius: t.size.radiusControl,
85
96
  },
86
97
  // Com a pílula do tamanho do conteúdo, `flex: 1` MATA as abas: no RN ele é base ZERO, e a soma
@@ -100,6 +111,57 @@ const folha = criarFolha((t) => ({
100
111
  width: 32, height: 32, borderRadius: t.size.radiusControl },
101
112
  marcaRedonda: { borderRadius: t.size.radiusFull },
102
113
  contador: { position: "absolute", top: -t.size.space05, right: -t.size.space05 },
114
+ // ── A BARRA FICA PARADA QUANDO SE TROCA DE ABA — pedido do Victor, 03/10/2026 ──────────────
115
+ // *"me incomoda o bottom nav todo se mexer [...] quero ele estático, apenas os botões
116
+ // dinâmicos"*. Medido na bancada com o `react-native-web`: no `circle-bold` com
117
+ // `width="content"` a barra ia de 249 a 257 de largura conforme a aba escolhida, e por estar no
118
+ // centro, andava inteira. A regra daqui para baixo: NENHUMA medida depende de o item estar
119
+ // escolhido. Só a pintura muda. Quem cobra: `tests/unit/native-bottomnav-parada.test.tsx`.
120
+ //
121
+ // `circle-bold`: o disco amarelo é uma caixa DENTRO do item, de 56 (o `--bottomnav-bold` do
122
+ // `aurea.css`), em TODOS os itens. Antes o disco ERA o item escolhido: ele media 56 e os outros
123
+ // mediam o conteúdo, então a soma mudava com a escolha. O recheio passa do item para o disco,
124
+ // e a altura da barra não muda.
125
+ aba_disco: { paddingHorizontal: 0, paddingVertical: 0 },
126
+ // Sem a moldura de 32 (é o disco que desenha a forma, e não ela) e sem vão, o nome fica colado
127
+ // no ícone; o recheio de cima e de baixo volta a `space1` para o disco continuar quase redondo
128
+ // (56 × 48).
129
+ disco: {
130
+ alignItems: "center", justifyContent: "center", gap: 0,
131
+ width: 56, minHeight: t.size.controlHLg, borderRadius: t.size.radiusFull,
132
+ paddingHorizontal: t.size.space1, paddingVertical: t.size.space1,
133
+ },
134
+ // `capsule` (a "A" da bancada, o Material 3 Expressive): a moldura do ícone vira a cápsula de
135
+ // 56 × 32 do indicador ativo dele — os mesmos `--bottomnav-bold` e `--bottomnav-ring` do
136
+ // `aurea.css`, em todos os itens. O rótulo fica embaixo, fora dela.
137
+ marcaCapsula: { width: 56, height: 32, borderRadius: t.size.radiusFull },
138
+ // O contador fica no canto do ÍCONE, e não da cápsula: sem a cápsula pintada (item não
139
+ // escolhido) ele ficaria solto, longe do sino. São os (56 − 32) / 2 a mais para dentro.
140
+ contadorCapsula: { right: (56 - 32) / 2 - t.size.space05 },
141
+ // No `content` o item tem a largura da cápsula mais o recheio de sempre (`space1`), e não o
142
+ // `space3` do `aba_content`: com ele cada item teria 80 e a barra, 340.
143
+ aba_capsula_content: { paddingHorizontal: t.size.space1 },
144
+ // `expand` (a "C" da bancada): os outros itens mostram só o ícone; o escolhido vira cápsula
145
+ // amarela com o nome ao lado. É o ÚNICO em que o botão escolhido cresce — os botões andam, e a
146
+ // BARRA não: a vaga do escolhido é FIXA, e não do tamanho do nome. Ícone (24) + vão (`space2`)
147
+ // + lugar do nome (`space20`, 80) + recheio (`space3` dos dois lados) = 136. Nome maior que 80
148
+ // termina em "…". Medir o nome de cada aba daria uma barra que muda com a lista; esta não muda.
149
+ aba_expande: {
150
+ flexDirection: "row", gap: t.size.space2, paddingVertical: 0, paddingHorizontal: 0,
151
+ minHeight: t.size.targetMin, minWidth: t.size.targetMin,
152
+ },
153
+ aba_expande_outro_content: { flexGrow: 0, flexShrink: 0, flexBasis: "auto", width: t.size.targetMin },
154
+ aba_expande_escolhido: {
155
+ flexGrow: 0, flexShrink: 0, flexBasis: "auto",
156
+ width: t.size.iconLg + t.size.space2 + t.size.space20 + t.size.space3 * 2,
157
+ paddingHorizontal: t.size.space3, backgroundColor: t.color.primary,
158
+ },
159
+ // A moldura do ícone no `expand` tem a largura do ícone: o contador pendura nele e o nome vem
160
+ // logo ao lado — com 32 de moldura sobrava folga dos dois lados do ícone dentro da cápsula.
161
+ marcaExpande: { width: t.size.iconLg },
162
+ // O nome do item que NÃO é texto, nos não escolhidos do `expand`: fora da tela e sem ocupar
163
+ // lugar, mas na árvore — o texto simples vira `accessibilityLabel` do item.
164
+ rotuloEscondido: { position: "absolute", opacity: 0 },
103
165
  // ── NavList ────────────────────────────────────────────────────────────────────────────────
104
166
  lista: { gap: t.size.space05 },
105
167
  linha: {
@@ -241,6 +303,9 @@ export function BottomNav({ items, current, variant = "floating", indicator = "n
241
303
  const strings = useAureaStrings();
242
304
  const inset = useSafeAreaInsets();
243
305
  const redondo = indicator.startsWith("circle");
306
+ const disco = indicator === "circle-bold";
307
+ const capsula = indicator === "capsule";
308
+ const expande = indicator === "expand";
244
309
  const anotar = useAnotarBottomNav();
245
310
  const flutua = variant !== "edge";
246
311
  const justa = flutua && width === "content";
@@ -270,38 +335,72 @@ export function BottomNav({ items, current, variant = "floating", indicator = "n
270
335
  const ativo = it.id === current;
271
336
  const corDoTexto = !ativo ? t.color.mutedForeground
272
337
  : indicator === "none" ? t.color.primaryEmphasis
273
- : indicator === "circle-bold" ? t.color.primaryForeground
338
+ : indicator === "circle-bold" || indicator === "expand" ? t.color.primaryForeground
274
339
  : indicator === "subtle" || indicator === "pill" ? t.color.primaryEmphasis
275
340
  : redondo ? t.color.primaryEmphasis
276
341
  : t.color.foreground;
277
- return (_jsxs(Pressable, { onPress: it.onPress, disabled: it.disabled,
342
+ // Na `capsule` o ícone está SOBRE o amarelo e o rótulo não: cada um tem a sua cor.
343
+ const corDoIcone = ativo && capsula ? t.color.primaryForeground : corDoTexto;
344
+ // No `expand` o nome só aparece no escolhido; nos outros ele vira o nome do item para o
345
+ // leitor de tela (o texto simples) ou fica na árvore sem ocupar lugar (o que não é texto).
346
+ const nomeVisivel = !expande || ativo;
347
+ const marca = (_jsxs(View, { style: [
348
+ // A moldura de 32 só serve aos indicadores redondos, em que ela VIRA o círculo. Nos
349
+ // outros ela era folga vazia em volta do ícone de 24: fica da altura dele. A largura
350
+ // continua 32, que é o canto do contador. Igual na web (`aurea.css`).
351
+ // No `circle-bold` a moldura NÃO é o círculo (é o disco, em volta do ícone E do nome):
352
+ // ela fica da altura do ícone, e o nome encosta nele.
353
+ s.marca, redondo && !disco ? s.marcaRedonda : { height: t.size.iconLg },
354
+ capsula && s.marcaCapsula, expande && s.marcaExpande,
355
+ ativo && indicator === "circle" && { backgroundColor: comOpacidade(t.color.primary, 0.14) },
356
+ ativo && indicator === "circle-raised" && {
357
+ backgroundColor: t.color.popover, boxShadow: [t.shadow.shadowMd],
358
+ },
359
+ ativo && indicator === "circle-outline" && {
360
+ borderWidth: t.size.borderWidth, borderColor: t.color.primaryEmphasis,
361
+ },
362
+ ativo && capsula && { backgroundColor: t.color.primary },
363
+ ], children: [it.icon && _jsx(Icon, { name: it.icon, size: "lg", color: corDoIcone, weight: ativo ? "fill" : undefined }), it.badge != null && it.badge !== false && (_jsx(View, { style: [s.contador, capsula && s.contadorCapsula], children: typeof it.badge === "number"
364
+ ? _jsx(Badge, { tone: "danger", emphasis: "solid", size: "xs", count: it.badge })
365
+ : _jsx(Badge, { tone: "danger", emphasis: "solid", size: "xs", dot: true }) }))] }));
366
+ // 🔴 O PESO DO RÓTULO NÃO MUDA COM A ESCOLHA (03/10/2026). Era 500 no escolhido e 400 nos
367
+ // outros, e texto mais grosso é texto mais LARGO: no `content` a barra mudava de largura a
368
+ // cada toque, em todos os indicadores. Agora é 500 em todos — o "Label medium" do Material
369
+ // 3, que também deixou de engrossar o escolhido no Expressive pela mesma razão. O que marca
370
+ // a escolha é a cor, o ícone cheio e a forma. No `expand` o nome é `sm`: ao lado do ícone,
371
+ // e não embaixo dele.
372
+ const rotulo = typeof it.label === "string"
373
+ ? (nomeVisivel
374
+ // 12 numa linha de 16, o do Telegram — e o `text-xs` da web. Antes era o `xs` do `Text`
375
+ // nativo, que sobe para 14 (ADR-0050) com linha de 21: era o que mais engrossava a
376
+ // barra. A linha é o tamanho mais `space1`, como na web: com a entrelinha justa (14,4)
377
+ // a perna do "j" passava da caixa. No `expand` o nome vai ao lado do ícone, em 14.
378
+ ? _jsx(Text, { type: expande ? "body-sm" : "body-xs", weight: 500, numberOfLines: 1, style: [{ color: corDoTexto, lineHeight: (expande ? t.size.textSm : t.size.textXs) + t.size.space1 },
379
+ expande && { flexShrink: 1 }], children: it.label })
380
+ : null)
381
+ : (nomeVisivel ? it.label : _jsx(View, { style: s.rotuloEscondido, children: it.label }));
382
+ return (_jsx(Pressable, { onPress: it.onPress, disabled: it.disabled,
278
383
  // `link` e não `tab`: uma aba troca um painel DESTA tela; isto troca de tela.
279
- accessibilityRole: "link", ...estadoAcessivel({ selected: ativo, disabled: !!it.disabled }), style: [
384
+ accessibilityRole: "link", accessibilityLabel: !nomeVisivel && typeof it.label === "string" ? it.label : undefined, ...estadoAcessivel({ selected: ativo, disabled: !!it.disabled }), style: [
280
385
  s.aba, justa && s.aba_content,
281
386
  indicator === "subtle" && { borderRadius: t.size.radiusCard - t.size.space1 },
282
387
  ativo && (indicator === "subtle" || indicator === "pill")
283
388
  && { backgroundColor: comOpacidade(t.color.primary, 0.12) },
284
- ativo && indicator === "circle-bold" && {
285
- flex: 0, width: 56, borderRadius: t.size.radiusFull,
286
- backgroundColor: t.color.primary,
287
- },
288
- ], children: [_jsxs(View, { style: [
289
- // A moldura de 32 só serve aos indicadores redondos, em que ela VIRA o círculo. Nos
290
- // outros ela era folga vazia em volta do ícone de 24: fica da altura dele. A largura
291
- // continua 32, que é o canto do contador. Igual na web (`aurea.css`).
292
- s.marca, redondo ? s.marcaRedonda : { height: t.size.iconLg },
293
- ativo && indicator === "circle" && { backgroundColor: comOpacidade(t.color.primary, 0.14) },
294
- ativo && indicator === "circle-raised" && {
295
- backgroundColor: t.color.popover, boxShadow: [t.shadow.shadowMd],
296
- },
297
- ativo && indicator === "circle-outline" && {
298
- borderWidth: t.size.borderWidth, borderColor: t.color.primaryEmphasis,
299
- },
300
- ], children: [it.icon && _jsx(Icon, { name: it.icon, size: "lg", color: corDoTexto, weight: ativo ? "fill" : undefined }), it.badge != null && it.badge !== false && (_jsx(View, { style: s.contador, children: typeof it.badge === "number"
301
- ? _jsx(Badge, { tone: "danger", emphasis: "solid", size: "xs", count: it.badge })
302
- : _jsx(Badge, { tone: "danger", emphasis: "solid", size: "xs", dot: true }) }))] }), typeof it.label === "string"
303
- ? _jsx(Text, { size: "xs", weight: ativo ? 500 : 400, numberOfLines: 1, style: { color: corDoTexto }, children: it.label })
304
- : it.label] }, it.id));
389
+ disco && s.aba_disco,
390
+ capsula && justa && s.aba_capsula_content,
391
+ // 🔴 Larguras por EXTENSO, e não `flex: 0` — defeito visto pelo Victor no app rodando
392
+ // no navegador (02/10/2026): o `react-native-web` 0.21.3 passa `flex: 0` cru para o
393
+ // CSS, onde ele vale `0 1 0%`, e a peça encolhia até o recheio. No Yoga `flex: 0` é
394
+ // "inflexível"; as três por extenso valem o mesmo nos dois.
395
+ // Quem cobra: `tests/unit/native-flex-navegador.test.tsx`.
396
+ expande && s.aba_expande,
397
+ expande && !ativo && justa && s.aba_expande_outro_content,
398
+ expande && ativo && s.aba_expande_escolhido,
399
+ ], children: disco
400
+ // O disco do `circle-bold`: 56 de largura em todos os itens, amarelo só no escolhido.
401
+ // É o `.bottom-nav-disc` da web.
402
+ ? _jsxs(View, { style: [s.disco, ativo && { backgroundColor: t.color.primary }], children: [marca, rotulo] })
403
+ : _jsxs(_Fragment, { children: [marca, rotulo] }) }, it.id));
305
404
  }) }));
306
405
  }
307
406
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aurea-uds/native",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "A Aurea UDS para React Native: o provider de tema e densidade, os componentes e os ícones do Phosphor sobre react-native-svg.",
5
5
  "keywords": [
6
6
  "aurea",
@@ -55,7 +55,7 @@
55
55
  "NOTICE"
56
56
  ],
57
57
  "dependencies": {
58
- "@aurea-uds/tokens": "^0.16.0"
58
+ "@aurea-uds/tokens": "^0.17.0"
59
59
  },
60
60
  "peerDependencies": {
61
61
  "@react-native-community/datetimepicker": ">=8",