@breathe-ds/ui 0.1.1 → 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 +21 -0
- package/README.md +157 -142
- package/components/Accordion.tsx +11 -10
- package/components/AnchorNav.tsx +33 -21
- package/components/BackgroundVideo.tsx +14 -14
- package/components/BottomNav.tsx +69 -68
- package/components/BreatheMark.tsx +12 -11
- package/components/Button.tsx +12 -12
- package/components/Card.tsx +5 -5
- package/components/Container.tsx +4 -4
- package/components/Curtain.tsx +152 -0
- package/components/Footer.tsx +186 -77
- package/components/Header.tsx +32 -17
- package/components/LanguageSwitcher.tsx +139 -0
- package/components/Lockup.tsx +40 -39
- package/components/PageVideoHero.tsx +53 -53
- package/components/PillList.tsx +4 -4
- package/components/SectionKicker.tsx +3 -3
- package/components/icons.tsx +6 -6
- package/components/marks/creeta.tsx +22 -22
- package/components/marks/ttcab.tsx +13 -13
- package/package.json +7 -5
- package/theme.css +21 -21
- package/tokens/creeta.css +10 -11
- package/tokens/neutral.css +7 -7
- package/tokens/ttcab.css +8 -8
- package/components/Cortina.tsx +0 -152
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** —
|
|
4
|
-
creeta, ttcab, metodorta…).
|
|
5
|
-
|
|
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
|
-
>
|
|
7
|
+
> npm package: `@breathe-ds/ui` (the technical address). **Breathe** is the system's name.
|
|
8
8
|
|
|
9
|
-
> **v0.1** — tokens +
|
|
10
|
-
>
|
|
9
|
+
> **v0.1** — tokens + components (see `components/`) + the sites' brand marks.
|
|
10
|
+
> See everything rendered in the playground (below).
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Idea
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
creeta
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
##
|
|
22
|
+
## Install
|
|
23
23
|
|
|
24
|
-
|
|
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"; /*
|
|
39
|
+
@import "@breathe-ds/ui/tokens/ttcab.css"; /* or define the --cg-* inline */
|
|
30
40
|
```
|
|
31
41
|
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
/* …
|
|
51
|
+
/* … see tokens/ttcab.css for the full list */
|
|
42
52
|
}
|
|
43
53
|
```
|
|
44
54
|
|
|
45
|
-
##
|
|
55
|
+
## Token roles
|
|
46
56
|
|
|
47
|
-
| Token (Tailwind) |
|
|
57
|
+
| Token (Tailwind) | Role | `--cg-*` |
|
|
48
58
|
|---|---|---|
|
|
49
|
-
| `surface` |
|
|
50
|
-
| `surface-panel` |
|
|
51
|
-
| `ink` |
|
|
52
|
-
| `ink-soft` |
|
|
53
|
-
| `ink-body` |
|
|
54
|
-
| `action` | CTA /
|
|
55
|
-
| `action-text` |
|
|
56
|
-
| `line` | hairlines /
|
|
57
|
-
| `font-heading` / `font-body` |
|
|
58
|
-
| `radius-xl/2xl/pill` · `ease-cg` |
|
|
59
|
-
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
|
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
|
|
67
|
-
| surface `#fbf5e9` / ink-soft `#54707a` | 4
|
|
68
|
-
| surface `#fbf5e9` / ink-body `#1e4f5b` | 8
|
|
69
|
-
| surface `#fbf5e9` / action-text `#9a5d07` | 4
|
|
70
|
-
| surface-panel `#144753` /
|
|
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`
|
|
82
|
+
`action` `#f6a22a` is decorative/CTA only — do not use it as text on cream.
|
|
73
83
|
|
|
74
|
-
##
|
|
84
|
+
## Family typography
|
|
75
85
|
|
|
76
|
-
|
|
86
|
+
Decision (2026-10-28): **open-source fonts only** — no licensed ones (Gotham is out).
|
|
77
87
|
|
|
78
|
-
|
|
|
88
|
+
| Role | Font | Note |
|
|
79
89
|
|---|---|---|
|
|
80
|
-
| `font-heading` | **Quicksand** |
|
|
81
|
-
| `font-body` | **Figtree** |
|
|
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
|
-
**
|
|
89
|
-
|
|
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
|
-
##
|
|
96
|
+
## First consumers
|
|
92
97
|
|
|
93
|
-
**
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
##
|
|
110
|
+
## Decision — card style
|
|
106
111
|
|
|
107
|
-
|
|
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
|
-
##
|
|
114
|
+
## Decision — hero width
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
—
|
|
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
|
-
##
|
|
120
|
+
## Site brand marks (`components/marks/`)
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`
|
|
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 |
|
|
126
|
+
| Site | Symbol | Wordmark | Import |
|
|
122
127
|
|---|---|---|---|
|
|
123
|
-
| TTCAB | `TtcabSymbol` (
|
|
124
|
-
| Creeta | `CreetaSymbol` (
|
|
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
|
-
<
|
|
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
|
-
**
|
|
132
|
-
`*_LOCKUP.wordmarkPerSymbol` (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`color`
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
`Lockup`
|
|
142
|
-
novo.creeta:
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
##
|
|
146
|
-
|
|
147
|
-
- **Header**:
|
|
148
|
-
(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
- **BottomNav**:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
(`pages`,
|
|
155
|
-
ResizeObserver);
|
|
156
|
-
|
|
157
|
-
JS).
|
|
158
|
-
(ttcab). `labels`
|
|
159
|
-
- **AnchorNav**:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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 +
|
|
170
|
-
- [x] Accordion (
|
|
171
|
-
- [x]
|
|
172
|
-
- [ ] **
|
|
173
|
-
**+
|
|
174
|
-
- [ ]
|
|
175
|
-
|
|
176
|
-
- [ ] **MediaCard** (
|
|
177
|
-
|
|
178
|
-
- [ ] **TestimonialCard**
|
|
179
|
-
**
|
|
180
|
-
|
|
181
|
-
- [ ] **VideoStoryCard** (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
- [ ] **ProfessionalCard** (
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
**
|
|
188
|
-
- [x] **PillList** (chips
|
|
189
|
-
- [x] **AnchorNav** (
|
|
190
|
-
- [x] **Footer** · **PageVideoHero** (loop
|
|
191
|
-
- [x] **
|
|
192
|
-
- [ ] **ThreadRail** (
|
|
193
|
-
**
|
|
194
|
-
- [ ] **OrganicFrame** (
|
|
195
|
-
(
|
|
196
|
-
- [x]
|
|
197
|
-
- [ ]
|
|
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`
|
package/components/Accordion.tsx
CHANGED
|
@@ -10,20 +10,21 @@ export type AccordionItem = {
|
|
|
10
10
|
|
|
11
11
|
type Props = {
|
|
12
12
|
items: AccordionItem[];
|
|
13
|
-
/** id
|
|
13
|
+
/** id open initially (default: the first). null = all closed. */
|
|
14
14
|
defaultOpenId?: string | null;
|
|
15
|
-
/** true (
|
|
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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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:
|
|
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
|
|
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]"
|
package/components/AnchorNav.tsx
CHANGED
|
@@ -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
|
-
/**
|
|
19
|
+
/** Sticky offset class (header height). Default `top-20` (80px header). */
|
|
11
20
|
stickyTop?: string;
|
|
12
|
-
/**
|
|
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
|
-
*
|
|
19
|
-
* (
|
|
20
|
-
* 1. Scroll-spy:
|
|
21
|
-
*
|
|
22
|
-
* 2.
|
|
23
|
-
* Width,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `scroll-mt-*`
|
|
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
|
-
//
|
|
57
|
-
// load
|
|
58
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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=
|
|
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
|
-
{/*
|
|
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"
|