@breathe-ds/ui 0.6.7 → 0.6.9
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 +12 -0
- package/components/Curtain.tsx +4 -9
- package/components/FounderSpotlight.tsx +109 -0
- package/components/TestimonialCard.tsx +20 -3
- package/components/curtainHeadScript.ts +13 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -162,6 +162,8 @@ files (`public/brand`) in the repositories, split into two components so the
|
|
|
162
162
|
|
|
163
163
|
```tsx
|
|
164
164
|
<Curtain logo={<TtcabSymbol />} name={<TtcabWordmark />} wordmarkRatio={TTCAB_LOCKUP.wordmarkPerSymbol} tagline="…" />
|
|
165
|
+
// once per session: the head script, from its own module (callable in a server layout, 0.6.8)
|
|
166
|
+
// import { curtainHeadScript } from "@breathe-ds/ui/components/curtainHeadScript";
|
|
165
167
|
<Lockup variant="horizontal" size="md" logo={<CreetaSymbol />} wordmark={<CreetaWordmark />} wordmarkRatio={CREETA_LOCKUP.wordmarkPerSymbol} name="creeta" />
|
|
166
168
|
```
|
|
167
169
|
|
|
@@ -196,6 +198,10 @@ from color, not from extra markup.
|
|
|
196
198
|
portrait on the left (photo or `living`, the team pages' living portrait), then
|
|
197
199
|
eyebrow, name, role, registration, bio and `actions`. `variant` `plain` (on the page
|
|
198
200
|
color) or `card` (white card). Stacks on phones.
|
|
201
|
+
- **FounderSpotlight**: the family founder (Mariangela, creator of RTA and TTCAB) in one
|
|
202
|
+
block: `ProfileSpotlight` card + living portrait on hover, filled with the text validated
|
|
203
|
+
on ttcab (pt/en/es), her Instagram and the courses link. `<FounderSpotlight locale="pt" />`.
|
|
204
|
+
Media live in each site's `public/` (`/images/mariangela.png`, `/equipe/vivas/mariangela.mp4|.webp`).
|
|
199
205
|
- **Texture & rhythm (opt-in)**: `.cg-grain` adds a barely-there paper grain (noise in
|
|
200
206
|
the ink color) to any background — the body, a band. `bg-surface-alt` (token
|
|
201
207
|
`--cg-surface-alt`, falls back to `surface`) is a band one step deeper than the page,
|
|
@@ -306,6 +312,12 @@ from color, not from extra markup.
|
|
|
306
312
|
the page-level pause contract (`CARD_VIDEOS_PAUSE_EVENT` / `CARD_VIDEOS_PAUSED_ATTR`).
|
|
307
313
|
- **TestimonialCard**: clip with a play button on the left, quote + author +
|
|
308
314
|
outbound link on the right ("Famílias contam").
|
|
315
|
+
**Rule (0.6.8): in a grid of testimonials with `panel`, the card is ONLY a quote**
|
|
316
|
+
— a short excerpt in the person's own words, large, with the drawn quote mark;
|
|
317
|
+
the whole testimonial (and its video or photo) opens in the panel on click. Pass
|
|
318
|
+
the `excerpt` the client chose (`excerptMarks={false}` for a context line that is
|
|
319
|
+
not the person's speech); without one, the card takes the first sentence.
|
|
320
|
+
Never show the long text clamped on the card.
|
|
309
321
|
- **MapCard**: the Mapa of novo.creeta — a still preview fading into the card,
|
|
310
322
|
the address, "View the map" and the outbound "Directions" link; Google is only
|
|
311
323
|
called after the tap (no consent banner, lighter page). Open, the map fills
|
package/components/Curtain.tsx
CHANGED
|
@@ -98,16 +98,11 @@ export function Curtain({ name, tagline, logo, wordmarkRatio, symbolSide = "left
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
* Script for the <head
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* // Next (app/layout):
|
|
105
|
-
* <script dangerouslySetInnerHTML={{ __html: curtainHeadScript("cg-curtain") }} />
|
|
101
|
+
* Script for the <head>: moved to `components/curtainHeadScript.ts` (2026-11-05)
|
|
102
|
+
* so a server layout can call it (this file is "use client"). Kept here for old
|
|
103
|
+
* imports from client code.
|
|
106
104
|
*/
|
|
107
|
-
export
|
|
108
|
-
const k = JSON.stringify(storageKey);
|
|
109
|
-
return `try{var d=document.documentElement;if(sessionStorage.getItem(${k})){d.setAttribute('data-cg-curtain','seen')}else{sessionStorage.setItem(${k},'1')}}catch(e){}`;
|
|
110
|
-
}
|
|
105
|
+
export { curtainHeadScript } from "./curtainHeadScript";
|
|
111
106
|
|
|
112
107
|
const CURTAIN_CSS = `
|
|
113
108
|
.cg-curtain{
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { ProfileSpotlight } from "./ProfileSpotlight";
|
|
3
|
+
import { IconGraduationCap, IconInstagram } from "./icons";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The founder of the family: Mariangela Pinheiro de Lima, who created the RTA
|
|
7
|
+
* Method and TTCAB. One block, one text, for every site of the family (ttcab,
|
|
8
|
+
* metodorta, creeta…), so she reads the same everywhere. The text is the one
|
|
9
|
+
* validated on the ttcab site (06/11/2026), in pt, en and es.
|
|
10
|
+
*
|
|
11
|
+
* Media stay in each site's `public/` (the package ships no binaries); copy them
|
|
12
|
+
* from the ttcab site, or pass your own paths:
|
|
13
|
+
* /images/mariangela.png still portrait (4:5)
|
|
14
|
+
* /equipe/vivas/mariangela.mp4 + .webp living portrait (plays on hover)
|
|
15
|
+
*/
|
|
16
|
+
export const FOUNDER = {
|
|
17
|
+
name: "Mariangela Pinheiro de Lima",
|
|
18
|
+
registration: "CREFITO-10/2894-F",
|
|
19
|
+
instagram: "https://www.instagram.com/mariangela.rta/",
|
|
20
|
+
courses: "https://metodorta.com.br/cursos-e-eventos/",
|
|
21
|
+
pt: {
|
|
22
|
+
eyebrow: "Idealizadora",
|
|
23
|
+
role: "Criadora do método Reequilíbrio Toracoabdominal e fundadora da Escola Brasileira de RTA e Biomecânica Respiratória",
|
|
24
|
+
roleShort: "Criadora do Método RTA e do TTCAB",
|
|
25
|
+
bio: "Mariangela tem mais de 40 anos de carreira na Fisioterapia, com experiências marcantes na área pediátrica e respiratória. Iniciou como professora na PUCPR e atuou por sete anos nos hospitais Pequeno Príncipe e César Pernetta, em Curitiba. Especializou-se no Método Bobath, com influência de Oacy Veronesi e Sherry Arndt. Seu interesse em Fisioterapia Respiratória a levou a criar o Método Reequilíbrio Toracoabdominal (RTA), difundido há mais de três décadas por todo o Brasil. Também idealizou o TTCAB, a formação para o tratamento do torcicolo congênito e de outras assimetrias no bebê.",
|
|
26
|
+
instagram: "Instagram da Mariangela",
|
|
27
|
+
courses: "Cursos no site do Método RTA",
|
|
28
|
+
newTab: "(abre em nova aba)",
|
|
29
|
+
},
|
|
30
|
+
en: {
|
|
31
|
+
eyebrow: "Founder",
|
|
32
|
+
role: "Creator of the Reequilíbrio Toracoabdominal method and founder of the Escola Brasileira de RTA e Biomecânica Respiratória",
|
|
33
|
+
roleShort: "Creator of the RTA Method and of TTCAB",
|
|
34
|
+
bio: "Mariangela has a career of more than 40 years in Physiotherapy, with remarkable experience in paediatrics and respiratory care. She began as a lecturer at PUCPR and worked for seven years at the Pequeno Príncipe and César Pernetta hospitals in Curitiba. She specialised in the Bobath Concept, influenced by Oacy Veronesi and Sherry Arndt. Her interest in Respiratory Physiotherapy led her to create the Thoracoabdominal Rebalancing Method (RTA), taught across Brazil for more than three decades. She also created TTCAB, the training in the treatment of congenital torticollis and other asymmetries in babies.",
|
|
35
|
+
instagram: "Mariangela's Instagram",
|
|
36
|
+
courses: "Courses on the Método RTA website",
|
|
37
|
+
newTab: "(opens in a new tab)",
|
|
38
|
+
},
|
|
39
|
+
es: {
|
|
40
|
+
eyebrow: "Idealizadora",
|
|
41
|
+
role: "Creadora del método Reequilíbrio Toracoabdominal y fundadora de la Escola Brasileira de RTA e Biomecânica Respiratória",
|
|
42
|
+
roleShort: "Creadora del Método RTA y del TTCAB",
|
|
43
|
+
bio: "Mariangela tiene más de 40 años de carrera en la Fisioterapia, con experiencias destacadas en el área pediátrica y respiratoria. Comenzó como profesora en la PUCPR y trabajó durante siete años en los hospitales Pequeno Príncipe y César Pernetta, en Curitiba. Se especializó en el Método Bobath, con influencia de Oacy Veronesi y Sherry Arndt. Su interés por la Fisioterapia Respiratoria la llevó a crear el Método Reequilibrio Toracoabdominal (RTA), difundido desde hace más de tres décadas en todo Brasil. También ideó el TTCAB, la formación para el tratamiento de la tortícolis congénita y otras asimetrías en el bebé.",
|
|
44
|
+
instagram: "Instagram de Mariangela",
|
|
45
|
+
courses: "Cursos en el sitio del Método RTA",
|
|
46
|
+
newTab: "(se abre en una pestaña nueva)",
|
|
47
|
+
},
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export type FounderLocale = "pt" | "en" | "es";
|
|
51
|
+
|
|
52
|
+
type Props = {
|
|
53
|
+
locale?: FounderLocale;
|
|
54
|
+
/** Where "courses" points (default: the Método RTA courses page). */
|
|
55
|
+
coursesHref?: string;
|
|
56
|
+
/** Media paths in the site's public/ (defaults above). */
|
|
57
|
+
photo?: string;
|
|
58
|
+
living?: { src: string; poster: string } | false;
|
|
59
|
+
/** Your own image element (e.g. next/image) in place of the photo. */
|
|
60
|
+
media?: ReactNode;
|
|
61
|
+
/** Links or buttons at the foot, after the text. */
|
|
62
|
+
actions?: ReactNode;
|
|
63
|
+
variant?: "plain" | "card";
|
|
64
|
+
headingLevel?: "h2" | "h3";
|
|
65
|
+
id?: string;
|
|
66
|
+
className?: string;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Founder spotlight: `ProfileSpotlight` (card, living portrait on hover) filled
|
|
71
|
+
* with the founder's validated text, her Instagram and the courses link. Sites
|
|
72
|
+
* only drop it in: `<FounderSpotlight locale="pt" />`.
|
|
73
|
+
*/
|
|
74
|
+
export function FounderSpotlight({
|
|
75
|
+
locale = "pt",
|
|
76
|
+
coursesHref = FOUNDER.courses,
|
|
77
|
+
photo = "/images/mariangela.png",
|
|
78
|
+
living = { src: "/equipe/vivas/mariangela.mp4", poster: "/equipe/vivas/mariangela.webp" },
|
|
79
|
+
media,
|
|
80
|
+
actions,
|
|
81
|
+
variant = "card",
|
|
82
|
+
headingLevel = "h2",
|
|
83
|
+
id,
|
|
84
|
+
className,
|
|
85
|
+
}: Props) {
|
|
86
|
+
const t = FOUNDER[locale];
|
|
87
|
+
return (
|
|
88
|
+
<ProfileSpotlight
|
|
89
|
+
variant={variant}
|
|
90
|
+
headingLevel={headingLevel}
|
|
91
|
+
id={id}
|
|
92
|
+
className={className}
|
|
93
|
+
eyebrow={t.eyebrow}
|
|
94
|
+
name={FOUNDER.name}
|
|
95
|
+
role={t.role}
|
|
96
|
+
registration={FOUNDER.registration}
|
|
97
|
+
bio={t.bio}
|
|
98
|
+
photo={{ src: photo, alt: FOUNDER.name }}
|
|
99
|
+
living={living || undefined}
|
|
100
|
+
media={media}
|
|
101
|
+
actions={actions}
|
|
102
|
+
social={[
|
|
103
|
+
{ label: t.instagram, href: FOUNDER.instagram, Icon: IconInstagram },
|
|
104
|
+
{ label: t.courses, href: coursesHref, Icon: IconGraduationCap },
|
|
105
|
+
]}
|
|
106
|
+
newTabLabel={t.newTab}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -79,7 +79,11 @@ type Props = {
|
|
|
79
79
|
* or only in the panel — the card stays a clean quote with the avatar. */
|
|
80
80
|
mediaPlacement?: "card" | "panel";
|
|
81
81
|
/** The card shows only this excerpt, large, as a pull quote (the person's own
|
|
82
|
-
* words, never rewritten); the whole `quote` goes to the panel.
|
|
82
|
+
* words, never rewritten); the whole `quote` goes to the panel.
|
|
83
|
+
* RULE (2026-11-05, Felipe): with `panel`, the card is ONLY a quote — never
|
|
84
|
+
* the long text clamped. Pass the excerpt the client chose; without one, the
|
|
85
|
+
* card takes the first sentence of a string `quote` (cut at a word, with "…",
|
|
86
|
+
* when it runs long). The whole testimonial always opens in the panel. */
|
|
83
87
|
excerpt?: ReactNode;
|
|
84
88
|
/** Quote marks around the excerpt. false for a context line that is not the
|
|
85
89
|
* person's speech (e.g. the description of a video). Default: true. */
|
|
@@ -179,8 +183,21 @@ function TapVideo({ src, youtubeId, poster: posterProp, description, withSound =
|
|
|
179
183
|
// whole class names, so Tailwind generates them
|
|
180
184
|
const CLAMP: Record<number, string> = { 3: "line-clamp-3", 4: "line-clamp-4", 5: "line-clamp-5", 6: "line-clamp-6", 8: "line-clamp-8" };
|
|
181
185
|
|
|
186
|
+
/** First sentence of a testimonial, for the card when no excerpt was chosen.
|
|
187
|
+
* Long sentences are cut at a word (≤ 140 characters) with "…". */
|
|
188
|
+
export function firstSentence(text: string, max = 140): string {
|
|
189
|
+
const clean = text.replace(/\s+/g, " ").trim();
|
|
190
|
+
const m = /^(.+?[.!?])(\s|$)/.exec(clean);
|
|
191
|
+
const s = m ? m[1] : clean;
|
|
192
|
+
if (s.length <= max) return s;
|
|
193
|
+
const cut = s.slice(0, max);
|
|
194
|
+
return `${cut.slice(0, cut.lastIndexOf(" ")).replace(/[,;:\s]+$/, "")}…`;
|
|
195
|
+
}
|
|
196
|
+
|
|
182
197
|
export function TestimonialCard({ quote, author, authorPhoto, authorAvatar, authors, link, video, photo, mediaPlacement = "card", role, joiners, panel, excerpt, excerptMarks = true, clampLines, as: Root = "div", labels, className = "" }: Props) {
|
|
183
198
|
const t = { ...EN, ...labels };
|
|
199
|
+
// with a panel the card is only a quote: the given excerpt, or the first sentence
|
|
200
|
+
const shownExcerpt = excerpt ?? (panel && typeof quote === "string" ? firstSentence(quote) : undefined);
|
|
184
201
|
const watch = (d: string) => (typeof t.watch === "function" ? t.watch(d) : t.watch.replace("{description}", d));
|
|
185
202
|
const people = authors ?? [{ name: author ?? "", photo: authorPhoto, avatar: authorAvatar }];
|
|
186
203
|
const cardPhoto = mediaPlacement === "card" ? photo : undefined;
|
|
@@ -227,7 +244,7 @@ export function TestimonialCard({ quote, author, authorPhoto, authorAvatar, auth
|
|
|
227
244
|
) : null}
|
|
228
245
|
<figure className={`flex flex-1 flex-col justify-between gap-5 ${cardVideo || cardPhoto ? "p-5 md:p-7" : "p-6 md:p-8"}`}>
|
|
229
246
|
<div>
|
|
230
|
-
{
|
|
247
|
+
{shownExcerpt ? (
|
|
231
248
|
<blockquote className="m-0">
|
|
232
249
|
{/* the family's drawn mark (same as PullQuote) above the line; the line itself stays clean */}
|
|
233
250
|
{excerptMarks && (
|
|
@@ -235,7 +252,7 @@ export function TestimonialCard({ quote, author, authorPhoto, authorAvatar, auth
|
|
|
235
252
|
<path d="M0 36V22C0 10.5 6 2.6 18.2 0l2.3 5.2C14.6 7.4 11.4 11 11 16h8.6v20H0Zm27.4 0V22c0-11.5 6-19.4 18.2-22l2.3 5.2C42 7.4 38.8 11 38.4 16H47v20H27.4Z" />
|
|
236
253
|
</svg>
|
|
237
254
|
)}
|
|
238
|
-
<p className="m-0 font-heading text-[1.3rem] leading-snug font-semibold text-balance text-ink md:text-[1.5rem]">{
|
|
255
|
+
<p className="m-0 font-heading text-[1.3rem] leading-snug font-semibold text-balance text-ink md:text-[1.5rem]">{shownExcerpt}</p>
|
|
239
256
|
</blockquote>
|
|
240
257
|
) : (
|
|
241
258
|
<blockquote
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script for the <head> (before paint) of the Curtain: marks the curtain as seen
|
|
3
|
+
* in this session, so it does not repeat on navigation or reload. Without it, the
|
|
4
|
+
* curtain shows on every load. Plain module (no "use client"): a server layout
|
|
5
|
+
* can call it.
|
|
6
|
+
* // Next (app/layout):
|
|
7
|
+
* import { curtainHeadScript } from "@breathe-ds/ui/components/curtainHeadScript";
|
|
8
|
+
* <script dangerouslySetInnerHTML={{ __html: curtainHeadScript("cg-curtain") }} />
|
|
9
|
+
*/
|
|
10
|
+
export function curtainHeadScript(storageKey = "cg-curtain") {
|
|
11
|
+
const k = JSON.stringify(storageKey);
|
|
12
|
+
return `try{var d=document.documentElement;if(sessionStorage.getItem(${k})){d.setAttribute('data-cg-curtain','seen')}else{sessionStorage.setItem(${k},'1')}}catch(e){}`;
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@breathe-ds/ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"description": "Breathe DS — the design system of the Cuidado Global family of sites (creeta, ttcab, metodorta). Themeable tokens + React components.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"./components/BottomNav": "./components/BottomNav.tsx",
|
|
51
51
|
"./components/BreatheMark": "./components/BreatheMark.tsx",
|
|
52
52
|
"./components/Curtain": "./components/Curtain.tsx",
|
|
53
|
+
"./components/curtainHeadScript": "./components/curtainHeadScript.ts",
|
|
53
54
|
"./components/Lockup": "./components/Lockup.tsx",
|
|
54
55
|
"./tokens/neutral.css": {
|
|
55
56
|
"style": "./tokens/neutral.css",
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
"./components/SidePanel": "./components/SidePanel.tsx",
|
|
100
101
|
"./components/PullQuote": "./components/PullQuote.tsx",
|
|
101
102
|
"./components/ProfileSpotlight": "./components/ProfileSpotlight.tsx",
|
|
103
|
+
"./components/FounderSpotlight": "./components/FounderSpotlight.tsx",
|
|
102
104
|
"./components/FilterBar": "./components/FilterBar.tsx",
|
|
103
105
|
"./components/Callout": "./components/Callout.tsx",
|
|
104
106
|
"./components/KenBurnsImage": "./components/KenBurnsImage.tsx"
|