@breathe-ds/ui 0.3.0 → 0.5.1

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,94 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from "react";
4
+
5
+ type RailProps = {
6
+ /** Name of the list for screen readers: "Treatments", "Team". */
7
+ label: string;
8
+ /** Grid classes from md up, written in full (Tailwind only generates whole
9
+ * classes). Default: three columns. */
10
+ grid?: string;
11
+ /** <li> children, usually RailItem. */
12
+ children: ReactNode;
13
+ className?: string;
14
+ };
15
+
16
+ /**
17
+ * Rail — novo.creeta's `Trilho` (components/trilho.tsx) with the Breathe
18
+ * tokens. On a phone it is a horizontal rail with snap, and the next card
19
+ * shows by half to say there is more. From md it becomes a grid.
20
+ *
21
+ * Under the rail, a small bar follows the finger: it starts the size of what
22
+ * is already in view and fills to the end — it says how much is left. Shown
23
+ * only when the rail really scrolls, decoration for screen readers. The fill
24
+ * is written straight to the style, without re-rendering on every scroll frame.
25
+ */
26
+ export function Rail({ label, grid = "md:grid-cols-3", children, className = "" }: RailProps) {
27
+ const list = useRef<HTMLUListElement>(null);
28
+ const bar = useRef<HTMLSpanElement>(null);
29
+ const [scrolls, setScrolls] = useState(false);
30
+
31
+ useEffect(() => {
32
+ const ul = list.current;
33
+ if (!ul) return;
34
+ let frame = 0;
35
+ const measure = () => {
36
+ frame = 0;
37
+ const overflow = ul.scrollWidth - ul.clientWidth;
38
+ setScrolls(overflow > 8);
39
+ if (bar.current && ul.scrollWidth > 0) {
40
+ const seen = Math.min(1, (ul.scrollLeft + ul.clientWidth) / ul.scrollWidth);
41
+ bar.current.style.transform = `scaleX(${seen})`;
42
+ }
43
+ };
44
+ const ask = () => {
45
+ if (!frame) frame = requestAnimationFrame(measure);
46
+ };
47
+ measure();
48
+ ul.addEventListener("scroll", ask, { passive: true });
49
+ window.addEventListener("resize", ask);
50
+ return () => {
51
+ ul.removeEventListener("scroll", ask);
52
+ window.removeEventListener("resize", ask);
53
+ if (frame) cancelAnimationFrame(frame);
54
+ };
55
+ }, []);
56
+
57
+ return (
58
+ <>
59
+ <ul
60
+ ref={list}
61
+ aria-label={label}
62
+ className={`cg-no-scrollbar -mx-4 flex snap-x snap-mandatory scroll-px-4 gap-3 overflow-x-auto px-4 pb-1 md:mx-0 md:grid md:snap-none md:gap-5 md:overflow-visible md:px-0 ${grid} ${className}`}
63
+ >
64
+ {children}
65
+ </ul>
66
+ <div
67
+ aria-hidden="true"
68
+ data-cg-rail-progress=""
69
+ className={`mx-auto mt-4 h-1 w-14 overflow-hidden rounded-full bg-ink/15 md:hidden ${scrolls ? "" : "hidden"}`}
70
+ >
71
+ <span ref={bar} className="block h-full w-full origin-left rounded-full bg-ink-soft" />
72
+ </div>
73
+ </>
74
+ );
75
+ }
76
+
77
+ /** Widths of one item on the rail (phone / sm); from md the grid decides.
78
+ * The next card shows by half: that is what says "there is more". */
79
+ export const RAIL_ITEM = {
80
+ /** Treatments, videos: one card almost full width. */
81
+ wide: "w-[78%] shrink-0 snap-start sm:w-[46%] md:w-auto",
82
+ /** Team (medium ProfessionalCard): two cards and a bit. */
83
+ narrow: "w-[46%] shrink-0 snap-start sm:w-[30%] md:w-auto",
84
+ } as const;
85
+
86
+ type RailItemProps = ComponentPropsWithoutRef<"li"> & {
87
+ /** `wide` (default) or `narrow`. */
88
+ width?: keyof typeof RAIL_ITEM;
89
+ };
90
+
91
+ /** One <li> of the Rail, with the phone width preset. */
92
+ export function RailItem({ width = "wide", className = "", ...rest }: RailItemProps) {
93
+ return <li className={`${RAIL_ITEM[width]} ${className}`} {...rest} />;
94
+ }
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { Check } from "lucide-react";
5
+
6
+ export type TicketOption = {
7
+ id: string;
8
+ label: ReactNode;
9
+ /** "6 left", as a small pill after the label. */
10
+ remaining?: ReactNode;
11
+ /** "Free", "R$ 1.900". */
12
+ price: ReactNode;
13
+ disabled?: boolean;
14
+ };
15
+
16
+ type Props = {
17
+ /** The strip at the top ("Registration"). */
18
+ title: ReactNode;
19
+ /** The sentence above the options ("Hi! Choose your ticket:"). */
20
+ intro?: ReactNode;
21
+ options: TicketOption[];
22
+ /** Selected option id (controlled); without it, the first enabled one. */
23
+ value?: string;
24
+ onChange?: (id: string) => void;
25
+ /** The button at the foot (a Button `accent`, or `solid`); receives nothing: read the selection through onChange. */
26
+ action: ReactNode;
27
+ /** Small line under the button ("Only physiotherapists can attend."). */
28
+ footnote?: ReactNode;
29
+ /** Name of the radio group, for the form. */
30
+ name?: string;
31
+ className?: string;
32
+ };
33
+
34
+ /**
35
+ * Registration box of an event (Luma): a strip with the title, a sentence,
36
+ * the ticket options as a radio group (the chosen one gets the ring and the
37
+ * check), the price on the right, and the one button at the foot. By tone:
38
+ * the box is a surface card inside the page's card.
39
+ */
40
+ export function RegistrationBox({ title, intro, options, value, onChange, action, footnote, name = "ticket", className = "" }: Props) {
41
+ const first = options.find((o) => !o.disabled)?.id;
42
+ const [inner, setInner] = useState(first);
43
+ const selected = value ?? inner;
44
+ const choose = (id: string) => {
45
+ setInner(id);
46
+ onChange?.(id);
47
+ };
48
+ return (
49
+ <section className={`overflow-hidden rounded-[24px] bg-surface ${className}`}>
50
+ <h2 className="border-b border-ink/10 px-5 py-3 text-sm font-semibold text-ink-soft">{title}</h2>
51
+ <div className="p-5">
52
+ {intro && <p className="mb-4 text-ink">{intro}</p>}
53
+ <fieldset className="m-0 border-0 p-0">
54
+ <legend className="sr-only">{title}</legend>
55
+ <div className="flex flex-col gap-2">
56
+ {options.map((o) => {
57
+ const on = o.id === selected;
58
+ return (
59
+ <label
60
+ key={o.id}
61
+ className={`flex min-h-12 cursor-pointer items-center gap-3 rounded-2xl px-4 py-2.5 transition-colors duration-150 motion-reduce:transition-none ${on ? "bg-card shadow-[inset_0_0_0_2px_var(--color-ink)]" : "bg-card/60 hover:bg-card"} ${o.disabled ? "cursor-not-allowed opacity-50" : ""}`}
62
+ >
63
+ <input type="radio" name={name} value={o.id} checked={on} disabled={o.disabled} onChange={() => choose(o.id)} className="sr-only" />
64
+ <span aria-hidden="true" className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full ${on ? "bg-ink text-surface" : "border-2 border-ink/25"}`}>
65
+ {on && <Check size={12} strokeWidth={3} />}
66
+ </span>
67
+ <span className="flex min-w-0 flex-1 flex-wrap items-center gap-2 font-semibold text-ink">
68
+ {o.label}
69
+ {o.remaining && <span className="rounded-pill bg-ink/[0.08] px-2 py-0.5 text-xs font-semibold text-ink-soft">{o.remaining}</span>}
70
+ </span>
71
+ <span className="shrink-0 font-semibold text-ink">{o.price}</span>
72
+ </label>
73
+ );
74
+ })}
75
+ </div>
76
+ </fieldset>
77
+ <div className="mt-4 [&>*]:w-full [&>*]:justify-center">{action}</div>
78
+ {footnote && <p className="mt-3 text-sm text-ink-soft">{footnote}</p>}
79
+ </div>
80
+ </section>
81
+ );
82
+ }
@@ -18,8 +18,14 @@ import { ArrowDown, ArrowUpRight, ChevronDown } from "lucide-react";
18
18
  export const ROW =
19
19
  "flex min-h-12 cursor-pointer list-none items-center justify-between gap-3 text-sm font-semibold text-ink focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink [&::-webkit-details-marker]:hidden";
20
20
 
21
+ /** Hover tells the two rows apart (2026-10-29): a link row underlines its label
22
+ * (the family's sign for "goes somewhere", as the Footer and the Byline) and
23
+ * its arrow takes the step; a collapse row never underlines — it goes nowhere —
24
+ * its chevron dips 2px, "opens down here", and turns up when open. */
25
+ const ROW_LINK = `${ROW} underline decoration-transparent underline-offset-4 transition-[text-decoration-color] duration-150 hover:decoration-ink motion-reduce:transition-none`;
26
+
21
27
  const OPEN_CHEVRON =
22
- "shrink-0 text-ink-soft transition-transform duration-200 group-open:rotate-180 motion-reduce:transition-none";
28
+ "shrink-0 text-ink-soft transition-[transform,translate] duration-200 group-hover/row:translate-y-0.5 group-open/row:rotate-180 group-open/row:group-hover/row:translate-y-0 motion-reduce:transition-none";
23
29
 
24
30
  /** What appears out of nowhere (the content of a <details>) fades in briefly
25
31
  * instead of jumping on screen: `@starting-style` via Tailwind's `starting:`
@@ -72,7 +78,7 @@ export function CollapseRow({ label, srLabel, count, links, linkComponent, child
72
78
  const L: ElementType = linkComponent ?? "a";
73
79
  const n = count ?? links?.length;
74
80
  return (
75
- <details className={`group ${className}`} {...rest}>
81
+ <details className={`group/row ${className}`} {...rest}>
76
82
  <summary className={ROW}>
77
83
  <span>
78
84
  {label}
@@ -120,7 +126,7 @@ export type LinkRowProps = Omit<ComponentPropsWithoutRef<"a">, "children"> & {
120
126
  export function LinkRow({ label, href, srLabel, arrow, external = false, icon, linkComponent, className = "", ...rest }: LinkRowProps) {
121
127
  const L: ElementType = linkComponent ?? "a";
122
128
  return (
123
- <L href={href} className={`${ROW} ${className}`} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})} {...rest}>
129
+ <L href={href} className={`${ROW_LINK} ${className}`} {...(external ? { target: "_blank", rel: "noopener noreferrer" } : {})} {...rest}>
124
130
  <span>
125
131
  {label}
126
132
  {srLabel && <span className="sr-only">{srLabel}</span>}
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import { useEffect, useId, useRef, type ReactNode } from "react";
4
+ import { X } from "lucide-react";
5
+
6
+ type Labels = {
7
+ close: string;
8
+ };
9
+ const EN: Labels = { close: "Close" };
10
+
11
+ type Props = {
12
+ open: boolean;
13
+ /** Called when the person closes it (Esc, the backdrop, the close button). */
14
+ onClose: () => void;
15
+ /** Accessible name of the panel (sr-only unless you render it in `children`). */
16
+ title: string;
17
+ /** Small actions on the top bar, beside the close button ("Copy link", "Event page ↗"). */
18
+ actions?: ReactNode;
19
+ children: ReactNode;
20
+ labels?: Partial<Labels>;
21
+ className?: string;
22
+ };
23
+
24
+ /**
25
+ * Side panel of the Cuidado Global family (Luma's event drawer): a native
26
+ * <dialog> that slides in from the right, full height, up to 32rem wide (full
27
+ * width on the phone), with a top bar (close on the left, actions on the
28
+ * right) and a scrolling body. Focus is trapped by the dialog, Esc and the
29
+ * backdrop close it, focus goes back to what opened it. The content is
30
+ * whatever you pass: a course, an event, a person.
31
+ */
32
+ export function SidePanel({ open, onClose, title, actions, children, labels, className = "" }: Props) {
33
+ const t = { ...EN, ...labels };
34
+ const box = useRef<HTMLDialogElement>(null);
35
+ const headingId = useId();
36
+
37
+ // open/close follow the prop; the way out waits for the slide to finish
38
+ useEffect(() => {
39
+ const d = box.current;
40
+ if (!d) return;
41
+ if (open) {
42
+ if (!d.open) d.showModal();
43
+ // paint the closed state first (the dialog was display:none), then open:
44
+ // a forced reflow plus a frame, or the slide would not play
45
+ void d.getBoundingClientRect();
46
+ const frame = requestAnimationFrame(() => d.setAttribute("data-open", ""));
47
+ return () => cancelAnimationFrame(frame);
48
+ }
49
+ if (!d.open) return;
50
+ d.removeAttribute("data-open");
51
+ const wait = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0 : 260;
52
+ const timer = window.setTimeout(() => d.close(), wait);
53
+ return () => window.clearTimeout(timer);
54
+ }, [open]);
55
+
56
+ // Esc (cancel) and click on the backdrop → the parent closes it
57
+ useEffect(() => {
58
+ const d = box.current;
59
+ if (!d) return;
60
+ const onCancel = (e: Event) => {
61
+ e.preventDefault();
62
+ onClose();
63
+ };
64
+ const onClick = (e: MouseEvent) => {
65
+ if (e.target === d) onClose();
66
+ };
67
+ d.addEventListener("cancel", onCancel);
68
+ d.addEventListener("click", onClick);
69
+ return () => {
70
+ d.removeEventListener("cancel", onCancel);
71
+ d.removeEventListener("click", onClick);
72
+ };
73
+ }, [onClose]);
74
+
75
+ return (
76
+ <dialog
77
+ ref={box}
78
+ aria-labelledby={headingId}
79
+ className={`fixed inset-y-0 right-0 left-auto m-0 flex h-dvh max-h-none w-[min(100vw,32rem)] max-w-none translate-x-full flex-col overflow-hidden bg-card p-0 text-ink shadow-2xl [transition:translate_280ms_var(--ease-cg)] data-open:translate-x-0 motion-reduce:transition-none backdrop:bg-ink/50 backdrop:backdrop-blur-sm backdrop:opacity-0 backdrop:[transition:opacity_240ms_ease-out] [&[data-open]::backdrop]:opacity-100 ${className}`}
80
+ >
81
+ <h2 id={headingId} className="sr-only">{title}</h2>
82
+ <div className="flex shrink-0 items-center justify-between gap-3 border-b border-ink/10 px-4 py-3">
83
+ <button
84
+ type="button"
85
+ onClick={onClose}
86
+ className="flex h-11 w-11 items-center justify-center rounded-full text-ink transition-colors duration-150 hover:bg-ink/[0.06] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink motion-reduce:transition-none"
87
+ >
88
+ <X aria-hidden="true" size={20} strokeWidth={2} />
89
+ <span className="sr-only">{t.close}</span>
90
+ </button>
91
+ {actions && <div className="flex flex-wrap items-center justify-end gap-2">{actions}</div>}
92
+ </div>
93
+ <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">{children}</div>
94
+ </dialog>
95
+ );
96
+ }
@@ -0,0 +1,107 @@
1
+ import type { ElementType, ReactNode } from "react";
2
+
3
+ export type StoryPillar = { title: ReactNode; text: ReactNode };
4
+ export type StoryLink = { label: ReactNode; href: string; external?: boolean };
5
+
6
+ type Labels = {
7
+ /** sr-only after an external link. */
8
+ newTab: string;
9
+ };
10
+ const EN: Labels = { newTab: "(opens in a new tab)" };
11
+
12
+ type Props = {
13
+ /** What fills the media box at the top: a BackgroundVideo, an <img>
14
+ * (absolute inset-0, object-cover). The panel's film fades it into the panel. */
15
+ media: ReactNode;
16
+ /** Box of the media: 4:3 on the phone, 21:8 from md (creeta). */
17
+ mediaAspect?: string;
18
+ /** Logo for the dark background, at the base of the media where the film is already full. */
19
+ logo?: ReactNode;
20
+ title: ReactNode;
21
+ lead?: ReactNode;
22
+ /** Three columns with a hairline on top: a short title and a paragraph. */
23
+ pillars: StoryPillar[];
24
+ /** Outbound links at the end ("Learn about the method ↗"). */
25
+ links?: StoryLink[];
26
+ headingLevel?: "h2" | "h3";
27
+ /** Colour of the pillar titles. Default `text-surface`; creeta passes its light teal. */
28
+ pillarTitleClassName?: string;
29
+ linkComponent?: ElementType;
30
+ labels?: Partial<Labels>;
31
+ className?: string;
32
+ };
33
+
34
+ const ArrowUpRight = () => (
35
+ <svg aria-hidden="true" viewBox="0 0 24 24" width={18} height={18} className="cg-arrow-go" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M7 17 17 7M9 7h8v8" /></svg>
36
+ );
37
+
38
+ const link =
39
+ "inline-flex min-h-11 items-center gap-1.5 font-semibold underline underline-offset-4 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface";
40
+
41
+ /**
42
+ * Story panel of the Cuidado Global family — creeta's `OrigemRta`, by tokens:
43
+ * a dark panel (surface-panel) that groups a story in one piece. Media at the
44
+ * top with a film that fades into the panel, the logo at its base, a title and
45
+ * a lead, three pillars in columns with a hairline above each, and the outbound
46
+ * links. Good for "who we are" blocks: one rounded card, everything inside.
47
+ */
48
+ export function StoryPanel({
49
+ media,
50
+ mediaAspect = "aspect-[4/3] md:aspect-[21/8]",
51
+ logo,
52
+ title,
53
+ lead,
54
+ pillars,
55
+ links,
56
+ headingLevel: Title = "h2",
57
+ pillarTitleClassName = "text-surface",
58
+ linkComponent,
59
+ labels,
60
+ className = "",
61
+ }: Props) {
62
+ const L: ElementType = linkComponent ?? "a";
63
+ const t = { ...EN, ...labels };
64
+ return (
65
+ <section className={`overflow-hidden rounded-[28px] bg-surface-panel text-surface ${className}`}>
66
+ <div className={`relative ${mediaAspect}`}>
67
+ {media}
68
+ {/* -bottom-px: the box's height is fractional and a light half pixel showed under the film */}
69
+ <span aria-hidden="true" className="pointer-events-none absolute inset-0 -bottom-px bg-gradient-to-t from-surface-panel from-10% via-surface-panel/70 via-40% to-surface-panel/10" />
70
+ {logo && <div className="absolute bottom-2 left-6 md:bottom-4 md:left-12">{logo}</div>}
71
+ </div>
72
+
73
+ <div className="px-6 pb-9 pt-7 md:px-12 md:pb-14 md:pt-9">
74
+ <Title className="max-w-3xl font-heading text-[1.75rem] font-medium leading-[1.12] tracking-[-0.01em] md:text-5xl">{title}</Title>
75
+ {lead && <p className="mt-4 max-w-2xl text-lg leading-relaxed text-surface/85">{lead}</p>}
76
+
77
+ <ul className="mt-8 grid list-none gap-6 md:mt-12 md:grid-cols-3 md:gap-10">
78
+ {pillars.map((p, i) => (
79
+ <li key={i} className="border-t border-surface/25 pt-5">
80
+ <p className={`font-heading text-2xl font-medium ${pillarTitleClassName}`}>{p.title}</p>
81
+ <p className="mt-2 leading-relaxed text-surface/85">{p.text}</p>
82
+ </li>
83
+ ))}
84
+ </ul>
85
+
86
+ {links && links.length > 0 && (
87
+ <div className="mt-8 flex flex-wrap gap-x-8 gap-y-1 md:mt-10">
88
+ {links.map((l, i) =>
89
+ l.external ? (
90
+ <a key={i} href={l.href} target="_blank" rel="noopener noreferrer" className={link}>
91
+ {l.label}
92
+ <ArrowUpRight />
93
+ <span className="sr-only"> {t.newTab}</span>
94
+ </a>
95
+ ) : (
96
+ <L key={i} href={l.href} className={link}>
97
+ {l.label}
98
+ <ArrowUpRight />
99
+ </L>
100
+ ),
101
+ )}
102
+ </div>
103
+ )}
104
+ </div>
105
+ </section>
106
+ );
107
+ }
@@ -0,0 +1,39 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef, useState } from "react";
4
+
5
+ /** whole: the drawing finished, not animating (no JavaScript, no
6
+ * IntersectionObserver, or reduced motion).
7
+ * waiting: hidden, until it shows up on screen.
8
+ * playing: it showed up; the animation runs. */
9
+ export type AppearState = "whole" | "waiting" | "playing";
10
+
11
+ export const reducedMotion = () =>
12
+ typeof window !== "undefined" && !!window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
13
+
14
+ /** For drawings that animate once half of them is on screen (creeta's
15
+ * `useAoAparecer`). The default is the finished drawing: it only hides to
16
+ * animate when it can animate. `onScreen` follows the drawing the whole time:
17
+ * a looping animation only runs while someone can see it. */
18
+ export function useOnAppear<T extends HTMLElement>() {
19
+ const ref = useRef<T>(null);
20
+ const [state, setState] = useState<AppearState>("whole");
21
+ const [onScreen, setOnScreen] = useState(false);
22
+
23
+ useEffect(() => {
24
+ const el = ref.current;
25
+ if (!el || typeof IntersectionObserver === "undefined" || reducedMotion()) return;
26
+ setState("waiting");
27
+ const eye = new IntersectionObserver(
28
+ ([e]) => {
29
+ setOnScreen(!!e.isIntersecting);
30
+ if (e.isIntersecting) setState("playing");
31
+ },
32
+ { threshold: 0.5 },
33
+ );
34
+ eye.observe(el);
35
+ return () => eye.disconnect();
36
+ }, []);
37
+
38
+ return { ref, state, onScreen };
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breathe-ds/ui",
3
- "version": "0.3.0",
3
+ "version": "0.5.1",
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",
@@ -15,9 +15,18 @@
15
15
  "LICENSE"
16
16
  ],
17
17
  "exports": {
18
- "./theme.css": "./theme.css",
19
- "./tokens/ttcab.css": "./tokens/ttcab.css",
20
- "./tokens/creeta.css": "./tokens/creeta.css",
18
+ "./theme.css": {
19
+ "style": "./theme.css",
20
+ "default": "./theme.css"
21
+ },
22
+ "./tokens/ttcab.css": {
23
+ "style": "./tokens/ttcab.css",
24
+ "default": "./tokens/ttcab.css"
25
+ },
26
+ "./tokens/creeta.css": {
27
+ "style": "./tokens/creeta.css",
28
+ "default": "./tokens/creeta.css"
29
+ },
21
30
  "./components/Accordion": "./components/Accordion.tsx",
22
31
  "./components/PageVideoHero": "./components/PageVideoHero.tsx",
23
32
  "./components/Footer": "./components/Footer.tsx",
@@ -42,12 +51,16 @@
42
51
  "./components/BreatheMark": "./components/BreatheMark.tsx",
43
52
  "./components/Curtain": "./components/Curtain.tsx",
44
53
  "./components/Lockup": "./components/Lockup.tsx",
45
- "./tokens/neutral.css": "./tokens/neutral.css",
54
+ "./tokens/neutral.css": {
55
+ "style": "./tokens/neutral.css",
56
+ "default": "./tokens/neutral.css"
57
+ },
46
58
  "./components/icons": "./components/icons.tsx",
47
59
  "./components/AnchorNav": "./components/AnchorNav.tsx",
48
60
  "./components/LanguageSwitcher": "./components/LanguageSwitcher.tsx",
49
61
  "./components/List": "./components/List.tsx",
50
62
  "./components/Rows": "./components/Rows.tsx",
63
+ "./components/Rail": "./components/Rail.tsx",
51
64
  "./components/Avatar": "./components/Avatar.tsx",
52
65
  "./components/Byline": "./components/Byline.tsx",
53
66
  "./components/ProfessionalCard": "./components/ProfessionalCard.tsx",
@@ -57,7 +70,24 @@
57
70
  "./components/Breadcrumb": "./components/Breadcrumb.tsx",
58
71
  "./components/Timeline": "./components/Timeline.tsx",
59
72
  "./components/marks/ttcab": "./components/marks/ttcab.tsx",
60
- "./components/marks/creeta": "./components/marks/creeta.tsx"
73
+ "./components/marks/creeta": "./components/marks/creeta.tsx",
74
+ "./components/CareSteps": "./components/CareSteps.tsx",
75
+ "./components/ChatPreview": "./components/ChatPreview.tsx",
76
+ "./components/PlanTimeline": "./components/PlanTimeline.tsx",
77
+ "./components/PhotoSequence": "./components/PhotoSequence.tsx",
78
+ "./components/PauseButton": "./components/PauseButton.tsx",
79
+ "./components/useOnAppear": "./components/useOnAppear.ts",
80
+ "./components/StoryPanel": "./components/StoryPanel.tsx",
81
+ "./components/PostCard": "./components/PostCard.tsx",
82
+ "./components/FeaturedPosts": "./components/FeaturedPosts.tsx",
83
+ "./components/PostArticle": "./components/PostArticle.tsx",
84
+ "./components/CourseCard": "./components/CourseCard.tsx",
85
+ "./components/DateBadge": "./components/DateBadge.tsx",
86
+ "./components/EventRow": "./components/EventRow.tsx",
87
+ "./components/EventList": "./components/EventList.tsx",
88
+ "./components/EventFacts": "./components/EventFacts.tsx",
89
+ "./components/RegistrationBox": "./components/RegistrationBox.tsx",
90
+ "./components/SidePanel": "./components/SidePanel.tsx"
61
91
  },
62
92
  "publishConfig": {
63
93
  "access": "public"