@breathe-ds/ui 0.0.0-stage → 0.1.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.
@@ -0,0 +1,281 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState, type ReactNode } from "react";
4
+ import { BackgroundVideo } from "./BackgroundVideo";
5
+
6
+ type Panel = { kicker?: ReactNode; title: ReactNode; lead?: ReactNode };
7
+
8
+ /** Clipe de fundo local (modo loop), sem som, em repetição. */
9
+ type LoopPreview = {
10
+ src: string;
11
+ srcLarge?: string;
12
+ poster: string;
13
+ posterLarge?: string;
14
+ trimEdges?: boolean;
15
+ objectPosition?: string;
16
+ };
17
+
18
+ /** Dois tipos de hero com vídeo:
19
+ * - loop: um clipe local em repetição (sem player). Fundo = BackgroundVideo.
20
+ * - player: o PRÓPRIO vídeo do YouTube roda mudo, em repetição, como fundo
21
+ * (16:9, cobrindo o cartão, sem corte/zoom). Ao apertar o som ele
22
+ * remonta do zero — com áudio e controles — e surge a barra de
23
+ * fechar no pé. Mantém a consistência: a prévia e o vídeo são o
24
+ * mesmo material. */
25
+ type VideoLoop = { mode: "loop"; preview: LoopPreview };
26
+ type VideoPlayer = {
27
+ mode: "player";
28
+ youtubeId: string;
29
+ /** Quadro parado (movimento reduzido / economia de dados). Sem ele, usa a
30
+ * miniatura do próprio YouTube (maxresdefault). */
31
+ poster?: string;
32
+ legenda?: ReactNode;
33
+ };
34
+
35
+ type Props = {
36
+ /** Fundo em vídeo — define o tipo do hero (loop ou player). */
37
+ video: VideoLoop | VideoPlayer;
38
+ /** Slot no topo-esquerdo (selo de local, "← voltar" + ícone). */
39
+ topLeft?: ReactNode;
40
+ /** Legenda curta no canto inferior-esquerdo (pílula) — versão simples. */
41
+ caption?: ReactNode;
42
+ /** Painel de conteúdo inferior-esquerdo (kicker + título + lead). */
43
+ panel?: Panel;
44
+ /** CTAs no canto inferior-direito. No modo player, sem isto mostra "Assistir
45
+ * ao vídeo"; null esconde. No modo loop, só aparece o que vier aqui. */
46
+ actions?: ReactNode | null;
47
+ /** Rótulo + meta do botão padrão "Assistir ao vídeo" (modo player). */
48
+ watch?: { label?: string; meta?: ReactNode };
49
+ /** Rótulo do botão de fechar na barra inferior (modo player tocando). */
50
+ closeLabel?: string;
51
+ /** Rótulos do botão de pausa do fundo (modo player, prévia muda). */
52
+ previewLabels?: { pause?: string; play?: string };
53
+ videoTitle?: string;
54
+ };
55
+
56
+ function PlayGlyph({ size = 20 }: { size?: number }) {
57
+ return <svg viewBox="0 0 24 24" width={size} height={size} className="ml-0.5" fill="currentColor" aria-hidden="true"><path d="M6 3v18l15-9z" /></svg>;
58
+ }
59
+ function XGlyph({ size = 18 }: { size?: number }) {
60
+ return <svg viewBox="0 0 24 24" width={size} height={size} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" /></svg>;
61
+ }
62
+
63
+ /**
64
+ * Fundo em vídeo do YouTube rodando MUDO, em repetição, cobrindo o cartão
65
+ * (modo player, antes de o som começar). É enfeite: fica fora da leitura de
66
+ * tela. Respeita movimento reduzido e economia de dados (aí fica no quadro
67
+ * parado, sem pedir o vídeo). O "cover" é feito com unidades de container
68
+ * (cqw/cqh), então a proporção 16:9 nunca é distorcida nem cortada em falso.
69
+ * Tem botão de pausa (WCAG 2.2.2). O clique passa reto (pointer-events-none):
70
+ * quem recebe o toque é o botão de assistir, por cima.
71
+ */
72
+ function YouTubeBackground({
73
+ id,
74
+ poster,
75
+ title,
76
+ labels,
77
+ }: {
78
+ id: string;
79
+ poster?: string;
80
+ title?: string;
81
+ labels?: { pause?: string; play?: string };
82
+ }) {
83
+ const [show, setShow] = useState(false);
84
+ const [paused, setPaused] = useState(false);
85
+ const frame = useRef<HTMLIFrameElement>(null);
86
+
87
+ useEffect(() => {
88
+ const lessMotion =
89
+ typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
90
+ const saveData = (navigator as Navigator & { connection?: { saveData?: boolean } }).connection?.saveData;
91
+ if (lessMotion || saveData) return; // fica no quadro parado
92
+ setShow(true);
93
+ }, []);
94
+
95
+ function toggle() {
96
+ const w = frame.current?.contentWindow;
97
+ if (!w) return;
98
+ // API do YouTube por postMessage (enablejsapi=1): pausa/retoma sem carregar o script.
99
+ w.postMessage(JSON.stringify({ event: "command", func: paused ? "playVideo" : "pauseVideo", args: [] }), "*");
100
+ setPaused((p) => !p);
101
+ }
102
+
103
+ const posterSrc = poster ?? `https://i.ytimg.com/vi/${id}/maxresdefault.jpg`;
104
+
105
+ return (
106
+ <div className="absolute inset-0 overflow-hidden" style={{ containerType: "size" }}>
107
+ {show ? (
108
+ <iframe
109
+ ref={frame}
110
+ title={title ?? "Vídeo"}
111
+ tabIndex={-1}
112
+ aria-hidden="true"
113
+ src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1&mute=1&loop=1&playlist=${id}&controls=0&disablekb=1&fs=0&modestbranding=1&rel=0&playsinline=1&iv_load_policy=3&enablejsapi=1`}
114
+ allow="autoplay; encrypted-media"
115
+ className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 border-0"
116
+ style={{ width: "max(100cqw, 177.78cqh)", height: "max(100cqh, 56.25cqw)" }}
117
+ />
118
+ ) : (
119
+ // miniatura do próprio YouTube (16:9) quando não se toca o vídeo
120
+ // eslint-disable-next-line @next/next/no-img-element
121
+ <img src={posterSrc} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
122
+ )}
123
+ {show && (
124
+ <button
125
+ type="button"
126
+ onClick={toggle}
127
+ aria-label={paused ? labels?.play ?? "Reproduzir vídeo" : labels?.pause ?? "Pausar vídeo"}
128
+ className="absolute top-4 right-4 z-10 flex h-11 w-11 items-center justify-center rounded-full bg-ink/85 text-surface backdrop-blur-md transition-colors duration-200 hover:bg-ink focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface motion-reduce:transition-none"
129
+ >
130
+ {paused ? (
131
+ <svg viewBox="0 0 24 24" className="ml-0.5 h-[18px] w-[18px]" fill="currentColor" aria-hidden="true"><path d="M6 3v18l15-9z" /></svg>
132
+ ) : (
133
+ <svg viewBox="0 0 24 24" className="h-[18px] w-[18px]" fill="currentColor" aria-hidden="true"><rect x="6" y="4" width="4" height="16" rx="1.2" /><rect x="14" y="4" width="4" height="16" rx="1.2" /></svg>
134
+ )}
135
+ </button>
136
+ )}
137
+ </div>
138
+ );
139
+ }
140
+
141
+ /**
142
+ * Hero full-bleed da família Cuidado Global — espelha o "TopoVideo" do
143
+ * novo.creeta (tamanhos/espaçamentos exatos: max-w-1600, rounded-32,
144
+ * min-h 560/820), com a linguagem do Breathe (tokens: ink/surface).
145
+ *
146
+ * - loop: o fundo é um clipe local em repetição (BackgroundVideo).
147
+ * - player: o fundo é o PRÓPRIO vídeo do YouTube rodando mudo, em repetição.
148
+ * Ao apertar o som, ele remonta do zero (com áudio e controles) no
149
+ * próprio cartão e abre a barra de fechar no pé (legenda à esquerda,
150
+ * "Fechar" à direita).
151
+ */
152
+ export function PageVideoHero({
153
+ video,
154
+ topLeft,
155
+ caption,
156
+ panel,
157
+ actions,
158
+ watch,
159
+ closeLabel = "Fechar o vídeo",
160
+ previewLabels,
161
+ videoTitle,
162
+ }: Props) {
163
+ const isPlayer = video.mode === "player";
164
+ const [playing, setPlaying] = useState(false);
165
+ const watchRef = useRef<HTMLButtonElement>(null);
166
+ const playedOnce = useRef(false);
167
+
168
+ // Ao fechar, o foco volta para o botão de assistir.
169
+ useEffect(() => {
170
+ if (playing) playedOnce.current = true;
171
+ else if (playedOnce.current) watchRef.current?.focus();
172
+ }, [playing]);
173
+
174
+ const legenda = isPlayer ? (video as VideoPlayer).legenda : undefined;
175
+
176
+ return (
177
+ <div className="px-4 pt-2 md:px-6 md:pt-6">
178
+ <section className="relative isolate mx-auto flex min-h-[min(calc(100svh-7.5rem),560px)] max-w-[1600px] flex-col justify-end overflow-hidden rounded-[32px] bg-ink text-surface lg:min-h-[min(calc(100svh-7.5rem),820px)]">
179
+ {/* mídia — fundo (prévia) ou, tocando, o vídeo inteiro com som */}
180
+ <div className={`absolute inset-x-0 top-0 ${playing ? "bottom-[4.5rem]" : "bottom-0 -z-10"}`}>
181
+ {playing ? (
182
+ <iframe
183
+ key="cg-hero-som"
184
+ title={videoTitle ?? "Vídeo"}
185
+ src={`https://www.youtube-nocookie.com/embed/${(video as VideoPlayer).youtubeId}?autoplay=1&mute=0&start=0&rel=0&modestbranding=1&playsinline=1&iv_load_policy=3`}
186
+ allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; fullscreen"
187
+ allowFullScreen
188
+ className="absolute inset-0 h-full w-full border-0 bg-black"
189
+ />
190
+ ) : (
191
+ <>
192
+ {isPlayer ? (
193
+ <YouTubeBackground
194
+ id={(video as VideoPlayer).youtubeId}
195
+ poster={(video as VideoPlayer).poster}
196
+ title={videoTitle}
197
+ labels={previewLabels}
198
+ />
199
+ ) : (
200
+ <BackgroundVideo
201
+ key={(video as VideoLoop).preview.src}
202
+ src={(video as VideoLoop).preview.src}
203
+ srcLarge={(video as VideoLoop).preview.srcLarge}
204
+ poster={(video as VideoLoop).preview.poster}
205
+ posterLarge={(video as VideoLoop).preview.posterLarge}
206
+ trimEdges={(video as VideoLoop).preview.trimEdges}
207
+ objectPosition={(video as VideoLoop).preview.objectPosition}
208
+ className="absolute inset-0"
209
+ />
210
+ )}
211
+ {/* véus — legibilidade do texto (base) e dos elementos do topo */}
212
+ <div aria-hidden="true" className="pointer-events-none absolute inset-0 bg-gradient-to-t from-ink/70 via-ink/10 to-transparent" />
213
+ <div aria-hidden="true" className="pointer-events-none absolute inset-x-0 top-0 h-28 bg-gradient-to-b from-ink/35 to-transparent" />
214
+ </>
215
+ )}
216
+ </div>
217
+
218
+ {!playing && (
219
+ <>
220
+ {/* topo-esquerdo */}
221
+ {topLeft && <div className="absolute top-4 left-4 z-10">{topLeft}</div>}
222
+
223
+ {/* painel / legenda — no fluxo (justify-end), m-6 */}
224
+ <div className="relative z-10 p-6 md:m-6 md:p-0">
225
+ {panel ? (
226
+ <div className="inline-flex max-w-full items-center rounded-[24px] bg-ink/75 px-6 py-4 backdrop-blur-md md:max-w-[calc(100%-16rem)] lg:px-7 lg:py-5">
227
+ <div>
228
+ {panel.kicker && (
229
+ <p className="mb-3 font-heading text-[0.82rem] font-bold tracking-[0.1em] text-surface/80 uppercase">{panel.kicker}</p>
230
+ )}
231
+ <h1 className="font-heading text-[clamp(2.1rem,4.6vw,3.6rem)] leading-[1.04] font-semibold tracking-[-0.015em] text-balance text-surface">{panel.title}</h1>
232
+ {panel.lead && (
233
+ <p className="mt-4 max-w-[46ch] text-base leading-[1.55] text-pretty text-surface/85 md:text-lg">{panel.lead}</p>
234
+ )}
235
+ </div>
236
+ </div>
237
+ ) : caption ? (
238
+ <div className="inline-block rounded-[22px] bg-ink/70 px-5 py-3 font-heading text-[1.05rem] font-medium text-surface backdrop-blur-md md:text-xl">{caption}</div>
239
+ ) : null}
240
+ </div>
241
+
242
+ {/* ação inferior-direita */}
243
+ {actions === null ? null : actions ? (
244
+ <div className="absolute right-4 bottom-4 z-10 flex flex-wrap items-center justify-end gap-2 md:right-6 md:bottom-6">{actions}</div>
245
+ ) : isPlayer ? (
246
+ <button
247
+ ref={watchRef}
248
+ type="button"
249
+ onClick={() => setPlaying(true)}
250
+ className="absolute right-4 bottom-4 z-10 flex items-center gap-3 rounded-full border border-surface/15 bg-ink/80 py-2 pr-7 pl-2 font-body font-semibold text-surface backdrop-blur-md transition-colors duration-200 hover:bg-ink/90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface motion-reduce:transition-none md:right-6 md:bottom-6"
251
+ >
252
+ <span className="relative flex h-12 w-12 items-center justify-center rounded-full bg-surface text-ink">
253
+ <PlayGlyph size={20} />
254
+ </span>
255
+ <span className="flex flex-col items-start leading-tight">
256
+ {watch?.label ?? "Assistir ao vídeo"}
257
+ {watch?.meta && <span className="text-sm font-normal text-surface/80">{watch.meta}</span>}
258
+ </span>
259
+ </button>
260
+ ) : null}
261
+ </>
262
+ )}
263
+
264
+ {/* barra de fechar — pé do vídeo tocando (legenda à esquerda, fechar à direita) */}
265
+ {playing && (
266
+ <div className="relative z-10 flex h-[4.5rem] items-center justify-between gap-6 bg-ink px-6">
267
+ <p className="min-w-0 truncate text-sm text-surface/80">{legenda}</p>
268
+ <button
269
+ type="button"
270
+ onClick={() => setPlaying(false)}
271
+ className="inline-flex min-h-11 shrink-0 items-center gap-2 rounded-full border border-surface/25 px-5 text-sm font-semibold text-surface transition-colors duration-200 hover:bg-surface/10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface motion-reduce:transition-none"
272
+ >
273
+ <XGlyph size={18} />
274
+ {closeLabel}
275
+ </button>
276
+ </div>
277
+ )}
278
+ </section>
279
+ </div>
280
+ );
281
+ }
@@ -0,0 +1,79 @@
1
+ import type { ReactNode } from "react";
2
+ import { Button } from "./Button";
3
+
4
+ export type Pill = { label: ReactNode; href?: string };
5
+
6
+ type Props = {
7
+ title?: ReactNode;
8
+ items: Pill[];
9
+ /** Link "Ver todos" opcional. */
10
+ seeAll?: { label: ReactNode; href?: string };
11
+ };
12
+
13
+ function ArrowUpRight() {
14
+ return (
15
+ <svg
16
+ aria-hidden="true"
17
+ viewBox="0 0 24 24"
18
+ className="h-4 w-4 flex-none"
19
+ fill="none"
20
+ stroke="currentColor"
21
+ strokeWidth="2.25"
22
+ strokeLinecap="round"
23
+ strokeLinejoin="round"
24
+ >
25
+ <path d="M7 17 17 7M9 7h8v8" />
26
+ </svg>
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Lista de pills (ex.: "Condições que atendemos"): chips brancos com rótulo +
32
+ * seta, em grade que quebra, e um link "Ver todos". Chip clicável vira `<a>`.
33
+ * Estilo de card discreto do DS (borda + sombra suaves), raio pill.
34
+ */
35
+ export function PillList({ title, items, seeAll }: Props) {
36
+ return (
37
+ <div>
38
+ {title && (
39
+ <h2 className="mb-6 font-heading text-[clamp(1.5rem,3vw,2rem)] font-semibold tracking-[-0.01em] text-ink">
40
+ {title}
41
+ </h2>
42
+ )}
43
+ <ul className="flex flex-wrap gap-2.5">
44
+ {items.map((p, i) => {
45
+ const inner = (
46
+ <>
47
+ <span>{p.label}</span>
48
+ <span className="grid h-7 w-7 flex-none place-items-center rounded-full bg-ink/[0.06] text-ink-soft transition-colors group-hover:bg-ink/[0.1] group-hover:text-ink">
49
+ <ArrowUpRight />
50
+ </span>
51
+ </>
52
+ );
53
+ const cls =
54
+ "group inline-flex items-center gap-3 rounded-pill border border-line bg-surface py-2 pr-2 pl-5 font-heading text-[0.98rem] font-semibold text-ink shadow-[0_2px_10px_rgba(20,71,83,0.05)] transition hover:-translate-y-0.5 hover:shadow-[0_10px_24px_rgba(20,71,83,0.1)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink";
55
+ return (
56
+ <li key={i}>
57
+ {p.href ? (
58
+ <a href={p.href} className={cls}>
59
+ {inner}
60
+ </a>
61
+ ) : (
62
+ <span className={cls}>{inner}</span>
63
+ )}
64
+ </li>
65
+ );
66
+ })}
67
+ </ul>
68
+ {seeAll && (
69
+ <div className="mt-6">
70
+ {seeAll.href ? (
71
+ <Button href={seeAll.href} variant="link">{seeAll.label} <ArrowUpRight /></Button>
72
+ ) : (
73
+ <Button variant="link">{seeAll.label} <ArrowUpRight /></Button>
74
+ )}
75
+ </div>
76
+ )}
77
+ </div>
78
+ );
79
+ }
@@ -0,0 +1,25 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ type Props = {
4
+ /** Numeral da seção (ex.: "01"). */
5
+ num?: string;
6
+ children: ReactNode;
7
+ };
8
+
9
+ /**
10
+ * Sobrelinha de seção numerada (ex.: "01 — TRATAMENTO"), padrão da família.
11
+ * Numeral em `action-text`, rótulo em `ink-soft` caixa-alta. Sem dependência.
12
+ */
13
+ export function SectionKicker({ num, children }: Props) {
14
+ return (
15
+ <p className="mb-[clamp(20px,3vw,40px)] flex items-center gap-3 font-heading text-[0.82rem] font-bold tracking-[0.18em] uppercase">
16
+ {num && (
17
+ <span className="text-[1.6rem] leading-none font-bold tracking-normal text-action-text">
18
+ {num}
19
+ </span>
20
+ )}
21
+ <span aria-hidden="true" className="h-px w-8 bg-line" />
22
+ <span className="text-ink-soft">{children}</span>
23
+ </p>
24
+ );
25
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Biblioteca de ícones do Breathe. Re-exporta a lib INTEIRA do lucide-react
3
+ * (centenas de ícones), para você escolher qualquer um ao montar interfaces:
4
+ * import { Heart, Wind, Baby } from "@breathe-ds/ui/components/icons";
5
+ * Os ícones usam `currentColor` e aceitam `size`/`strokeWidth`. Os componentes
6
+ * do DS recebem ícones por slot, então injete o que quiser.
7
+ */
8
+ export * from "lucide-react";
9
+
10
+ import {
11
+ House, Wind, Activity, Baby, Search, Users, GraduationCap, BookOpen, Info,
12
+ MapPin, Phone, Mail, MessageCircle, AtSign, Video, Play, Pause, Volume2,
13
+ Quote, Menu, X, ChevronDown, ArrowUpRight, Globe,
14
+ } from "lucide-react";
15
+
16
+ /* Aliases semânticos dos ícones usados pela família (os "principais"). */
17
+ export const IconHome = House;
18
+ export const IconWind = Wind;
19
+ export const IconActivity = Activity;
20
+ export const IconBaby = Baby;
21
+ export const IconSearch = Search;
22
+ export const IconUsers = Users;
23
+ export const IconGraduationCap = GraduationCap;
24
+ export const IconBook = BookOpen;
25
+ export const IconInfo = Info;
26
+ export const IconMapPin = MapPin;
27
+ export const IconPhone = Phone;
28
+ export const IconMail = Mail;
29
+ export const IconMessage = MessageCircle;
30
+ export const IconInstagram = AtSign;
31
+ export const IconVideo = Video;
32
+ export const IconPlay = Play;
33
+ export const IconPause = Pause;
34
+ export const IconVolume2 = Volume2;
35
+ export const IconQuote = Quote;
36
+ export const IconMenu = Menu;
37
+ export const IconX = X;
38
+ export const IconChevronDown = ChevronDown;
39
+ export const IconArrowUpRight = ArrowUpRight;
40
+ export const IconGlobe = Globe;
41
+
42
+ /* Mapa dos usados (principais) — nome → componente. */
43
+ export const used = {
44
+ Home: IconHome, Wind: IconWind, Activity: IconActivity, Baby: IconBaby,
45
+ Search: IconSearch, Users: IconUsers, GraduationCap: IconGraduationCap,
46
+ Book: IconBook, Info: IconInfo, MapPin: IconMapPin, Phone: IconPhone,
47
+ Mail: IconMail, Message: IconMessage, Instagram: IconInstagram,
48
+ Video: IconVideo, Play: IconPlay, Pause: IconPause, Volume2: IconVolume2,
49
+ Quote: IconQuote, Menu: IconMenu, X: IconX, ChevronDown: IconChevronDown,
50
+ ArrowUpRight: IconArrowUpRight, Globe: IconGlobe,
51
+ };
@@ -0,0 +1,68 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ /**
4
+ * Marca do Creeta (Centro de Reequilíbrio Toracoabdominal), copiada de
5
+ * public/brand/creeta-simbolo.svg e creeta-tipo.svg do site. Símbolo (três
6
+ * ondas + ponto magenta) e wordmark "creeta" separados, para a Cortina e o
7
+ * Lockup montarem os arranjos. Não editar à mão: o desenho é o do manual.
8
+ * As cores do símbolo são as do logotipo, não os tokens do site.
9
+ */
10
+ export const CREETA_COLORS = {
11
+ ondaMedia: "#3B9BAF",
12
+ arco: "#266675",
13
+ ondaClara: "#63CACD",
14
+ ponto: "#A2158A",
15
+ } as const;
16
+
17
+ type SymbolProps = {
18
+ /** Lado em px (caixa quadrada 86 × 86). */
19
+ size?: number;
20
+ /** Cor única para todas as partes (ex.: "#fff" sobre painel escuro). Padrão: as cores da marca. */
21
+ color?: string;
22
+ className?: string;
23
+ title?: string;
24
+ };
25
+
26
+ export function CreetaSymbol({ size = 48, color, className, title = "Creeta" }: SymbolProps) {
27
+ const c = (k: keyof typeof CREETA_COLORS) => color ?? CREETA_COLORS[k];
28
+ return (
29
+ <svg width={size} height={size} viewBox="0 0 86 86" fill="none" role="img" aria-label={title} className={className}>
30
+ <path d="M85.4746 46.2096C83.9959 65.2746 70.0266 80.8266 51.7546 84.696C51.6493 84.7214 51.544 84.7481 51.4507 84.761C48.7053 85.3293 45.8666 85.62 42.9613 85.6334H42.816C26.4706 84.973 13.4266 71.5053 13.4266 54.9892C13.4266 44.1626 19.0387 34.6309 27.5147 29.1908C30.7093 27.1309 34.3133 25.6397 38.196 24.9001C30.5106 26.8798 24.7547 33.6668 24.3453 41.8653C24.3187 42.1824 24.3186 42.4986 24.3186 42.8157C24.3186 43.1334 24.3187 43.449 24.3453 43.7668C24.8347 60.2574 38.3666 73.473 54.9893 73.473C61.1026 73.473 66.7919 71.6908 71.5719 68.6012C79.1506 63.7293 84.4319 55.6096 85.4746 46.2096Z" fill={c("ondaMedia")} />
31
+ <path d="M55.98 12.16V12.173C55.9667 18.8012 50.672 24.1746 44.084 24.32H43.8067C43.6747 24.32 43.5427 24.32 43.4107 24.3334H43.3587C43.1733 24.32 43.0014 24.32 42.8174 24.32C41.2854 24.32 39.7933 24.5038 38.38 24.861C38.3147 24.8746 38.2614 24.8881 38.196 24.9012C34.3147 25.6397 30.7094 27.132 27.5147 29.1918C19.0387 34.6309 13.428 44.1642 13.428 54.9892C13.428 71.5064 26.472 84.973 42.8174 85.6334C19.1707 85.6334 0 66.4626 0 42.8173C0 19.1705 19.1707 -3.8147e-05 42.8174 -3.8147e-05C43.028 -3.8147e-05 43.2387 -3.52981e-05 43.4507 0.013298C43.9 0.013298 44.3613 0.0266218 44.8107 0.0399551C51.056 0.554622 55.98 5.78262 55.98 12.16Z" fill={c("arco")} />
32
+ <path d="M73.4467 31.2278C70.6867 31.2278 68.152 32.1387 66.1187 33.684C63.188 35.9012 61.2867 39.4267 61.2867 43.3882V42.8335C61.2867 53.0387 53.0093 61.3174 42.8027 61.3174C32.9013 61.3174 24.8214 53.5534 24.332 43.784C24.8214 60.2747 38.3534 73.4908 54.976 73.4908C61.0894 73.4908 66.7787 71.708 71.5587 68.619C79.0947 63.7731 84.3547 55.7158 85.44 46.3824C85.4694 45.6944 85.6067 43.4491 85.6067 43.1892C85.5014 36.5611 80.1013 31.2278 73.4467 31.2278Z" fill={c("ondaClara")} />
33
+ <path d="M71.2799 26.5798C77.1253 26.5798 81.8626 21.8252 81.8626 15.9585C81.8626 10.0933 77.1253 5.33731 71.2799 5.33731C65.4346 5.33731 60.6973 10.0933 60.6973 15.9585C60.6973 21.8252 65.4346 26.5798 71.2799 26.5798Z" fill={c("ponto")} />
34
+ </svg>
35
+ );
36
+ }
37
+
38
+ type WordmarkProps = {
39
+ /** Altura. Padrão `var(--cg-wordmark, 1em)`: dentro do Lockup/Cortina a
40
+ * variável vem da razão do manual (`wordmarkPerSymbol`); solto, 1em. */
41
+ height?: string;
42
+ /** Padrão `currentColor`: herda `ink` (ou branco sobre painel). */
43
+ color?: string;
44
+ className?: string;
45
+ style?: CSSProperties;
46
+ title?: string;
47
+ };
48
+
49
+ /** Razão do manual: altura do wordmark ÷ altura do símbolo no logotipo
50
+ * horizontal (40.3 / 86 (nome a 1.4643× no logotipo horizontal)). Passe como `wordmarkRatio` ao Lockup/Cortina. */
51
+ /** No creeta o símbolo fica à esquerda do nome (padrão da família). */
52
+ export const CREETA_LOCKUP = { wordmarkPerSymbol: 0.469, symbolSide: "left" } as const;
53
+
54
+ /** Caixa justa ao desenho das letras: 116.66 × 27.53. */
55
+ export function CreetaWordmark({ height = "var(--cg-wordmark, 1em)", color = "currentColor", className, style, title = "creeta" }: WordmarkProps) {
56
+ return (
57
+ <svg
58
+ viewBox="0 0 116.66 27.53"
59
+ fill="none"
60
+ role="img"
61
+ aria-label={title}
62
+ className={className}
63
+ style={{ height, width: "auto", aspectRatio: "116.66 / 27.53", display: "block", ...style }}
64
+ >
65
+ <path d="M9.99069 27.532C12.3507 27.532 14.5533 26.6667 16.244 25.448C17.1493 24.66 17.3067 23.4027 16.6773 22.536C15.8907 21.5934 14.592 21.436 13.7267 22.1027C12.704 22.8507 11.4067 23.3626 9.99069 23.3626C6.80536 23.3626 4.16934 20.7667 4.16934 17.7387C4.16934 14.708 6.80536 12.0747 9.99069 12.0747C11.4454 12.0747 12.704 12.584 13.7267 13.3334C14.632 14.0414 15.8907 13.884 16.6773 12.9387C17.3067 12.0747 17.1493 10.736 16.244 10.0293C14.5533 8.72931 12.3507 7.86532 9.99069 7.86532C4.56269 7.90532 0 12.2694 0 17.7387C0 23.1267 4.56269 27.532 9.99069 27.532ZM33.8654 7.90536C31.8987 7.86536 30.208 8.57202 28.9094 9.43869C28.6347 9.63602 28.3201 9.95068 28.0841 10.2253V9.95067C28.0841 8.808 27.1001 7.90536 25.9201 7.90536C24.7801 7.90536 23.836 8.808 23.836 9.95067V25.448C23.836 26.588 24.7801 27.532 25.9201 27.532C27.1001 27.532 28.0841 26.588 28.0841 25.448V17.9747C28.2801 17.424 28.752 15.9294 29.696 14.6694C30.168 13.9227 30.7587 13.2547 31.5053 12.7813C32.1733 12.2693 32.96 12.0347 33.8654 12.0347C35.0454 12.0347 35.9494 11.0507 35.9494 9.95067C35.9494 8.808 35.0454 7.90536 33.8654 7.90536ZM47.3561 27.532C50.1494 27.532 52.4693 26.7054 54.3973 24.936C55.3013 24.1107 54.908 22.9294 54.3973 22.38C53.8853 21.7507 52.2733 21.632 51.4467 22.496C50.464 23.2453 48.7721 23.5987 47.3561 23.4813C45.9801 23.3627 44.4067 22.6547 43.58 21.7107C42.872 21.0414 42.44 20.02 42.2427 19.1147H54.7507C55.8521 19.1147 56.6787 18.484 56.796 17.5414C56.836 17.4627 56.836 17.2666 56.836 17.188V16.9906C56.5213 11.6813 52.4694 7.86532 47.3561 7.86532C41.9681 7.86532 37.7987 12.348 37.76 17.6986C37.7987 23.048 41.9681 27.492 47.3561 27.532ZM47.3561 11.6027C50.7787 11.8774 52.784 14.1987 53.1773 15.772H42.0467C42.3613 14.1587 44.0134 11.76 47.3561 11.6027ZM71.3094 27.532C74.1027 27.532 76.4227 26.7054 78.3507 24.936C79.2547 24.1107 78.8614 22.9294 78.3507 22.38C77.8387 21.7507 76.2267 21.632 75.4 22.496C74.4173 23.2453 72.7254 23.5987 71.3094 23.4813C69.9334 23.3627 68.36 22.6547 67.5333 21.7107C66.8253 21.0414 66.3934 20.02 66.196 19.1147H78.704C79.8054 19.1147 80.6307 18.484 80.7494 17.5414C80.788 17.4627 80.788 17.2666 80.788 17.188V16.9906C80.4734 11.6813 76.4227 7.86532 71.3094 7.86532C65.9214 7.86532 61.752 12.348 61.712 17.6986C61.752 23.048 65.9214 27.492 71.3094 27.532ZM71.3094 11.6027C74.732 11.8774 76.7373 14.1987 77.1307 15.772H66C66.3147 14.1587 67.9667 11.76 71.3094 11.6027ZM92.116 7.86532H91.1333V2.16265C91.1333 0.943981 90.1893 1.79171e-05 88.9693 1.79171e-05C87.7507 1.79171e-05 86.7667 0.943981 86.7667 2.16265V7.86532H85.6267C84.4067 7.86532 83.4627 8.80798 83.4627 10.0293C83.4627 11.208 84.4067 12.192 85.6267 12.192H86.7667V23.1667C86.7667 25.5653 88.7333 27.532 91.1333 27.532C92.352 27.532 93.296 26.588 93.296 25.3694C93.296 24.148 92.352 23.1667 91.1333 23.1667V12.192H92.116C93.296 12.192 94.3187 11.208 94.3187 10.0293C94.3187 8.80798 93.296 7.86532 92.116 7.86532ZM116.66 9.98934C116.66 8.80801 115.716 7.86532 114.575 7.86532C113.552 7.86532 112.765 8.61331 112.529 9.59598C111.035 8.53465 109.265 7.86532 107.299 7.86532C104.624 7.86532 102.303 9.00533 100.612 10.8147C98.92 12.584 97.9374 15.024 97.9374 17.7387C97.9374 20.4134 98.92 22.8506 100.612 24.66C102.303 26.4293 104.624 27.532 107.299 27.532C109.265 27.532 111.035 26.9027 112.529 25.8414C112.765 26.784 113.552 27.532 114.575 27.532C115.716 27.532 116.66 26.588 116.66 25.448V17.7387V9.98934ZM110.956 21.7893C110.012 22.772 108.675 23.324 107.299 23.324C105.883 23.324 104.545 22.772 103.64 21.7893C102.696 20.7667 102.145 19.3507 102.145 17.7387C102.145 16.0867 102.696 14.6693 103.64 13.6867C104.545 12.6253 105.883 12.0747 107.299 12.0747C108.675 12.0747 110.012 12.6253 110.956 13.6867C111.9 14.6693 112.491 16.0867 112.491 17.7387C112.491 19.3507 111.9 20.7667 110.956 21.7893Z" fill={color} />
66
+ </svg>
67
+ );
68
+ }
@@ -0,0 +1,64 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ /**
4
+ * Marca do TTCAB (Torcicolo Congênito e Assimetrias do Bebê), copiada dos
5
+ * arquivos de public/brand do site. Símbolo (mãe + bebê, laranja) e
6
+ * wordmark "TTCAB" separados, para a Cortina e o Lockup montarem os arranjos.
7
+ * Não editar à mão: o desenho é o do manual da marca.
8
+ */
9
+ type SymbolProps = {
10
+ /** Largura em px; a altura segue a proporção (136:126). */
11
+ size?: number;
12
+ /** Cor única (ex.: "currentColor" ou "#fff" sobre painel escuro). Padrão: o laranja da marca. */
13
+ color?: string;
14
+ className?: string;
15
+ title?: string;
16
+ };
17
+
18
+ export const TTCAB_ORANGE = "#F6A22A";
19
+
20
+ export function TtcabSymbol({ size = 48, color = TTCAB_ORANGE, className, title = "TTCAB" }: SymbolProps) {
21
+ return (
22
+ <svg width={size} height={Math.round((size * 126) / 136)} viewBox="0 0 136 126" fill="none" role="img" aria-label={title} className={className}>
23
+ <path d="M56.1003 59.9408C49.923 62.9355 42.4856 60.3555 39.4923 54.1782C36.5003 47.9995 39.0816 40.5648 45.2603 37.5688C51.439 34.5755 58.8736 37.1541 61.867 43.3328C64.8616 49.5101 62.2776 56.9475 56.1003 59.9408Z" fill={color} />
24
+ <path d="M130.154 82.0699C124.045 92.3605 114.702 100.754 103.696 105.482C88.4651 112.023 70.3385 111.422 55.5745 103.886C50.1491 101.117 46.2331 96.8285 43.5918 91.3392C41.2118 93.7205 43.0038 101.131 44.0011 104.263C39.7825 100.318 39.7931 94.0712 40.1371 90.0405C40.4811 86.0072 41.3998 81.9432 40.7158 77.9552C39.0878 68.4659 28.9491 62.0272 27.7838 52.4699C26.8545 44.8499 31.9678 37.6352 31.8651 29.9592C31.8198 26.5925 29.6825 22.5139 26.3465 22.9605C24.6518 23.1885 23.3465 24.5565 22.3758 25.9645C15.0945 36.5219 19.1585 51.5165 13.4865 63.0192C15.2545 51.0005 12.1105 37.7979 18.0851 27.2205C20.3718 23.1739 23.8091 19.9272 26.7265 16.3085C29.6425 12.6912 32.1438 8.36319 31.9718 3.71919C31.9211 2.34719 31.4891 0.788528 30.2425 0.21386C28.7358 -0.480804 27.0531 0.645851 25.8118 1.74586C1.63448 23.1725 -7.66685 59.0672 7.05715 88.3592C11.8145 97.8232 18.6931 106.247 27.2065 112.574C45.8771 126.449 71.8345 129.854 93.4505 121.266C115.068 112.678 131.612 92.3872 135.67 69.4832C134.366 73.8925 132.498 78.1205 130.154 82.0699Z" fill={color} />
25
+ <path d="M79.5654 101.119C81.2107 101.379 82.8641 101.567 84.5214 101.618C102.456 102.132 117.573 89.6429 125.523 74.5256C120.697 78.7883 114.387 80.5456 108.573 82.1603C96.8347 85.4216 85.3561 79.3149 79.9441 68.1803L80.2374 68.7856C78.1374 64.4643 74.2254 61.6056 69.8974 60.7083C66.8934 60.0789 63.6841 60.4016 60.7187 61.8349C53.5654 65.2923 50.5894 73.9176 53.9894 81.2029C54.1281 81.4856 55.4654 83.8643 55.8627 84.5989C61.3974 93.3069 70.0601 99.0829 79.5654 101.119Z" fill={color} />
26
+ </svg>
27
+ );
28
+ }
29
+
30
+ type WordmarkProps = {
31
+ /** Altura. Padrão `var(--cg-wordmark, 1em)`: dentro do Lockup/Cortina a
32
+ * variável vem da razão do manual (`wordmarkPerSymbol`); solto, 1em. */
33
+ height?: string;
34
+ /** Padrão `currentColor`: herda `ink` (ou branco sobre painel). */
35
+ color?: string;
36
+ className?: string;
37
+ style?: CSSProperties;
38
+ title?: string;
39
+ };
40
+
41
+ /** Razão do manual: altura do wordmark ÷ altura do símbolo no logotipo
42
+ * horizontal (43.67 / 78.39). Passe como `wordmarkRatio` ao Lockup/Cortina. */
43
+ /** No TTCAB o símbolo fica à DIREITA do nome (logotipo horizontal do manual). */
44
+ export const TTCAB_LOCKUP = { wordmarkPerSymbol: 0.557, symbolSide: "right" } as const;
45
+
46
+ /** Caixa justa ao desenho das letras: 149.75 × 43.67. */
47
+ export function TtcabWordmark({ height = "var(--cg-wordmark, 1em)", color = "currentColor", className, style, title = "TTCAB" }: WordmarkProps) {
48
+ return (
49
+ <svg
50
+ viewBox="0 21.36 149.75 43.67"
51
+ fill="none"
52
+ role="img"
53
+ aria-label={title}
54
+ className={className}
55
+ style={{ height, width: "auto", aspectRatio: "149.75 / 43.67", display: "block", ...style }}
56
+ >
57
+ <path d="M11.8373 64.2444C10.0853 64.2444 8.69733 62.8551 8.69733 61.1044V31.0871H3.14C1.388 31.0871 0 29.6991 0 27.9471V25.2897C0 23.5377 1.388 22.1497 3.14 22.1497H24.6413C26.3933 22.1497 27.7813 23.5377 27.7813 25.2897V27.9471C27.7813 29.6991 26.3933 31.0871 24.6413 31.0871H19.084V61.1044C19.084 62.8551 17.696 64.2444 15.944 64.2444H11.8373Z" fill={color} />
58
+ <path d="M40.9432 64.2444C39.1912 64.2444 37.8032 62.8551 37.8032 61.1044V31.0871H32.2459C30.4939 31.0871 29.1059 29.6991 29.1059 27.9471V25.2897C29.1059 23.5377 30.4939 22.1497 32.2459 22.1497H53.7472C55.4979 22.1497 56.8872 23.5377 56.8872 25.2897V27.9471C56.8872 29.6991 55.4979 31.0871 53.7472 31.0871H48.1899V61.1044C48.1899 62.8551 46.8006 64.2444 45.0499 64.2444H40.9432Z" fill={color} />
59
+ <path d="M75.5859 65.0297C63.9299 65.0297 58.3739 58.9297 58.3739 43.227C58.3739 25.7723 65.0166 21.363 75.3446 21.363C78.7259 21.363 81.6259 21.967 83.8606 22.9337C85.3712 23.5977 86.0952 25.471 85.3099 27.0403L84.1019 29.4563C83.3779 31.027 81.5046 31.7523 79.8739 31.027C78.7259 30.543 77.3979 30.2417 76.0086 30.2417C71.1166 30.2417 70.0299 31.8123 70.0299 42.5617C70.0299 54.339 72.2046 56.091 76.0086 56.091C77.6406 56.091 79.0286 55.7283 80.2366 55.1243C81.8072 54.339 83.6792 54.883 84.5846 56.3937L85.7926 58.4457C85.9739 58.6883 86.2166 59.4723 86.2166 60.0163C86.2166 61.1043 85.6726 62.191 84.7059 62.795C82.5312 64.123 79.6926 65.0297 75.5859 65.0297Z" fill={color} />
60
+ <path d="M123.659 64.2444C121.907 64.2444 120.519 62.8551 120.519 61.1044V25.2897C120.519 23.5377 121.907 22.1497 123.659 22.1497H137.793C143.59 22.1497 148.301 24.8657 148.301 32.7177C148.301 37.3684 147.033 40.087 142.745 41.7777C146.61 42.5617 149.75 45.6431 149.75 51.6217C149.75 60.2591 146.066 64.2444 136.645 64.2444H123.659ZM130.786 38.5164H134.229C136.705 38.5164 137.793 37.0057 137.793 34.4097C137.793 31.8724 136.523 30.7257 134.169 30.7257H130.786V38.5164ZM130.786 55.6684H134.591C137.127 55.6684 138.397 54.2791 138.397 51.0777C138.397 48.4817 137.611 46.7297 134.47 46.7297H130.786V55.6684Z" fill={color} />
61
+ <path d="M118.945 61.2244L109.644 25.1684C109.161 23.4164 107.531 22.1484 105.719 22.1484H101.309C99.4987 22.1484 97.8067 23.4164 97.3241 25.1684L87.4187 61.2244C86.9961 62.7337 88.1427 64.2444 89.7134 64.2444H94.1827C95.9947 64.2444 97.5654 63.0364 97.9881 61.1644L98.8934 57.2391L102.156 42.5017C102.759 39.9057 103.061 36.7644 103.181 35.1937H103.243C103.303 36.7644 103.604 39.9057 104.148 42.5017L107.168 57.2391L107.953 61.1644C108.315 62.8551 109.947 64.2444 111.759 64.2444H116.589C118.16 64.2444 119.368 62.7337 118.945 61.2244Z" fill={color} />
62
+ </svg>
63
+ );
64
+ }
package/package.json CHANGED
@@ -1,6 +1,49 @@
1
1
  {
2
2
  "name": "@breathe-ds/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Breathe DS \u2014 design system da fam\u00edlia Cuidado Global (creeta, ttcab, metodorta). Tokens tematiz\u00e1veis + componentes React.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "sideEffects": [
8
+ "*.css"
9
+ ],
10
+ "files": [
11
+ "theme.css",
12
+ "tokens",
13
+ "components",
14
+ "README.md"
15
+ ],
16
+ "exports": {
17
+ "./theme.css": "./theme.css",
18
+ "./tokens/ttcab.css": "./tokens/ttcab.css",
19
+ "./tokens/creeta.css": "./tokens/creeta.css",
20
+ "./components/Accordion": "./components/Accordion.tsx",
21
+ "./components/PageVideoHero": "./components/PageVideoHero.tsx",
22
+ "./components/Footer": "./components/Footer.tsx",
23
+ "./components/Button": "./components/Button.tsx",
24
+ "./components/Container": "./components/Container.tsx",
25
+ "./components/SectionKicker": "./components/SectionKicker.tsx",
26
+ "./components/Card": "./components/Card.tsx",
27
+ "./components/PillList": "./components/PillList.tsx",
28
+ "./components/Header": "./components/Header.tsx",
29
+ "./components/BottomNav": "./components/BottomNav.tsx",
30
+ "./components/BreatheMark": "./components/BreatheMark.tsx",
31
+ "./components/Cortina": "./components/Cortina.tsx",
32
+ "./components/Lockup": "./components/Lockup.tsx",
33
+ "./tokens/neutral.css": "./tokens/neutral.css",
34
+ "./components/icons": "./components/icons.tsx",
35
+ "./components/AnchorNav": "./components/AnchorNav.tsx",
36
+ "./components/marks/ttcab": "./components/marks/ttcab.tsx",
37
+ "./components/marks/creeta": "./components/marks/creeta.tsx"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=18",
44
+ "react-dom": ">=18"
45
+ },
46
+ "dependencies": {
47
+ "lucide-react": "^1.63.0"
48
+ }
49
+ }