@breathe-ds/ui 0.2.0 → 0.4.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.
Files changed (49) hide show
  1. package/README.md +118 -13
  2. package/components/Accordion.tsx +6 -4
  3. package/components/AnchorNav.tsx +12 -4
  4. package/components/Avatar.tsx +37 -0
  5. package/components/BottomNav.tsx +1 -1
  6. package/components/Breadcrumb.tsx +45 -0
  7. package/components/BreatheMark.tsx +0 -13
  8. package/components/Button.tsx +11 -4
  9. package/components/Byline.tsx +105 -0
  10. package/components/CaptionBand.tsx +74 -0
  11. package/components/CareSteps.tsx +86 -0
  12. package/components/ChatPreview.tsx +138 -0
  13. package/components/CtaCard.tsx +52 -0
  14. package/components/FactStrip.tsx +55 -0
  15. package/components/FeaturedPosts.tsx +80 -0
  16. package/components/Footer.tsx +50 -30
  17. package/components/Header.tsx +4 -2
  18. package/components/Heading.tsx +86 -0
  19. package/components/InfoCard.tsx +116 -0
  20. package/components/LanguageSwitcher.tsx +1 -1
  21. package/components/List.tsx +81 -0
  22. package/components/Lockup.tsx +0 -18
  23. package/components/MapCard.tsx +158 -0
  24. package/components/MediaCard.tsx +266 -0
  25. package/components/PageVideoHero.tsx +65 -23
  26. package/components/PauseButton.tsx +41 -0
  27. package/components/PersonChip.tsx +49 -0
  28. package/components/PhotoSequence.tsx +99 -0
  29. package/components/Pill.tsx +42 -0
  30. package/components/PillList.tsx +20 -47
  31. package/components/PlanTimeline.tsx +66 -0
  32. package/components/PostArticle.tsx +102 -0
  33. package/components/PostCard.tsx +69 -0
  34. package/components/ProfessionalCard.tsx +446 -0
  35. package/components/ProfessionalModal.tsx +243 -0
  36. package/components/Rail.tsx +94 -0
  37. package/components/Rows.tsx +137 -0
  38. package/components/StoryPanel.tsx +107 -0
  39. package/components/TestimonialCard.tsx +138 -0
  40. package/components/Text.tsx +31 -0
  41. package/components/Timeline.tsx +69 -0
  42. package/components/VideoInviteCard.tsx +168 -0
  43. package/components/VideoStoryCard.tsx +382 -0
  44. package/components/useOnAppear.ts +39 -0
  45. package/package.json +34 -2
  46. package/theme.css +206 -0
  47. package/tokens/creeta.css +7 -2
  48. package/tokens/neutral.css +12 -4
  49. package/tokens/ttcab.css +14 -5
package/theme.css CHANGED
@@ -18,12 +18,15 @@
18
18
  /* ── Colors (roles) — value comes from the site's --cg-* ── */
19
19
  --color-surface: var(--cg-surface); /* base background */
20
20
  --color-surface-panel: var(--cg-surface-panel); /* dark panel (footer/CTA) */
21
+ --color-card: var(--cg-card, #ffffff); /* raised surface: cards, pills, menus over the page */
21
22
  --color-ink: var(--cg-ink); /* primary text / headings */
22
23
  --color-ink-soft: var(--cg-ink-soft); /* secondary text */
23
24
  --color-ink-body: var(--cg-ink-body); /* body copy */
24
25
  --color-action: var(--cg-action); /* CTA/highlight — never plain text */
25
26
  --color-action-text: var(--cg-action-text); /* text in the action color (AA) */
26
27
  --color-on-action: var(--cg-on-action); /* text ON the action color (button) */
28
+ --color-action-tint: var(--cg-action-tint, var(--cg-surface)); /* highlighted panel/card: a 6% wash of the action color */
29
+ --color-action-line: var(--cg-action-line, var(--cg-action)); /* border of a highlighted card (3:1 on surface) */
27
30
  --color-line: var(--cg-line); /* hairlines/borders */
28
31
 
29
32
  /* ── Typography (roles; actual family comes from --cg-font-*) ── */
@@ -38,3 +41,206 @@
38
41
  /* ── Motion — the family's signature curve ── */
39
42
  --ease-cg: cubic-bezier(0.2, 0.8, 0.2, 1);
40
43
  }
