@breathe-ds/ui 0.6.10 → 0.6.12
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 +9 -0
- package/components/EvidenceSection.tsx +107 -0
- package/components/PageHero.tsx +55 -12
- package/package.json +2 -1
- package/theme.css +5 -0
package/README.md
CHANGED
|
@@ -207,6 +207,10 @@ from color, not from extra markup.
|
|
|
207
207
|
- **TestimonialsSection**: the testimonials block, as on ttcab (the standard): "Depoimentos /
|
|
208
208
|
Histórias de sucesso" (pt/en/es), masonry in two columns, video first, pull-quote cards that
|
|
209
209
|
open the side panel. Sites pass only `items`.
|
|
210
|
+
- **EvidenceSection**: the evidence block, as on ttcab (the standard): "Evidência /
|
|
211
|
+
Referências científicas" (pt/en/es), heading sticky on the left, article rows on the right,
|
|
212
|
+
authors joined by " · " and cleaned (`authorNames`, `authorLine`). Sites pass `items`,
|
|
213
|
+
`allHref` and the `lead` (what the collection is about).
|
|
210
214
|
- **Texture & rhythm (opt-in)**: `.cg-grain` adds a barely-there paper grain (noise in
|
|
211
215
|
the ink color) to any background — the body, a band. `bg-surface-alt` (token
|
|
212
216
|
`--cg-surface-alt`, falls back to `surface`) is a band one step deeper than the page,
|
|
@@ -220,6 +224,11 @@ from color, not from extra markup.
|
|
|
220
224
|
centred) + **FilterBar** (thin sticky bar of compact filters — `SelectField density="sm"` —
|
|
221
225
|
with the count and "clear" on the right; it publishes its height as `--cg-filterbar-h`, so
|
|
222
226
|
`StickyGroupHeading stickyTop="top-[calc(80px+var(--cg-filterbar-h,0px))]"` sticks right below).
|
|
227
|
+
- **Directory hero with people** (0.6.12): `PageHero photos={[…]}` — the photo wall from
|
|
228
|
+
metodorta's professionals page: real portraits (4:5) in three columns drifting up on the right
|
|
229
|
+
half, fading into the band; a strip of faces on phones. Decorative, still with reduced motion,
|
|
230
|
+
needs ≥ 6 photos, replaces the watermark. `tone="callout"` gives the panel the Callout's
|
|
231
|
+
colour ("info well"), as on metodorta.
|
|
223
232
|
- **Callout** (a.k.a. info well): a secondary invitation under a page's main control
|
|
224
233
|
(location invite under the professionals search, a tip under the articles search), at
|
|
225
234
|
most one action. Never for errors or warnings. Default `tone="wash"`: no border, the
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Button } from "./Button";
|
|
3
|
+
import { SectionHeading } from "./Heading";
|
|
4
|
+
import { LinkRow, RowList } from "./Rows";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Evidence block of the family (0.6.11, 06/11/2026): the ttcab section made the
|
|
8
|
+
* standard. Two columns: heading, lead and "see all" on the left (sticky while
|
|
9
|
+
* reading), the articles on the right, one row each: title in bold, authors
|
|
10
|
+
* below joined by " · ", without affiliation marks or degrees. Sites pass the
|
|
11
|
+
* articles, the link to the full list and the lead (what the collection is
|
|
12
|
+
* about); the eyebrow, title and link text are the ones validated on ttcab.
|
|
13
|
+
* Put it inside your section's Container.
|
|
14
|
+
*/
|
|
15
|
+
const COPY = {
|
|
16
|
+
pt: { eyebrow: "Evidência", title: "Referências científicas", all: "Ver todos os artigos" },
|
|
17
|
+
en: { eyebrow: "Evidence", title: "Scientific references", all: "See all articles" },
|
|
18
|
+
es: { eyebrow: "Evidencia", title: "Referencias científicas", all: "Ver todos los artículos" },
|
|
19
|
+
} as const;
|
|
20
|
+
|
|
21
|
+
// degrees the PDFs glue to the names ("Nathan Watemberg MDa", "Stephanie Nichter, DPT, PT, CCI1")
|
|
22
|
+
const DEGREES = new Set(["MD", "MDa", "PhD", "PT", "DPT", "MS", "MSN", "MSPT", "RN", "CRNP", "AuD", "AS", "CCI", "R.T.", "(R)", "(CT)", "RT"]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Authors as a clean list (the ttcab cleaning): no affiliation numbers or
|
|
26
|
+
* symbols (1,2 * †), no loose affiliation letters (",a"), no degrees, and the
|
|
27
|
+
* mixed separators of the records (& • · ; , and e) unified.
|
|
28
|
+
*/
|
|
29
|
+
export function authorNames(raw: string | undefined): string[] {
|
|
30
|
+
if (!raw) return [];
|
|
31
|
+
const clean = raw
|
|
32
|
+
.replace(/[\d*†‡§]+/g, "")
|
|
33
|
+
.replace(/,\s*[a-d](?=[\s,]|$)/g, ",")
|
|
34
|
+
.replace(/\s+/g, " ");
|
|
35
|
+
return clean
|
|
36
|
+
.split(/\s*(?:;|•|·|&|,\s*(?:and|e)\s+|\s+and\s+|\s+e\s+(?=[A-ZÀ-Ú])|,)\s*/)
|
|
37
|
+
.map((name) =>
|
|
38
|
+
name
|
|
39
|
+
.split(" ")
|
|
40
|
+
.filter((w) => w && !DEGREES.has(w) && !DEGREES.has(w.replace(/[,.]$/, "")))
|
|
41
|
+
.join(" ")
|
|
42
|
+
.replace(/[,.\s]+$/, "")
|
|
43
|
+
.trim(),
|
|
44
|
+
)
|
|
45
|
+
.filter((name) => name.split(" ").length >= 2 || /^[A-ZÀ-Ú][a-zà-ú]+[A-Z]/.test(name));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** "Ana Lima1, Beto Souza PhD e Caio Reis." → "Ana Lima · Beto Souza · Caio Reis" */
|
|
49
|
+
export const authorLine = (raw: string | string[] | undefined) =>
|
|
50
|
+
(Array.isArray(raw) ? raw : authorNames(raw)).join(" · ");
|
|
51
|
+
|
|
52
|
+
export type EvidenceArticle = {
|
|
53
|
+
href: string;
|
|
54
|
+
title: string;
|
|
55
|
+
/** Raw authors from the record, or an already clean list. */
|
|
56
|
+
authors?: string | string[];
|
|
57
|
+
/** Language of the title when it differs from the page (e.g. "en"). */
|
|
58
|
+
lang?: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type Props = {
|
|
62
|
+
items: EvidenceArticle[];
|
|
63
|
+
/** Where "see all" points (the site's article list). */
|
|
64
|
+
allHref: string;
|
|
65
|
+
/** What the collection is about (one sentence, from the site). */
|
|
66
|
+
lead?: ReactNode;
|
|
67
|
+
locale?: "pt" | "en" | "es";
|
|
68
|
+
/** Override only when the client asks; the default is the standard. */
|
|
69
|
+
eyebrow?: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
allLabel?: string;
|
|
72
|
+
headingId?: string;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export function EvidenceSection({ items, allHref, lead, locale = "pt", eyebrow, title, allLabel, headingId }: Props) {
|
|
76
|
+
const c = COPY[locale];
|
|
77
|
+
return (
|
|
78
|
+
<div className="grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-16">
|
|
79
|
+
<div className="lg:sticky lg:top-44 lg:self-start">
|
|
80
|
+
<SectionHeading id={headingId} eyebrow={eyebrow ?? c.eyebrow} title={title ?? c.title} lead={lead} />
|
|
81
|
+
<div className="mt-6">
|
|
82
|
+
<Button variant="link" href={allHref}>
|
|
83
|
+
{allLabel ?? c.all} →
|
|
84
|
+
</Button>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
<RowList>
|
|
88
|
+
{items.map((a) => {
|
|
89
|
+
const authors = authorLine(a.authors);
|
|
90
|
+
return (
|
|
91
|
+
<LinkRow
|
|
92
|
+
key={a.href}
|
|
93
|
+
className="py-4"
|
|
94
|
+
href={a.href}
|
|
95
|
+
label={
|
|
96
|
+
<>
|
|
97
|
+
<span className="block" lang={a.lang}>{a.title}</span>
|
|
98
|
+
{authors && <span className="mt-1 block text-sm font-normal text-ink-soft">{authors}</span>}
|
|
99
|
+
</>
|
|
100
|
+
}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</RowList>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
package/components/PageHero.tsx
CHANGED
|
@@ -16,15 +16,54 @@ type Props = {
|
|
|
16
16
|
* 6% in `ink`). Decorative: rendered aria-hidden. */
|
|
17
17
|
watermark?: ReactNode;
|
|
18
18
|
/** `alt` (default): the section band colour (`surface-alt`), light, ink
|
|
19
|
-
* text at full contrast (8.85:1 on ttcab). `card`: white.
|
|
20
|
-
|
|
19
|
+
* text at full contrast (8.85:1 on ttcab). `card`: white. `callout`: the
|
|
20
|
+
* colour of the Callout ("info well"), a step deeper (0.6.12). */
|
|
21
|
+
tone?: "alt" | "card" | "callout";
|
|
21
22
|
/** `panel` (default): the rounded band with the tools inside. `centered`: the
|
|
22
23
|
* Apple search page — no box, title and lead centred, one search below on the
|
|
23
24
|
* page colour; refining filters live outside, after the results begin. */
|
|
24
25
|
variant?: "panel" | "centered";
|
|
26
|
+
/** Photo wall (0.6.12, from metodorta's professionals page): real portraits
|
|
27
|
+
* (4:5) in three columns drifting slowly up on the right half of the panel,
|
|
28
|
+
* fading into the band on the left so the title sits on a clean background;
|
|
29
|
+
* on phones, one strip of faces moving sideways at the top. Decorative
|
|
30
|
+
* (aria-hidden, alt=""), still with prefers-reduced-motion. Needs ≥ 6 photos;
|
|
31
|
+
* replaces the watermark. `panel` variant only. */
|
|
32
|
+
photos?: string[];
|
|
25
33
|
className?: string;
|
|
26
34
|
};
|
|
27
35
|
|
|
36
|
+
/** The photo wall of `PageHero` (CSS animation, no JS: 70–95 s a lap, the
|
|
37
|
+
* middle column the other way). Content is doubled, so the loop has no jump. */
|
|
38
|
+
function PhotoWall({ photos }: { photos: string[] }) {
|
|
39
|
+
const columns = [0, 1, 2].map((c) => photos.filter((_, i) => i % 3 === c));
|
|
40
|
+
const speed = ["animate-[cg-wall-up_80s_linear_infinite]", "animate-[cg-wall-up_95s_linear_infinite_reverse]", "animate-[cg-wall-up_70s_linear_infinite]"];
|
|
41
|
+
return (
|
|
42
|
+
<div aria-hidden="true" className="pointer-events-none">
|
|
43
|
+
<div className="absolute inset-y-0 right-0 hidden w-[52%] grid-cols-3 gap-3 px-3 [mask-image:linear-gradient(to_right,transparent,black_38%),linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)] [mask-composite:intersect] lg:grid">
|
|
44
|
+
{columns.map((col, c) => (
|
|
45
|
+
<div key={c} className="overflow-hidden">
|
|
46
|
+
<div className={`flex flex-col [&>img]:mb-3 motion-reduce:!animate-none ${speed[c]}`}>
|
|
47
|
+
{[...col, ...col].map((src, i) => (
|
|
48
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
49
|
+
<img key={i} src={src} alt="" width={384} height={480} decoding="async" className="aspect-[4/5] w-full rounded-[20px] object-cover" />
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="relative overflow-hidden pt-4 [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)] lg:hidden">
|
|
56
|
+
<div className="flex w-max [&>img]:mr-2 motion-reduce:!animate-none animate-[cg-wall-side_60s_linear_infinite]">
|
|
57
|
+
{[...photos.slice(0, 12), ...photos.slice(0, 12)].map((src, i) => (
|
|
58
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
59
|
+
<img key={i} src={src} alt="" width={384} height={480} decoding="async" className="aspect-[4/5] w-24 shrink-0 rounded-2xl object-cover sm:w-28" />
|
|
60
|
+
))}
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
28
67
|
/**
|
|
29
68
|
* PageHero — the opening panel of an inner page (a library, a directory).
|
|
30
69
|
* Wide, in novo.creeta's proportion (up to 1600px, 16/24px from the screen
|
|
@@ -35,7 +74,8 @@ type Props = {
|
|
|
35
74
|
* and the panel groups title, explanation and controls (common region)
|
|
36
75
|
* without the weight of a dark panel.
|
|
37
76
|
*/
|
|
38
|
-
export function PageHero({ title, lead, note, breadcrumb, children, watermark, tone = "alt", variant = "panel", className = "" }: Props) {
|
|
77
|
+
export function PageHero({ title, lead, note, breadcrumb, children, watermark, tone = "alt", variant = "panel", photos, className = "" }: Props) {
|
|
78
|
+
const wall = photos && photos.length >= 6 ? photos : null;
|
|
39
79
|
if (variant === "centered") {
|
|
40
80
|
return (
|
|
41
81
|
<section className={`px-4 pt-[clamp(40px,7vw,96px)] pb-[clamp(28px,4vw,48px)] text-center md:px-6 ${className}`}>
|
|
@@ -53,20 +93,23 @@ export function PageHero({ title, lead, note, breadcrumb, children, watermark, t
|
|
|
53
93
|
}
|
|
54
94
|
return (
|
|
55
95
|
<div className={`px-4 pt-2 md:px-6 md:pt-4 ${className}`}>
|
|
56
|
-
<section className={`relative isolate mx-auto max-w-[1600px] overflow-hidden rounded-[28px] md:rounded-[32px] ${tone === "card" ? "bg-card" : "bg-surface-alt"}`}>
|
|
57
|
-
{
|
|
96
|
+
<section className={`relative isolate mx-auto max-w-[1600px] overflow-hidden rounded-[28px] md:rounded-[32px] ${tone === "card" ? "bg-card" : tone === "callout" ? "bg-callout" : "bg-surface-alt"}`}>
|
|
97
|
+
{wall && <PhotoWall photos={wall} />}
|
|
98
|
+
{watermark && !wall && (
|
|
58
99
|
<span aria-hidden="true" className="pointer-events-none absolute -right-10 -bottom-16 -z-10 hidden w-[min(34vw,420px)] md:block">
|
|
59
100
|
{watermark}
|
|
60
101
|
</span>
|
|
61
102
|
)}
|
|
62
|
-
<div className=
|
|
103
|
+
<div className={`relative mx-auto w-full max-w-6xl px-5 sm:px-6 lg:px-8 ${wall ? "pt-[clamp(28px,5vw,88px)] pb-[clamp(32px,5vw,88px)] lg:min-h-[30rem]" : "py-[clamp(36px,6vw,72px)]"}`}>
|
|
63
104
|
{breadcrumb && <div className="mb-6">{breadcrumb}</div>}
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
105
|
+
<div className={wall ? "max-w-xl lg:max-w-[46%]" : undefined}>
|
|
106
|
+
<Heading as="h1" size="display" className="text-balance">
|
|
107
|
+
{title}
|
|
108
|
+
</Heading>
|
|
109
|
+
{lead && <p className="m-0 mt-4 max-w-xl text-lg leading-relaxed text-ink-body">{lead}</p>}
|
|
110
|
+
{note && <p className="m-0 mt-3 max-w-xl text-sm leading-relaxed text-ink-soft">{note}</p>}
|
|
111
|
+
</div>
|
|
112
|
+
{children && <div className={`mt-[clamp(24px,4vw,40px)] ${wall ? "lg:max-w-[52%]" : ""}`}>{children}</div>}
|
|
70
113
|
</div>
|
|
71
114
|
</section>
|
|
72
115
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@breathe-ds/ui",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
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",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"./components/ProfileSpotlight": "./components/ProfileSpotlight.tsx",
|
|
103
103
|
"./components/FounderSpotlight": "./components/FounderSpotlight.tsx",
|
|
104
104
|
"./components/TestimonialsSection": "./components/TestimonialsSection.tsx",
|
|
105
|
+
"./components/EvidenceSection": "./components/EvidenceSection.tsx",
|
|
105
106
|
"./components/FilterBar": "./components/FilterBar.tsx",
|
|
106
107
|
"./components/Callout": "./components/Callout.tsx",
|
|
107
108
|
"./components/KenBurnsImage": "./components/KenBurnsImage.tsx"
|
package/theme.css
CHANGED
|
@@ -321,3 +321,8 @@
|
|
|
321
321
|
mix-blend-mode: lighten;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
|
|
325
|
+
/* PageHero photo wall (0.6.12): columns drift up, the phone strip drifts sideways.
|
|
326
|
+
Half the track, because the content is doubled: the loop has no jump. */
|
|
327
|
+
@keyframes cg-wall-up { to { transform: translateY(-50%); } }
|
|
328
|
+
@keyframes cg-wall-side { to { transform: translateX(-50%); } }
|