@breathe-ds/ui 0.1.0 → 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Felipe Genuino
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,197 +1,212 @@
1
1
  # Breathe
2
2
 
3
- **Breathe** — o design system da **família Cuidado Global** (ecossistema RTA / Mariangela:
4
- creeta, ttcab, metodorta…). Os sites **herdam** este pacote e aplicam a
5
- própria paleta sobre o mesmo contrato.
3
+ **Breathe** — the design system of the **Cuidado Global family** (the RTA / Mariangela
4
+ ecosystem: creeta, ttcab, metodorta…). The sites **inherit** this package and apply
5
+ their own palette on top of the same contract.
6
6
 
7
- > Pacote npm: `@breathe-ds/ui` (endereço técnico). **Breathe** é o nome do sistema.
7
+ > npm package: `@breathe-ds/ui` (the technical address). **Breathe** is the system's name.
8
8
 
9
- > **v0.1** — tokens + componentes (ver `components/`) + marcas dos sites.
10
- > Confira tudo renderizado no playground (abaixo).
9
+ > **v0.1** — tokens + components (see `components/`) + the sites' brand marks.
10
+ > See everything rendered in the playground (below).
11
11
 
12
- ## Ideia
12
+ ## Idea
13
13
 
14
- Os sites da família **não compartilham paleta** (ttcab é petróleo/creme,
15
- creeta é petróleo/névoa/magenta). O que se compartilha são os **papéis de
16
- token** — `surface`, `ink`, `action`, `line`… — com nomes e estrutura iguais.
17
- Cada site define os valores (`--cg-*`) da sua paleta.
14
+ The family's sites **do not share a palette** (ttcab is teal/cream,
15
+ creeta is teal/mist/magenta). What they share are the **token roles** —
16
+ `surface`, `ink`, `action`, `line`… — with the same names and structure.
17
+ Each site defines the values (`--cg-*`) of its own palette.
18
18
 
19
- Token nunca é "a cor X": é **papel + contraste medido**. (O Felipe é daltônico;
20
- decisão de cor sempre por papel e contraste, nunca a olho.)
19
+ A token is never "color X": it is **role + measured contrast**. (Felipe is color-blind;
20
+ color decisions are always made by role and contrast, never by eye.)
21
21
 
22
- ## Uso (Tailwind v4)
22
+ ## Install
23
23
 
24
- No `globals.css` do site:
24
+ ```bash
25
+ npm i @breathe-ds/ui
26
+ ```
27
+
28
+ Public package on npm: https://www.npmjs.com/package/@breathe-ds/ui · source at
29
+ https://github.com/felipegenuino/breathe-ds (MIT license). The components ship as TSX source (no
30
+ build step): in Next, add `transpilePackages: ["@breathe-ds/ui"]` to `next.config`.
31
+
32
+ ## Usage (Tailwind v4)
33
+
34
+ In the site's `globals.css`:
25
35
 
26
36
  ```css
27
37
  @import "tailwindcss";
28
38
  @import "@breathe-ds/ui/theme.css";
29
- @import "@breathe-ds/ui/tokens/ttcab.css"; /* ou defina os --cg-* inline */
39
+ @import "@breathe-ds/ui/tokens/ttcab.css"; /* or define the --cg-* inline */
30
40
  ```
31
41
 
32
- Depois, as utilitárias do Tailwind usam os papéis: `bg-surface`, `text-ink`,
42
+ Then Tailwind utilities use the roles: `bg-surface`, `text-ink`,
33
43
  `border-line`, `bg-action`, `text-action-text`, `rounded-2xl`, etc.
34
44
 
35
- Para um site sem arquivo de tokens pronto, defina os papéis no próprio `:root`:
45
+ For a site without a ready-made token file, define the roles on `:root` itself:
36
46
 
37
47
  ```css
38
48
  :root {
39
49
  --cg-surface: #fbf5e9;
40
50
  --cg-ink: #144753;
41
- /* … ver tokens/ttcab.css para a lista completa */
51
+ /* … see tokens/ttcab.css for the full list */
42
52
  }
43
53
  ```
44
54
 
45
- ## Papéis de token
55
+ ## Token roles
46
56
 
47
- | Token (Tailwind) | Papel | `--cg-*` |
57
+ | Token (Tailwind) | Role | `--cg-*` |
48
58
  |---|---|---|