44
+
45
+ /* ── Base behaviour the whole family shares ──
46
+ Smooth scrolling for anchors (AnchorNav, "On this page", BottomNav); off
47
+ for people who asked for reduced motion. The header sticks at the top
48
+ (~5rem) and, on phones, the BottomNav sticks at the bottom: without this
49
+ padding the control that receives keyboard focus would stop hidden behind
50
+ them (WCAG 2.4.11). Anchored sections may still add their own scroll-margin. */
51
+ @layer base {
52
+ html {
53
+ scroll-behavior: smooth;
54
+ scroll-padding-top: 6rem;
55
+ scroll-padding-bottom: 7rem;
56
+ }
57
+ @media (prefers-reduced-motion: reduce) {
58
+ html {
59
+ scroll-behavior: auto;
60
+ }
61
+ }
62
+ /* Tailwind v4's reset leaves buttons with the default cursor; in the family
63
+ everything that can be tapped shows the hand: buttons, links, the summary
64
+ of a collapse, labels of controls, selects. Disabled ones keep the arrow. */
65
+ button:not(:disabled),
66
+ [role="button"]:not([aria-disabled="true"]),
67
+ a[href],
68
+ summary,
69
+ label[for],
70
+ select:not(:disabled),
71
+ input[type="checkbox"]:not(:disabled),
72
+ input[type="radio"]:not(:disabled),
73
+ input[type="file"]:not(:disabled) {
74
+ cursor: pointer;
75
+ }
76
+ }
77
+
78
+ /* Outbound/anchor arrow on links: takes a small step toward where the link
79
+ goes, on hover. Used by Footer, List, Byline, PersonChip, PillList. */
80
+ @media (prefers-reduced-motion: no-preference) {
81
+ .cg-arrow-go {
82
+ transition: transform 150ms var(--ease-cg);
83
+ }
84
+ a:hover .cg-arrow-go,
85
+ .group:hover .cg-arrow-go {
86
+ transform: translate(2px, -2px);
87
+ }
88
+ .cg-arrow-down {
89
+ transition: transform 150ms var(--ease-cg);
90
+ }
91
+ a:hover .cg-arrow-down,
92
+ .group:hover .cg-arrow-down {
93
+ transform: translateY(2px);
94
+ }
95
+ }
96
+
97
+ /* ── BreatheMark: the symbol "breathes" (scale pulse); off under reduced motion ── */
98
+ @keyframes breathe-mark-inhale {
99
+ 0%, 100% { transform: scale(0.93); }
100
+ 50% { transform: scale(1.05); }
101
+ }
102
+ .breathe-mark__g {
103
+ transform-box: fill-box;
104
+ transform-origin: center;
105
+ animation: breathe-mark-inhale var(--breathe-dur, 6s) cubic-bezier(0.37, 0, 0.63, 1) infinite;
106
+ }
107
+ .breathe-mark--static .breathe-mark__g { animation: none; }
108
+ @media (prefers-reduced-motion: reduce) { .breathe-mark__g { animation: none; } }
109
+
110
+
111
+ /* ── Lockup entrance: symbol (scale + slight turn) → name (slides in) → tagline.
112
+ Same vocabulary as the Curtain; under reduced motion only a short fade. ── */
113
+ .cg-lockup--entrance .cg-lockup__mark{ animation: cg-lockup-mark 500ms var(--ease-cg,cubic-bezier(.2,.8,.2,1)) var(--cg-entrance-delay,0ms) both; }
114
+ .cg-lockup--entrance .cg-lockup__name{ animation: cg-lockup-name 420ms var(--ease-cg,cubic-bezier(.2,.8,.2,1)) calc(var(--cg-entrance-delay,0ms) + 420ms) both; }
115
+ .cg-lockup--entrance .cg-lockup__tag{ animation: cg-lockup-tag 360ms ease calc(var(--cg-entrance-delay,0ms) + 700ms) both; }
116
+ @keyframes cg-lockup-mark{ from{ opacity:0; transform:scale(.6) rotate(-16deg); } }
117
+ @keyframes cg-lockup-name{ from{ opacity:0; transform:translateX(-8px); } }
118
+ @keyframes cg-lockup-tag{ from{ opacity:0; transform:translateY(4px); } }
119
+ @media (prefers-reduced-motion:reduce){
120
+ .cg-lockup--entrance .cg-lockup__mark, .cg-lockup--entrance .cg-lockup__name, .cg-lockup--entrance .cg-lockup__tag{ animation: cg-lockup-fade 240ms ease var(--cg-entrance-delay,0ms) both; }
121
+ @keyframes cg-lockup-fade{ from{ opacity:0; } }
122
+ }
123
+
124
+ /* Rail: the horizontal list hides its scrollbar (the progress bar under it says how much is left). */
125
+ .cg-no-scrollbar {
126
+ scrollbar-width: none;
127
+ }
128
+ .cg-no-scrollbar::-webkit-scrollbar {
129
+ display: none;
130
+ }
131
+
132
+ /* ── Care flow drawings (CareSteps): ChatPreview, PlanTimeline, PhotoSequence ──
133
+ Ported from novo.creeta's globals.css. Only opacity and transform. */
134
+
135
+ /* ChatPreview: each bubble animates when it enters; the component sets the
136
+ rhythm (and the loop). Nothing animates without data-state="playing". */
137
+ @keyframes cg-bubble-in {
138
+ from {
139
+ opacity: 0;
140
+ transform: translateY(8px) scale(0.96);
141
+ }
142
+ }
143
+ @keyframes cg-dot-pulse {
144
+ 0%,
145
+ 60%,
146
+ 100% {
147
+ opacity: 0.35;
148
+ transform: none;
149
+ }
150
+ 30% {
151
+ opacity: 1;
152
+ transform: translateY(-2px);
153
+ }
154
+ }
155
+ .cg-chat[data-state="playing"] .cg-bubble {
156
+ animation: cg-bubble-in 320ms cubic-bezier(0.23, 1, 0.32, 1) both;
157
+ }
158
+ .cg-chat[data-state="playing"] .cg-typing i {
159
+ animation: cg-dot-pulse 900ms ease-in-out calc(var(--n) * 140ms) infinite;
160
+ }
161
+ .cg-chat[data-paused] .cg-bubble,
162
+ .cg-chat[data-paused] .cg-typing i {
163
+ animation-play-state: paused;
164
+ }
165
+
166
+ /* PlanTimeline, 6s loop: the line comes down from 0.2s to 1.7s and each dot
167
+ lights up when it arrives (0.2s, 0.95s, 1.7s); everything stays lit until
168
+ 5.1s, goes off and restarts. One set of keyframes per dot. */
169
+ @keyframes cg-plan-line {
170
+ 0%, 3.3% { opacity: 1; transform: scaleY(0); }
171
+ 28.3%, 85% { opacity: 1; transform: scaleY(1); }
172
+ 92%, 100% { opacity: 0; transform: scaleY(1); }
173
+ }
174
+ @keyframes cg-plan-dot-1 {
175
+ 0%, 3.3% { opacity: 0; transform: scale(0.3); }
176
+ 8.6%, 85% { opacity: 1; transform: none; }
177
+ 92%, 100% { opacity: 0; transform: scale(0.3); }
178
+ }
179
+ @keyframes cg-plan-dot-2 {
180
+ 0%, 15.8% { opacity: 0; transform: scale(0.3); }
181
+ 21.1%, 85% { opacity: 1; transform: none; }
182
+ 92%, 100% { opacity: 0; transform: scale(0.3); }
183
+ }
184
+ @keyframes cg-plan-dot-3 {
185
+ 0%, 28.3% { opacity: 0; transform: scale(0.3); }
186
+ 33.6%, 85% { opacity: 1; transform: none; }
187
+ 92%, 100% { opacity: 0; transform: scale(0.3); }
188
+ }
189
+ /* The name out of turn stays at 75%, not less: ink at 75% over the card still
190
+ passes AA (creeta measured 5.03:1; at 45% it failed, WCAG 1.4.3). */
191
+ @keyframes cg-plan-name-1 {
192
+ 0%, 3.3%, 92%, 100% { opacity: 0.75; }
193
+ 8.6%, 85% { opacity: 1; }
194
+ }
195
+ @keyframes cg-plan-name-2 {
196
+ 0%, 15.8%, 92%, 100% { opacity: 0.75; }
197
+ 21.1%, 85% { opacity: 1; }
198
+ }
199
+ @keyframes cg-plan-name-3 {
200
+ 0%, 28.3%, 92%, 100% { opacity: 0.75; }
201
+ 33.6%, 85% { opacity: 1; }
202
+ }
203
+ .cg-plan[data-state="waiting"] .cg-plan-line { transform: scaleY(0); }
204
+ .cg-plan[data-state="waiting"] .cg-plan-dot { opacity: 0; }
205
+ .cg-plan[data-state="waiting"] .cg-plan-name { opacity: 0.75; }
206
+ .cg-plan[data-state="playing"] .cg-plan-line { animation: cg-plan-line 6s linear infinite; }
207
+ .cg-plan[data-state="playing"] .cg-plan-step[data-n="1"] .cg-plan-dot { animation: cg-plan-dot-1 6s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
208
+ .cg-plan[data-state="playing"] .cg-plan-step[data-n="2"] .cg-plan-dot { animation: cg-plan-dot-2 6s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
209
+ .cg-plan[data-state="playing"] .cg-plan-step[data-n="3"] .cg-plan-dot { animation: cg-plan-dot-3 6s cubic-bezier(0.23, 1, 0.32, 1) infinite; }
210
+ .cg-plan[data-state="playing"] .cg-plan-step[data-n="1"] .cg-plan-name { animation: cg-plan-name-1 6s ease infinite; }
211
+ .cg-plan[data-state="playing"] .cg-plan-step[data-n="2"] .cg-plan-name { animation: cg-plan-name-2 6s ease infinite; }
212
+ .cg-plan[data-state="playing"] .cg-plan-step[data-n="3"] .cg-plan-name { animation: cg-plan-name-3 6s ease infinite; }
213
+ /* Paused on the button, or off screen. Same weight as the rules above and after them. */
214
+ .cg-plan[data-state][data-stopped] .cg-plan-line,
215
+ .cg-plan[data-state][data-stopped] .cg-plan-step .cg-plan-dot,
216
+ .cg-plan[data-state][data-stopped] .cg-plan-step .cg-plan-name {
217
+ animation-play-state: paused;
218
+ }
219
+
220
+ /* PhotoSequence: the hidden checkbox pauses the photos through CSS. */
221
+ .cg-seq-pause:checked ~ .cg-seq img {
222
+ animation-play-state: paused !important;
223
+ }
224
+
225
+ /* Reduced motion: the components do not even start; this is the second lock,
226
+ in case the preference changes with the page open. The sequence keeps the
227
+ first photo and loses the control (a nameless checkbox in the tab order
228
+ would fail WCAG 4.1.2). */
229
+ @media (prefers-reduced-motion: reduce) {
230
+ .cg-chat .cg-bubble,
231
+ .cg-chat .cg-typing i,
232
+ .cg-plan .cg-plan-line,
233
+ .cg-plan .cg-plan-dot,
234
+ .cg-plan .cg-plan-name {
235
+ animation: none !important;
236
+ opacity: 1;
237
+ transform: none;
238
+ }
239
+ .cg-seq img {
240
+ animation: none !important;
241
+ }
242
+ .cg-seq-control,
243
+ .cg-seq-pause {
244
+ display: none;
245
+ }
246
+ }
package/tokens/creeta.css CHANGED
@@ -2,17 +2,22 @@
2
2
  Creeta palette (RTA clinic in Floripa) — petrol + mist + magenta.
3
3
  Values from novo.creeta. Measured contrasts (WCAG formula):
4
4
  - surface #eef3f4 / ink #144753 ............ ~9.1:1 (AAA)
5
- - surface #eef3f4 / ink-soft #52707a ....... ~4.7:1 (AA)
5
+ - surface #eef3f4 / ink-soft #266675 ....... 5.79:1 (AA) — the site's "teal-escuro":
6
+ second tone of two-tone headings, eyebrows, strong links
7
+ - surface #eef3f4 / ink-body #1e4f5b ....... 8.07:1 (AAA)
6
8
  - action #ab2781 (magenta) is the CTA ("Agendar"); text on it = white.
7
9
  */
8
10
  :root {
9
11
  --cg-surface: #eef3f4; /* mist */
10
12
  --cg-surface-panel: #144753; /* petrol */
13
+ --cg-card: #ffffff; /* cards, pills and menus over the page */
11
14
  --cg-ink: #144753; /* petrol */
12
- --cg-ink-soft: #52707a;
15
+ --cg-ink-soft: #266675; /* teal-escuro */
13
16
  --cg-ink-body: #1e4f5b;
14
17
  --cg-action: #ab2781; /* magenta (Agendar) */
15
18
  --cg-action-text: #8f1f6c; /* darker magenta for text */
19
+ --cg-action-tint: #eae7ed; /* 6% magenta over mist: highlighted card */
20
+ --cg-action-line: #ab2781; /* magenta itself: 5.62:1 on mist */
16
21
  --cg-on-action: #ffffff; /* white on magenta */
17
22
  --cg-line: rgb(20 71 83 / 0.16);
18
23
  --cg-font-heading: var(--font-quicksand), system-ui, sans-serif; /* Quicksand */
@@ -3,17 +3,25 @@
3
3
  purpose: color comes from EACH application (ttcab, creeta…). Use this as the
4
4
  base and override the --cg-* in your site's :root. Monochrome (gray/near-black)
5
5
  to make it clear it is a "bring your own color" placeholder.
6
- - surface #ffffff / ink #18181b ............ ~17:1 (AAA)
7
- - surface #ffffff / ink-soft #71717a ....... ~4.8:1 (AA)
6
+ The page is a light gray and the cards are white: the family is tonal (mist on
7
+ creeta, cream on ttcab), so the neutral palette is too — a white page would
8
+ swallow the white cards.
9
+ - surface #f4f4f5 / ink #18181b ............ 16.1:1 (AAA)
10
+ - surface #f4f4f5 / ink-soft #65656e ....... 5.25:1 (AA)
11
+ - surface #f4f4f5 / ink-body #3f3f46 ....... 9.5:1 (AAA)
12
+ - card #ffffff / ink-soft #65656e .......... 5.77:1 (AA)
8
13
  */
9
14
  :root {
10
- --cg-surface: #ffffff;
15
+ --cg-surface: #f4f4f5; /* page: light gray, so white cards read */
11
16
  --cg-surface-panel: #18181b;
17
+ --cg-card: #ffffff;
12
18
  --cg-ink: #18181b;
13
- --cg-ink-soft: #71717a;
19
+ --cg-ink-soft: #65656e;
14
20
  --cg-ink-body: #3f3f46;
15
21
  --cg-action: #18181b;
16
22
  --cg-action-text: #52525b;
23
+ --cg-action-tint: #e9e9ea;
24
+ --cg-action-line: #18181b;
17
25
  --cg-on-action: #ffffff;
18
26
  --cg-line: rgb(0 0 0 / 0.12);
19
27
  --cg-font-heading: var(--font-quicksand), system-ui, sans-serif; /* Quicksand (fixed across the family) */
package/tokens/ttcab.css CHANGED
@@ -3,19 +3,28 @@
3
3
  - surface #fbf5e9 / ink #144753 ............ 9.41:1 (AAA)
4
4
  - surface #fbf5e9 / ink-soft #54707a ....... 4.86:1 (AA)
5
5
  - surface #fbf5e9 / ink-body #1e4f5b ....... 8.32:1 (AAA)
6
- - surface #fbf5e9 / action-text #9a5d07 .... 4.90:1 (AA)
6
+ - surface #fbf5e9 / action-text #bd3f0c .... 4.98:1 (AA)
7
7
  - surface-panel #144753 / surface (text) ... 9.41:1 (AAA)
8
- - action #f6a22a is DECORATIVE/CTA — never text on cream.
8
+ - action #e8541e / on-action #ffffff ...... 3.67:1 — the accent button
9
+ The action color is a vivid red-orange with WHITE text (decided 2026-10-29,
10
+ the Cloudflare/Airbnb route): 3.67:1 passes the 3:1 of UI components and of
11
+ large text, NOT the 4.5:1 of normal text. Conscious exception, limited to the
12
+ accent Button (one per block, same type scale as the other buttons). Small
13
+ controls never put white on it (ink, or action-tint + action-text). The logo
14
+ keeps its own #f6a22a.
9
15
  */
10
16
  :root {
11
17
  --cg-surface: #fbf5e9;
12
18
  --cg-surface-panel: #144753;
19
+ --cg-card: #ffffff; /* cards, pills and menus over the page */
13
20
  --cg-ink: #144753;
14
21
  --cg-ink-soft: #54707a;
15
22
  --cg-ink-body: #1e4f5b;
16
- --cg-action: #f6a22a;
17
- --cg-on-action: #144753; /* ink on orange */
18
- --cg-action-text: #9a5d07;
23
+ --cg-action: #e8541e; /* vivid red-orange: white on it 3.67:1 (large text / UI 3:1) */
24
+ --cg-on-action: #ffffff;
25
+ --cg-action-text: #bd3f0c; /* 4.98:1 on cream, 4.64:1 on the tint */
26
+ --cg-action-tint: #faebdd; /* 6% action over cream: highlighted card (ink 8.75:1) */
27
+ --cg-action-line: #e8541e; /* the action color itself: 3.38:1 on cream */
19
28
  --cg-line: rgb(20 71 83 / 0.16);
20
29
  --cg-font-heading: var(--font-quicksand), system-ui, sans-serif; /* Quicksand */
21
30
  --cg-font-body: var(--font-figtree), system-ui, sans-serif; /* Figtree */