49
- | `surface` | fundo base | `--cg-surface` |
50
- | `surface-panel` | painel escuro (footer, CTA final) | `--cg-surface-panel` |
51
- | `ink` | texto principal / títulos | `--cg-ink` |
52
- | `ink-soft` | texto secundário | `--cg-ink-soft` |
53
- | `ink-body` | corpo de leitura | `--cg-ink-body` |
54
- | `action` | CTA / destaque — **nunca texto puro** | `--cg-action` |
55
- | `action-text` | texto na cor de ação (AA) | `--cg-action-text` |
56
- | `line` | hairlines / bordas | `--cg-line` |
57
- | `font-heading` / `font-body` | tipografia | `--cg-font-*` |
58
- | `radius-xl/2xl/pill` · `ease-cg` | raio e curva de motion | fixos |
59
-
60
- ## Contrastes medidos — TTCAB (`tokens/ttcab.css`)
61
-
62
- Valores pela fórmula WCAG padrão (os mesmos que o `preview/` calcula ao vivo):
63
-
64
- | Par | Contraste | Nível |
59
+ | `surface` | base background | `--cg-surface` |
60
+ | `surface-panel` | dark panel (footer, final CTA) | `--cg-surface-panel` |
61
+ | `ink` | primary text / headings | `--cg-ink` |
62
+ | `ink-soft` | secondary text | `--cg-ink-soft` |
63
+ | `ink-body` | reading body text | `--cg-ink-body` |
64
+ | `action` | CTA / highlight — **never plain text** | `--cg-action` |
65
+ | `action-text` | text in the action color (AA) | `--cg-action-text` |
66
+ | `line` | hairlines / borders | `--cg-line` |
67
+ | `font-heading` / `font-body` | typography | `--cg-font-*` |
68
+ | `radius-xl/2xl/pill` · `ease-cg` | radius and motion curve | fixed |
69
+
70
+ ## Measured contrast — TTCAB (`tokens/ttcab.css`)
71
+
72
+ Values from the standard WCAG formula (the same ones `preview/` computes live):
73
+
74
+ | Pair | Contrast | Level |
65
75
  |---|---|---|
66
- | surface `#fbf5e9` / ink `#144753` | 9,41:1 | AAA |
67
- | surface `#fbf5e9` / ink-soft `#54707a` | 4,86:1 | AA |
68
- | surface `#fbf5e9` / ink-body `#1e4f5b` | 8,32:1 | AAA |
69
- | surface `#fbf5e9` / action-text `#9a5d07` | 4,90:1 | AA |
70
- | surface-panel `#144753` / texto creme | 9,41:1 | AAA |
76
+ | surface `#fbf5e9` / ink `#144753` | 9.41:1 | AAA |
77
+ | surface `#fbf5e9` / ink-soft `#54707a` | 4.86:1 | AA |
78
+ | surface `#fbf5e9` / ink-body `#1e4f5b` | 8.32:1 | AAA |
79
+ | surface `#fbf5e9` / action-text `#9a5d07` | 4.90:1 | AA |
80
+ | surface-panel `#144753` / cream text | 9.41:1 | AAA |
71
81
 
72
- `action` `#f6a22a` é decorativo/CTA — não usar como texto sobre creme.
82
+ `action` `#f6a22a` is decorative/CTA only — do not use it as text on cream.
73
83
 
74
- ## Tipografia da família
84
+ ## Family typography
75
85
 
76
- Decisão (28/10/2026): **só fontes open-source** — nada de licenciadas (Gotham saiu).
86
+ Decision (2026-10-28): **open-source fonts only** — no licensed ones (Gotham is out).
77
87
 
78
- | Papel | Fonte | Observação |
88
+ | Role | Font | Note |
79
89
  |---|---|---|
80
- | `font-heading` | **Quicksand** | títulos, toda a família (Google Fonts) |
81
- | `font-body` | **Figtree** | corpo, toda a família (Google Fonts) |
82
-
83
- **Hierarquia/escala tipográfica:** usar a do **novo.creeta** como guia (bem trabalhada
84
- e refinada). _A extrair para tokens de tamanho/peso/entrelinha._
85
-
86
- ## Primeiros consumidores
90
+ | `font-heading` | **Quicksand** | headings, whole family (Google Fonts) |
91
+ | `font-body` | **Figtree** | body, whole family (Google Fonts) |
87
92
 
88
- **ttcab** e **creeta**. A paleta do creeta em `tokens/creeta.css` está
89
- ilustrativa (marcada `[conferir]`) — fecha ao migrar o creeta.
93
+ **Type hierarchy/scale:** use **novo.creeta**'s as the guide (well worked out
94
+ and refined). _To be extracted into size/weight/line-height tokens._
90
95
 
91
- ## O que muda por tema (e o que é fixo)
96
+ ## First consumers
92
97
 
93
- **Muda por tema:** apenas **cores** e **grafismos** (ThreadRail, OrganicFrame,
94
- marca d'água…). Cada site/marca da família tem a sua cor e os seus grafismos.
98
+ **ttcab** and **creeta**. The creeta palette in `tokens/creeta.css` is
99
+ illustrative (marked `[conferir]`) — to be finalized when creeta migrates.
95
100
 
96
- **Fixo em toda a família:** **tipografia** (Quicksand nos títulos + Figtree no
97
- corpo, em qualquer tema — inclusive o Neutro) e os **ícones** (lucide). Botões
98
- usam a fonte de corpo semibold (o 600 da Quicksand rende leve demais).
101
+ ## What changes per theme (and what is fixed)
99
102
 
100
- ## Decisão — estilo de card
103
+ **Changes per theme:** only **colors** and **graphic devices** (ThreadRail, OrganicFrame,
104
+ watermark…). Each site/brand in the family has its own color and its own graphic devices.
101
105
 
102
- Linguagem de card do DS: **bordas discretas + sombra discreta** (referência: "Famílias
103
- contam" do novo.creeta). Nada de borda forte ou sombra pesada. _A virar tokens `--shadow-card` + `--border`._
106
+ **Fixed across the whole family:** **typography** (Quicksand for headings + Figtree for
107
+ body, in any theme — including Neutral) and the **icons** (lucide). Buttons
108
+ use the body font at semibold (Quicksand's 600 renders too light).
104
109
 
105
- ## Decisão — estilo de card
110
+ ## Decision — card style
106
111
 
107
- Linguagem de card do DS: **bordas discretas + sombra discreta** (referência: "Famílias contam" do novo.creeta). Nada de borda forte ou sombra pesada. _A virar tokens `--shadow-card` + `--border`._
112
+ The DS card language: **subtle borders + subtle shadow** (reference: "Famílias contam" on novo.creeta). No strong borders or heavy shadows. _To become `--shadow-card` + `--border` tokens._
108
113
 
109
- ## Decisão — largura de hero
114
+ ## Decision — hero width
110
115
 
111
- Heros **mais largos / full-bleed**, na proporção do **novo.creeta** (mais imersivos, menos
112
- margem lateral). Vale para `PageVideoHero` e `PageHero`. O hero atual do ttcab é mais contido
113
- — alargar para o padrão creeta ao migrar. _Vira token/escala de largura de hero._
116
+ **Wider / full-bleed** heroes, in **novo.creeta**'s proportion (more immersive, less
117
+ side margin). Applies to `PageVideoHero` and `PageHero`. ttcab's current hero is more contained
118
+ — widen it to the creeta standard when migrating. _Becomes a hero-width token/scale._
114
119
 
115
- ## Marcas dos sites (`components/marks/`)
120
+ ## Site brand marks (`components/marks/`)
116
121
 
117
- Cada site da família tem o símbolo e o wordmark reais, copiados dos arquivos de
118
- marca (`public/brand`) dos repositórios, separados em dois componentes para a
119
- `Cortina` e o `Lockup` montarem os arranjos:
122
+ Each site in the family has its real symbol and wordmark, copied from the brand
123
+ files (`public/brand`) in the repositories, split into two components so the
124
+ `Curtain` and the `Lockup` can compose the arrangements:
120
125
 
121
- | Site | Símbolo | Wordmark | Import |
126
+ | Site | Symbol | Wordmark | Import |
122
127
  |---|---|---|---|
123
- | TTCAB | `TtcabSymbol` (mãe + bebê, laranja `#F6A22A`) | `TtcabWordmark` (caixa-alta, `currentColor`) | `@breathe-ds/ui/components/marks/ttcab` |
124
- | Creeta | `CreetaSymbol` (três ondas + ponto, cores da marca) | `CreetaWordmark` (minúsculas, `currentColor`) | `@breathe-ds/ui/components/marks/creeta` |
128
+ | TTCAB | `TtcabSymbol` (mother + baby, orange `#F6A22A`) | `TtcabWordmark` (uppercase, `currentColor`) | `@breathe-ds/ui/components/marks/ttcab` |
129
+ | Creeta | `CreetaSymbol` (three waves + dot, brand colors) | `CreetaWordmark` (lowercase, `currentColor`) | `@breathe-ds/ui/components/marks/creeta` |
125
130
 
126
131
  ```tsx
127
- <Cortina logo={<TtcabSymbol />} name={<TtcabWordmark />} wordmarkRatio={TTCAB_LOCKUP.wordmarkPerSymbol} tagline="…" />
132
+ <Curtain logo={<TtcabSymbol />} name={<TtcabWordmark />} wordmarkRatio={TTCAB_LOCKUP.wordmarkPerSymbol} tagline="…" />
128
133
  <Lockup variant="horizontal" size="md" logo={<CreetaSymbol />} wordmark={<CreetaWordmark />} wordmarkRatio={CREETA_LOCKUP.wordmarkPerSymbol} name="creeta" />
129
134
  ```
130
135
 
131
- **Proporção vem do manual, não do olho.** Cada arquivo de marca exporta
132
- `*_LOCKUP.wordmarkPerSymbol` (altura do wordmark ÷ altura do símbolo no
133
- logotipo horizontal: TTCAB 0,557 · creeta 0,469) e `symbolSide` (no TTCAB o
134
- símbolo fica à **direita** do nome; no creeta e nos demais, à esquerda). O `Lockup` e a `Cortina`
135
- passam isso ao svg pela variável `--cg-wordmark`; o símbolo injetado recebe a
136
- altura do tamanho (`size`). Medido nos sites (29/10/2026): no header dos dois o
137
- símbolo tem **44px** de altura — é o `size="md"` do Lockup. O símbolo aceita
138
- `color` para versão monocromática (branco sobre painel escuro). O desenho não se
139
- edita à mão: é o do manual de cada marca.
140
-
141
- `Lockup` com `entrance` (e `entranceDelay` em ms) faz a entrada do header do
142
- novo.creeta: símbolo, depois o nome, depois a tagline; com movimento reduzido vira
143
- um fade curto.
144
-
145
- ## Navegação
146
-
147
- - **Header**: marca (o app passa o link para a home) · itens · item com dropdown
148
- (ícones nos subitens; link final com seta → ou ↗ se `external`) · idioma · CTA.
149
- Padrão dos sites: "Início" é só a casa (`icon` + `iconOnly`), rótulo para
150
- leitor de tela.
151
- - **BottomNav**: no celular/tablet a navegação sai do header e vira a barra
152
- flutuante inferior. É o código da BarraNavegacao do novo.creeta (= BottomNav
153
- do ttcab), neutro por tokens: recebe TODAS as páginas em ordem de importância
154
- (`pages`, com o `Icon` lucide) e mostra quantas couberem (64px por aba,
155
- ResizeObserver); o resto e as saídas externas (`extras`, com o domínio escrito
156
- e seta ↗) vão para o painel "Mais", um popover nativo (Esc, toque fora, sem
157
- JS). Véu de gradiente atrás da barra. `hideFrom` = `md` (creeta) ou `lg`
158
- (ttcab). `labels` para outro idioma. `embedded` para a moldura da demo.
159
- - **AnchorNav**: cápsula flutuante de âncoras da página (scroll-spy + indicador
160
- deslizante), extraída do TreatmentAnchorNav do ttcab. As seções-alvo precisam
161
- de `scroll-mt-*` compatível com o sticky.
162
-
163
- ## Playground (ver os componentes)
164
-
165
- `cd playground && npm install && npm run dev` → kitchen-sink em http://localhost:5177 com os componentes reais renderizados (paleta ttcab).
136
+ **The proportion comes from the brand manual, not from the eye.** Each brand file exports
137
+ `*_LOCKUP.wordmarkPerSymbol` (wordmark height ÷ symbol height in the
138
+ horizontal logo: TTCAB 0.557 · creeta 0.469) and `symbolSide` (in TTCAB the
139
+ symbol sits to the **right** of the name; in creeta and the others, to the left). The `Lockup` and the `Curtain`
140
+ pass this to the svg through the `--cg-wordmark` variable; the injected symbol takes the
141
+ height of the size (`size`). Measured on the sites (2026-10-29): in both headers the
142
+ symbol is **44px** tall — that is the Lockup's `size="md"`. The symbol accepts
143
+ `color` for a monochrome version (white on a dark panel). The artwork is never
144
+ edited by hand: it is the one from each brand's manual.
145
+
146
+ `Lockup` with `entrance` (and `entranceDelay` in ms) reproduces the header entrance of
147
+ novo.creeta: symbol, then name, then tagline; with reduced motion it becomes
148
+ a short fade.
149
+
150
+ ## Navigation
151
+
152
+ - **Header**: brand (the app passes the link to the home page) · items · item with dropdown
153
+ (icons on sub-items; final link with a → arrow, or ↗ if `external`) · language · CTA.
154
+ Site convention: "Home" is just the house (`icon` + `iconOnly`), with a label for
155
+ screen readers.
156
+ - **BottomNav**: on phone/tablet, navigation leaves the header and becomes the
157
+ floating bottom bar. It is the code of novo.creeta's BarraNavegacao (= ttcab's
158
+ BottomNav), made neutral through tokens: it receives ALL pages in order of importance
159
+ (`pages`, with the lucide `Icon`) and shows as many as fit (64px per tab,
160
+ ResizeObserver); the rest and the external exits (`extras`, with the domain spelled out
161
+ and a ↗ arrow) go to the "More" panel, a native popover (Esc, tap outside, no
162
+ JS). Gradient veil behind the bar. `hideFrom` = `md` (creeta) or `lg`
163
+ (ttcab). `labels` for another language. `embedded` for the demo frame.
164
+ - **AnchorNav**: floating capsule of page anchors (scroll-spy + sliding
165
+ indicator), extracted from ttcab's TreatmentAnchorNav. Target sections need
166
+ a `scroll-mt-*` compatible with the sticky header.
167
+ - **LanguageSwitcher**: the SeletorDeIdioma of novo.creeta — `menu` (button with
168
+ the code, opens the three languages; header) or `list` (plain links; the Footer
169
+ uses it). Same page in the other language, each language written in itself,
170
+ current marked by ✓ and `aria-current`.
171
+ - **Footer**: the site-footer of novo.creeta, by tokens — full width like the
172
+ Header, with the signed brand + sentence, "Pages" (title on the first line, links in two
173
+ columns), "Where we are" (address, hours, "Directions ↗ · Waze ↗"), an optional
174
+ notice band on `surface`, the legal line with the **CNPJ (always required in
175
+ the family)** plus the contact chips, and the languages as plain links.
176
+ `navHiddenFrom` keeps room at the bottom for the BottomNav.
177
+
178
+ ## Playground (see the components)
179
+
180
+ `cd playground && npm install && npm run dev` → kitchen sink at http://localhost:5177 with the real components rendered (ttcab palette).
166
181
 
167
182
  ## Roadmap
168
183
 
169
- - [x] v0.1 — tokens (preset + paletas ttcab/creeta) + preview
170
- - [x] Accordion (1º componente; padrão de deps injetáveis)
171
- - [x] Tipografia da família (Quicksand + Figtree, só livres)
172
- - [ ] **Escalas do novo.creeta** (referência total, aprovado): **tipografia** (tamanhos/pesos/entrelinha)
173
- **+ espaçamento/ritmo** (padding de seção, gaps) **+ border-radius padrão** → viram tokens do DS
174
- - [ ] **Ícones**: empacotar o conjunto do lucide usado pela família, para os
175
- componentes importarem/injetarem (sem cada site depender do lucide direto)
176
- - [ ] **MediaCard** (vídeo/imagem atrás + badge de ícone + título + lead + seta;
177
- variante **carrossel horizontal no mobile** com barra de progresso + pause) — aprovado
178
- - [ ] **TestimonialCard** (depoimento) — aprovado. Variantes: **citação** (card de texto),
179
- **vídeo hero** (full-bleed com citação sobreposta), **vídeo ao lado** ("Famílias contam": vídeo + texto).
180
- Avaliar um **mix** das duas. **Já existe no ttcab** (TestimonialVideoHero/Card) → extrair/generalizar.
181
- - [ ] **VideoStoryCard** (vídeo curto, vertical) — aprovado. Play + badge de duração +
182
- título + lead + **"Ler a transcrição"** (colapso, reusa o Accordion). Padrão da página de Vídeos.
183
- _Nota: o card ainda pode melhorar num update futuro (ok deixar pra depois)._
184
- - [ ] **ProfessionalCard** (equipe) — aprovado. Duas versões: **small** (foto + nome, fila/linha)
185
- e **big** (foto + nome + função + CREFITO + accordions "Trajetória"/"Orientações").
186
- Variante com **vídeo**: badge "Apresentação", hover anima o vídeo, e **modal** (vídeo + bio).
187
- **Regra de dado:** todo profissional PRECISA ter **CREFITO** vinculado (exigência COFFITO).
188
- - [x] **PillList** (chips tipo "Condições que atendemos": rótulo + seta, grade que quebra, link "Ver todos")
189
- - [x] **AnchorNav** (cápsula flutuante de âncoras, scroll-spy + indicador deslizante) — extraída do ttcab (29/10)
190
- - [x] **Footer** · **PageVideoHero** (loop e player) · **Header** (dropdown, casa) · **BottomNav** · **Cortina** · **Lockup** (5 arranjos) · **BreatheMark**
191
- - [x] **Marcas dos sites** (`components/marks/`): símbolo + wordmark reais do TTCAB e do Creeta (29/10)
192
- - [ ] **ThreadRail** (grafismo lateral — fio que desce pela esquerda ligando seções) — aprovado,
193
- **já existe no ttcab** → extrair.
194
- - [ ] **OrganicFrame** (recorte de imagem orgânico, tipo blob) — registrado como **OPÇÃO**
195
- (não default pra todos os sites). **Já existe no ttcab** → extrair como variante opcional.
196
- - [x] Primitivos: Button (com variant link), Container, SectionKicker, Card
197
- - [ ] Migrar ttcab e creeta para consumir o pacote + `npm publish`
184
+ - [x] v0.1 — tokens (preset + ttcab/creeta palettes) + preview
185
+ - [x] Accordion (1st component; injectable-deps pattern)
186
+ - [x] Family typography (Quicksand + Figtree, free fonts only)
187
+ - [ ] **novo.creeta scales** (full reference, approved): **typography** (sizes/weights/line-height)
188
+ **+ spacing/rhythm** (section padding, gaps) **+ default border-radius** → become DS tokens
189
+ - [ ] **Icons**: package the lucide set used by the family, so components can
190
+ import/inject it (without each site depending on lucide directly)
191
+ - [ ] **MediaCard** (video/image behind + icon badge + title + lead + arrow;
192
+ **horizontal carousel on mobile** variant with progress bar + pause) — approved
193
+ - [ ] **TestimonialCard** — approved. Variants: **quote** (text card),
194
+ **video hero** (full-bleed with overlaid quote), **video beside** ("Famílias contam": video + text).
195
+ Consider a **mix** of the two. **Already exists in ttcab** (TestimonialVideoHero/Card) → extract/generalize.
196
+ - [ ] **VideoStoryCard** (short vertical video) — approved. Play + duration badge +
197
+ title + lead + **"Read the transcript"** (collapsible, reuses the Accordion). Pattern of the Videos page.
198
+ _Note: the card can still improve in a future update (fine to leave for later)._
199
+ - [ ] **ProfessionalCard** (team) — approved. Two versions: **small** (photo + name, in a row)
200
+ and **big** (photo + name + role + CREFITO + "Trajetória"/"Orientações" accordions).
201
+ **Video** variant: "Apresentação" badge, hover plays the video, and a **modal** (video + bio).
202
+ **Data rule:** every professional MUST have a **CREFITO** attached (COFFITO requirement).
203
+ - [x] **PillList** (chips like "Condições que atendemos": label + arrow, wrapping grid, "View all" link)
204
+ - [x] **AnchorNav** (floating capsule of anchors, scroll-spy + sliding indicator) — extracted from ttcab (10/29)
205
+ - [x] **Footer** · **PageVideoHero** (loop and player) · **Header** (dropdown, house) · **BottomNav** · **Curtain** · **Lockup** (5 arrangements) · **BreatheMark**
206
+ - [x] **Site brand marks** (`components/marks/`): real symbol + wordmark for TTCAB and Creeta (10/29)
207
+ - [ ] **ThreadRail** (side graphic device — a thread running down the left linking sections) — approved,
208
+ **already exists in ttcab** → extract.
209
+ - [ ] **OrganicFrame** (organic, blob-like image crop) — recorded as an **OPTION**
210
+ (not the default for every site). **Already exists in ttcab** → extract as an optional variant.
211
+ - [x] Primitives: Button (with link variant), Container, SectionKicker, Card
212
+ - [ ] Migrate ttcab and creeta to consume the package + `npm publish`
@@ -10,20 +10,21 @@ export type AccordionItem = {
10
10
 
11
11
  type Props = {
12
12
  items: AccordionItem[];
13
- /** id aberto inicialmente (padrão: o primeiro). null = todos fechados. */
13
+ /** id open initially (default: the first). null = all closed. */
14
14
  defaultOpenId?: string | null;
15
- /** true (padrão): só um aberto por vez, como no creeta. */
15
+ /** true (default): only one open at a time, as on creeta. */
16
16
  single?: boolean;
17
17
  };
18
18
 
19
19
  /**
20
- * Accordion / collapse da família Cuidado Global. Cartões em `surface` com
21
- * borda `line`, título `ink` e chevron que gira ao abrir. Sem dependência de
22
- * app: o ícone é um SVG inline. Anima a altura pelo truque grid-rows (0fr→1fr),
23
- * sem medir nada em JS, e respeita prefers-reduced-motion.
20
+ * Accordion / collapse of the Cuidado Global family. Cards in `surface` with
21
+ * `line` border, `ink` title and a chevron that rotates on open. No app
22
+ * dependency: the icon is an inline SVG. Animates height with the grid-rows
23
+ * trick (0fr→1fr), without measuring anything in JS, and respects
24
+ * prefers-reduced-motion.
24
25
  *
25
- * Cores: texto `ink` sobre `surface` (contraste do papel definido pelo site).
26
- * Consome os tokens via classes do Tailwind (surface/ink/line) — ver theme.css.
26
+ * Colors: `ink` text on `surface` (contrast of the role defined by the site).
27
+ * Consumes the tokens via Tailwind classes (surface/ink/line) — see theme.css.
27
28
  */
28
29
  export function Accordion({ items, defaultOpenId, single = true }: Props) {
29
30
  const initial =
@@ -78,12 +79,12 @@ export function Accordion({ items, defaultOpenId, single = true }: Props) {
78
79
  </svg>
79
80
  </button>
80
81
  </h3>
81
- {/* grid-rows 0fr→1fr: anima a altura sem medir em JS */}
82
+ {/* grid-rows 0fr→1fr: animates height without measuring in JS */}
82
83
  <div
83
84
  id={panelId}
84
85
  role="region"
85
86
  aria-labelledby={btnId}
86
- // React 18 não conhece `inert` como booleano: string vazia liga, undefined desliga (no 19 o booleano funciona igual).
87
+ // React 18 does not know `inert` as a boolean: empty string turns it on, undefined turns it off (in 19 the boolean works the same way).
87
88
  {...({ inert: isOpen ? undefined : "" } as Record<string, unknown>)}
88
89
  className={`grid transition-[grid-template-rows] duration-300 ease-[cubic-bezier(.2,.8,.2,1)] motion-reduce:transition-none ${
89
90
  isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
@@ -5,30 +5,42 @@ import { Container } from "./Container";
5
5
 
6
6
  export type AnchorItem = { id: string; label: string };
7
7
 
8
+ type Labels = {
9
+ /** aria-label of the anchor nav */
10
+ nav: string;
11
+ };
12
+
13
+ const EN: Labels = {
14
+ nav: "On this page",
15
+ };
16
+
8
17
  type Props = {
9
18
  items: AnchorItem[];
10
- /** Classe de deslocamento do sticky (altura do header). Padrão `top-20` (header de 80px). */
19
+ /** Sticky offset class (header height). Default `top-20` (80px header). */
11
20
  stickyTop?: string;
12
- /** Componente de link do app (ex.: next-intl Link). Default: <a>. */
21
+ /** The app's link component (e.g. next-intl Link). Default: <a>. */
13
22
  linkComponent?: ElementType;
14
23
  className?: string;
24
+ /** Strings (English defaults); the app passes the ones for its locale. */
25
+ labels?: Partial<Labels>;
15
26
  };
16
27
 
17
28
  /**
18
- * Navegação contextual de âncoras — cápsula flutuante que acompanha a rolagem
19
- * (extraída do TreatmentAnchorNav do ttcab). Dois comportamentos:
20
- * 1. Scroll-spy: um IntersectionObserver marca como ativa a seção que cruza a
21
- * faixa ~35% do topo; o pill correspondente acende.
22
- * 2. Indicador deslizante: um <span> absoluto mede o pill ativo (offsetLeft/
23
- * Width, imune ao scroll horizontal da cápsula) e desliza até ele na curva
24
- * da família. Em tela estreita a cápsula rola e centraliza o ativo.
25
- * Sinal do ativo = o indicador (preenchimento ink/12 + contorno), NÃO cor do
26
- * texto: o texto fica sempre `ink` sobre `surface`, sem frame de contraste
27
- * fraco. prefers-reduced-motion corta o deslize. As seções-alvo precisam de
28
- * `scroll-mt-*` compatível com o sticky (ex.: `scroll-mt-40`).
29
+ * Contextual anchor navigation — floating capsule that follows the scroll
30
+ * (extracted from ttcab's TreatmentAnchorNav). Two behaviors:
31
+ * 1. Scroll-spy: an IntersectionObserver marks as active the section that
32
+ * crosses the band at ~35% from the top; the matching pill lights up.
33
+ * 2. Sliding indicator: an absolute <span> measures the active pill (offsetLeft/
34
+ * Width, immune to the capsule's horizontal scroll) and slides to it on the
35
+ * family's curve. On narrow screens the capsule scrolls and centers the active one.
36
+ * The active signal = the indicator (ink/12 fill + outline), NOT text color:
37
+ * the text is always `ink` on `surface`, with no low-contrast frame.
38
+ * prefers-reduced-motion removes the slide. Target sections need a
39
+ * `scroll-mt-*` compatible with the sticky offset (e.g. `scroll-mt-40`).
29
40
  */
30
- export function AnchorNav({ items, stickyTop = "top-20", linkComponent, className = "" }: Props) {
41
+ export function AnchorNav({ items, stickyTop = "top-20", linkComponent, className = "", labels }: Props) {
31
42
  const L: ElementType = linkComponent ?? "a";
43
+ const t = { ...EN, ...labels };
32
44
  const [active, setActive] = useState(items[0]?.id ?? "");
33
45
  const listRef = useRef<HTMLUListElement>(null);
34
46
  const linkRefs = useRef<Record<string, HTMLAnchorElement | null>>({});
@@ -53,9 +65,9 @@ export function AnchorNav({ items, stickyTop = "top-20", linkComponent, classNam
53
65
  return () => io.disconnect();
54
66
  }, [items]);
55
67
 
56
- // Chegada com #hash por navegação client-side: rola até a âncora (no full
57
- // load o browser já faz; aqui é idempotente). Reaplica algumas vezes para
58
- // vencer o reset tardio de scroll do roteador.
68
+ // Arriving with a #hash via client-side navigation: scrolls to the anchor (on
69
+ // a full load the browser already does it; here it's idempotent). Reapplies a
70
+ // few times to beat the router's late scroll reset.
59
71
  useEffect(() => {
60
72
  const hash = window.location.hash.slice(1);
61
73
  if (!hash || !items.some((it) => it.id === hash)) return;
@@ -76,7 +88,7 @@ export function AnchorNav({ items, stickyTop = "top-20", linkComponent, classNam
76
88
  setPill({ x: el.offsetLeft, y: el.offsetTop, w: el.offsetWidth, h: el.offsetHeight, ready: true });
77
89
  };
78
90
 
79
- // Mede o pill ativo + revela na cápsula em telas estreitas
91
+ // Measures the active pill + reveals it in the capsule on narrow screens
80
92
  useLayoutEffect(() => {
81
93
  measure();
82
94
  const el = linkRefs.current[active];
@@ -88,7 +100,7 @@ export function AnchorNav({ items, stickyTop = "top-20", linkComponent, classNam
88
100
  // eslint-disable-next-line react-hooks/exhaustive-deps
89
101
  }, [active]);
90
102
 
91
- // Recalcula em resize (quebras/fontes mudam a largura)
103
+ // Recalculates on resize (line breaks/fonts change the width)
92
104
  useEffect(() => {
93
105
  window.addEventListener("resize", measure);
94
106
  return () => window.removeEventListener("resize", measure);
@@ -96,14 +108,14 @@ export function AnchorNav({ items, stickyTop = "top-20", linkComponent, classNam
96
108
  }, [active]);
97
109
 
98
110
  return (
99
- <nav aria-label="Nesta página" className={`sticky ${stickyTop} z-30 py-3 ${className}`}>
111
+ <nav aria-label={t.nav} className={`sticky ${stickyTop} z-30 py-3 ${className}`}>
100
112
  <Container>
101
113
  <div className="flex justify-center">
102
114
  <ul
103
115
  ref={listRef}
104
116
  className="relative flex max-w-full list-none gap-1 overflow-x-auto rounded-pill border border-line bg-surface/95 p-1.5 shadow-[0_10px_28px_-14px_rgba(0,0,0,0.35)] backdrop-blur [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
105
117
  >
106
- {/* Indicador deslizante */}
118
+ {/* Sliding indicator */}
107
119
  <span
108
120
  aria-hidden="true"
109
121
  className="pointer-events-none absolute top-0 left-0 rounded-pill bg-ink/[0.12] shadow-[inset_0_0_0_1px_var(--color-line)] transition-[transform,width,height,opacity] duration-[320ms] ease-cg motion-reduce:transition-none"