@breathe-ds/ui 0.6.5 → 0.6.7
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 +1 -1
- package/components/BottomNav.tsx +121 -26
- package/components/Callout.tsx +1 -1
- package/components/FactStrip.tsx +1 -1
- package/components/Footer.tsx +1 -1
- package/components/InfoCard.tsx +1 -1
- package/components/MediaCard.tsx +1 -1
- package/components/ProfessionalModal.tsx +1 -1
- package/components/ProfileSpotlight.tsx +1 -1
- package/components/Timeline.tsx +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -246,7 +246,7 @@ from color, not from extra markup.
|
|
|
246
246
|
shrinks the tap targets); the rest and the external exits (`extras`, with the domain spelled out
|
|
247
247
|
and a ↗ arrow) go to the "More" panel, a native popover (Esc, tap outside, no
|
|
248
248
|
JS). Gradient veil behind the bar. `hideFrom` = `md` (creeta) or `lg`
|
|
249
|
-
(ttcab). `labels` for another language. `embedded` for the demo frame.
|
|
249
|
+
(ttcab). `labels` for another language. `embedded` for the demo frame. `collapseOnScroll` (reading mode): scrolling down shrinks it to a slim icon strip; scrolling up, a tap or keyboard focus brings it back; full at the top and the end (`scrollContainer` when the window is not what scrolls). `group` on a page lists it under a section title in "More" (sticky while the list scrolls).
|
|
250
250
|
- **AnchorNav**: floating capsule of page anchors (scroll-spy + sliding
|
|
251
251
|
indicator), extracted from ttcab's TreatmentAnchorNav. Target sections need
|
|
252
252
|
a `scroll-mt-*` compatible with the sticky header. White capsule (`card`);
|
package/components/BottomNav.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { version as REACT_VERSION, useEffect, useId, useRef, useState, type ComponentType, type ElementType, type ReactNode } from "react";
|
|
3
|
+
import { version as REACT_VERSION, useEffect, useId, useRef, useState, type ComponentType, type ElementType, type ReactNode, type RefObject } from "react";
|
|
4
4
|
|
|
5
5
|
// the popover trigger attribute: camelCase in React 19+, lowercase passthrough in 18
|
|
6
6
|
const POPOVER_TARGET = parseInt(REACT_VERSION, 10) >= 19 ? "popoverTarget" : "popovertarget";
|
|
7
7
|
|
|
8
8
|
/** Icon in the lucide pattern (the component, not the node): the bar decides the size. */
|
|
9
|
-
export type IconType = ComponentType<{ size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
9
|
+
export type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
10
10
|
|
|
11
11
|
/** One page of the site, in order of importance. The first ones stay on the bar;
|
|
12
12
|
* those that don't fit go into "More". */
|
|
@@ -16,6 +16,9 @@ export type BottomPage = {
|
|
|
16
16
|
Icon: IconType;
|
|
17
17
|
/** Is this the current page? (the app decides: route/locale are its own) */
|
|
18
18
|
active?: boolean;
|
|
19
|
+
/** Section title in the "More" panel. Pages with the same group are listed
|
|
20
|
+
* together under it, in order of first appearance. Without it: one plain list. */
|
|
21
|
+
group?: string;
|
|
19
22
|
};
|
|
20
23
|
|
|
21
24
|
/** Extra exit in the "More" panel (e.g. courses on another site): address written
|
|
@@ -66,6 +69,12 @@ type Props = {
|
|
|
66
69
|
/** Embedded in a `relative` container (demo, phone frame): bar, veil and
|
|
67
70
|
* panel are `absolute` inside it and show at any width. */
|
|
68
71
|
embedded?: boolean;
|
|
72
|
+
/** Reading mode: scrolling down shrinks the bar to a slim icon strip at the
|
|
73
|
+
* bottom; scrolling up, tapping it or reaching it by keyboard brings it back.
|
|
74
|
+
* Always full at the top and at the end of the page. Off by default. */
|
|
75
|
+
collapseOnScroll?: boolean;
|
|
76
|
+
/** Element that scrolls, when it isn't the window (e.g. the demo frame). */
|
|
77
|
+
scrollContainer?: RefObject<HTMLElement | null>;
|
|
69
78
|
};
|
|
70
79
|
|
|
71
80
|
/** Minimum width of a tab, in px: below this the label doesn't fit. */
|
|
@@ -93,14 +102,21 @@ const ArrowUpRight = () => (
|
|
|
93
102
|
// Tab: icon+label column. The active one gets a SHAPE (light pill), not just color —
|
|
94
103
|
// readable for people who can't tell colors apart. Shrinks on touch, like an app button.
|
|
95
104
|
// The view-transition name makes the highlight slide between tabs on page change.
|
|
96
|
-
const tab = (active: boolean) =>
|
|
105
|
+
const tab = (active: boolean, collapsed = false) =>
|
|
97
106
|
[
|
|
98
|
-
"flex
|
|
99
|
-
"
|
|
107
|
+
"flex w-full flex-col items-center justify-center rounded-[20px] px-1 text-[10.5px] font-medium leading-none tracking-tight",
|
|
108
|
+
collapsed ? "h-9 gap-0" : "h-14 gap-1",
|
|
109
|
+
"transition-all duration-200 ease-[cubic-bezier(.23,1,.32,1)] active:scale-[0.96] motion-reduce:transition-none motion-reduce:active:scale-100",
|
|
100
110
|
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-surface",
|
|
101
111
|
active ? "bg-surface text-ink [view-transition-name:aba-ativa]" : "text-surface/85 hover:bg-white/10",
|
|
102
112
|
].join(" ");
|
|
103
113
|
|
|
114
|
+
// label under the icon: in the slim bar it folds away (still read by screen readers)
|
|
115
|
+
const LABEL = "w-full truncate text-center transition-all duration-200 motion-reduce:transition-none";
|
|
116
|
+
|
|
117
|
+
const GROUP_TITLE =
|
|
118
|
+
"sticky -top-2 z-10 -mx-2 bg-card px-5 pt-3 pb-1.5 font-heading text-[0.7rem] font-semibold tracking-[0.12em] text-ink-soft uppercase";
|
|
119
|
+
|
|
104
120
|
const row = (active?: boolean) =>
|
|
105
121
|
`flex min-h-14 items-center gap-3 rounded-[18px] px-3 font-medium transition-colors duration-150 active:bg-surface focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ink motion-reduce:transition-none ${
|
|
106
122
|
active ? "bg-surface text-ink" : "text-ink"
|
|
@@ -121,7 +137,7 @@ const bubble = "flex h-10 w-10 shrink-0 items-center justify-center rounded-full
|
|
|
121
137
|
* (demo), it becomes an absolute panel inside the frame, with the same entrance.
|
|
122
138
|
* - Hidden from `hideFrom` up (md on creeta, lg on ttcab), where the header takes over.
|
|
123
139
|
*/
|
|
124
|
-
export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg", labels, embedded = false }: Props) {
|
|
140
|
+
export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg", labels, embedded = false, collapseOnScroll = false, scrollContainer }: Props) {
|
|
125
141
|
const L: ElementType = linkComponent ?? "a";
|
|
126
142
|
const t = { ...EN, ...labels };
|
|
127
143
|
const list = useRef<HTMLUListElement>(null);
|
|
@@ -131,6 +147,56 @@ export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg",
|
|
|
131
147
|
// before measuring: the typical phone case (5 tabs = 4 pages + More)
|
|
132
148
|
const [visible, setVisible] = useState(() => howManyFit(5 * TAB_MIN_WIDTH, pages.length));
|
|
133
149
|
const [open, setOpen] = useState(false); // embedded mode only
|
|
150
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
151
|
+
|
|
152
|
+
// Reading mode: direction of the scroll, with a small dead zone so a shaky
|
|
153
|
+
// thumb doesn't make the bar blink. Down 24px → slim; up 12px → full.
|
|
154
|
+
useEffect(() => {
|
|
155
|
+
if (!collapseOnScroll) return setCollapsed(false);
|
|
156
|
+
const el = scrollContainer?.current ?? null;
|
|
157
|
+
const y = () => (el ? el.scrollTop : window.scrollY);
|
|
158
|
+
const atEnd = () =>
|
|
159
|
+
el ? el.scrollTop + el.clientHeight >= el.scrollHeight - 8 : window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 8;
|
|
160
|
+
let last = y();
|
|
161
|
+
let run = 0;
|
|
162
|
+
let raf = 0;
|
|
163
|
+
const onScroll = () => {
|
|
164
|
+
if (raf) return;
|
|
165
|
+
raf = requestAnimationFrame(() => {
|
|
166
|
+
raf = 0;
|
|
167
|
+
const now = y();
|
|
168
|
+
const d = now - last;
|
|
169
|
+
last = now;
|
|
170
|
+
if (now < 80 || atEnd()) {
|
|
171
|
+
run = 0;
|
|
172
|
+
return setCollapsed(false);
|
|
173
|
+
}
|
|
174
|
+
run = Math.sign(d) === Math.sign(run) ? run + d : d;
|
|
175
|
+
if (run > 24) setCollapsed(true);
|
|
176
|
+
else if (run < -12) setCollapsed(false);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const target: HTMLElement | Window = el ?? window;
|
|
180
|
+
target.addEventListener("scroll", onScroll, { passive: true });
|
|
181
|
+
return () => {
|
|
182
|
+
target.removeEventListener("scroll", onScroll);
|
|
183
|
+
cancelAnimationFrame(raf);
|
|
184
|
+
};
|
|
185
|
+
}, [collapseOnScroll, scrollContainer]);
|
|
186
|
+
// native popover (site mode): the panel's real state, so the bar stays full while it's open
|
|
187
|
+
const [popOpen, setPopOpen] = useState(false);
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
const el = panel.current;
|
|
190
|
+
if (!el) return;
|
|
191
|
+
const onToggle = (e: Event) => {
|
|
192
|
+
const isOpen = (e as Event & { newState?: string }).newState === "open";
|
|
193
|
+
setPopOpen(isOpen);
|
|
194
|
+
if (isOpen) setCollapsed(false);
|
|
195
|
+
};
|
|
196
|
+
el.addEventListener("toggle", onToggle);
|
|
197
|
+
return () => el.removeEventListener("toggle", onToggle);
|
|
198
|
+
}, []);
|
|
199
|
+
const slim = collapsed && !open && !popOpen;
|
|
134
200
|
|
|
135
201
|
useEffect(() => {
|
|
136
202
|
const ul = list.current;
|
|
@@ -178,6 +244,13 @@ export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg",
|
|
|
178
244
|
|
|
179
245
|
const onBar = pages.slice(0, visible);
|
|
180
246
|
const inMore = pages.slice(visible);
|
|
247
|
+
// "More" pages by group (order of first appearance); ungrouped ones form a group without title
|
|
248
|
+
const groups: { title?: string; items: BottomPage[] }[] = [];
|
|
249
|
+
for (const p of inMore) {
|
|
250
|
+
const g = groups.find((x) => x.title === p.group);
|
|
251
|
+
if (g) g.items.push(p);
|
|
252
|
+
else groups.push({ title: p.group, items: [p] });
|
|
253
|
+
}
|
|
181
254
|
const moreActive = inMore.some((p) => p.active) || extras.some((x) => x.active);
|
|
182
255
|
const pos = embedded ? "absolute" : "fixed";
|
|
183
256
|
// full class names (Tailwind only generates what it finds written in the code)
|
|
@@ -186,18 +259,25 @@ export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg",
|
|
|
186
259
|
const panelInner = (
|
|
187
260
|
<nav aria-label={t.morePages}>
|
|
188
261
|
{/* One item per row: in two columns the panel got confusing. */}
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
262
|
+
{groups.map(({ title, items }, gi) => {
|
|
263
|
+
const headId = `${panelId}-g${gi}`;
|
|
264
|
+
return (
|
|
265
|
+
<div key={gi} className={gi > 0 ? "mt-1 border-t border-ink/10 pt-1" : undefined}>
|
|
266
|
+
{/* title sticks to the top of the panel while its list scrolls by */}
|
|
267
|
+
{title && <p id={headId} className={GROUP_TITLE}>{title}</p>}
|
|
268
|
+
<ul className="grid list-none gap-1" aria-labelledby={title ? headId : undefined}>
|
|
269
|
+
{items.map(({ href, label, Icon, active }, i) => (
|
|
270
|
+
<li key={`${i}-${href}`}>
|
|
271
|
+
<L href={href} aria-current={active ? "page" : undefined} onClick={close} className={row(active)}>
|
|
272
|
+
<span className={bubble}><Icon aria-hidden="true" size={19} strokeWidth={1.75} /></span>
|
|
273
|
+
{label}
|
|
274
|
+
</L>
|
|
275
|
+
</li>
|
|
276
|
+
))}
|
|
277
|
+
</ul>
|
|
278
|
+
</div>
|
|
279
|
+
);
|
|
280
|
+
})}
|
|
201
281
|
{extras.length > 0 && (
|
|
202
282
|
<ul className={`grid list-none gap-1${inMore.length > 0 ? " mt-1 border-t border-ink/10 pt-1" : ""}`}>
|
|
203
283
|
{extras.map(({ href, label, Icon, description, external, active }, i) => {
|
|
@@ -235,21 +315,31 @@ export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg",
|
|
|
235
315
|
reaching it, instead of showing up clipped by the edges. */}
|
|
236
316
|
<div
|
|
237
317
|
aria-hidden="true"
|
|
238
|
-
className={`pointer-events-none ${pos} inset-x-0 bottom-0 z-40 h-28 bg-gradient-to-t from-surface from-35% via-surface/85 via-65% to-transparent [view-transition-name:veu]${hide}`}
|
|
318
|
+
className={`pointer-events-none ${pos} inset-x-0 bottom-0 z-40 ${slim ? "h-14" : "h-28"} transition-[height] duration-200 ease-[cubic-bezier(.23,1,.32,1)] motion-reduce:transition-none bg-gradient-to-t from-surface from-35% via-surface/85 via-65% to-transparent [view-transition-name:veu]${hide}`}
|
|
239
319
|
/>
|
|
240
320
|
<nav
|
|
241
321
|
aria-label={t.nav}
|
|
242
|
-
className={`${pos} inset-x-3 bottom-3 z-50 pb-[env(safe-area-inset-bottom)] [view-transition-name:barra]${hide}`}
|
|
322
|
+
className={`${pos} inset-x-3 ${slim ? "bottom-1.5" : "bottom-3"} z-50 pb-[env(safe-area-inset-bottom)] transition-[bottom] duration-200 ease-[cubic-bezier(.23,1,.32,1)] motion-reduce:transition-none [view-transition-name:barra]${hide}`}
|
|
323
|
+
// slim: a tap anywhere on the bar only brings it back (doesn't navigate);
|
|
324
|
+
// keyboard focus brings it back too (a mouse/touch focus doesn't, or the tap would navigate)
|
|
325
|
+
// …except "More": the hamburger is a clear intent, it expands AND opens the panel in one tap
|
|
326
|
+
onClickCapture={slim ? (e) => {
|
|
327
|
+
setCollapsed(false);
|
|
328
|
+
if ((e.target as Element).closest?.("[data-more]")) return;
|
|
329
|
+
e.preventDefault();
|
|
330
|
+
e.stopPropagation();
|
|
331
|
+
} : undefined}
|
|
332
|
+
onFocus={(e) => { if ((e.target as Element).matches?.(":focus-visible")) setCollapsed(false); }}
|
|
243
333
|
>
|
|
244
334
|
<ul
|
|
245
335
|
ref={list}
|
|
246
|
-
className=
|
|
336
|
+
className={`flex list-none items-stretch justify-between gap-0.5 rounded-[26px] bg-ink/95 ${slim ? "p-1" : "p-1.5"} transition-[padding] duration-200 motion-reduce:transition-none shadow-[0_12px_32px_-12px_rgba(0,0,0,0.5)] backdrop-blur-md`}
|
|
247
337
|
>
|
|
248
338
|
{onBar.map(({ href, label, Icon, active }, i) => (
|
|
249
339
|
<li key={`${i}-${href}`} className="min-w-0 flex-1">
|
|
250
|
-
<L href={href} aria-current={active ? "page" : undefined} className={tab(!!active)}>
|
|
340
|
+
<L href={href} aria-current={active ? "page" : undefined} className={tab(!!active, slim)}>
|
|
251
341
|
<Icon aria-hidden="true" size={20} strokeWidth={1.75} />
|
|
252
|
-
<span className="
|
|
342
|
+
<span className={`${LABEL} ${slim ? "max-h-0 opacity-0" : "max-h-4 opacity-100"}`}>{label}</span>
|
|
253
343
|
</L>
|
|
254
344
|
</li>
|
|
255
345
|
))}
|
|
@@ -257,11 +347,12 @@ export function BottomNav({ pages, extras = [], linkComponent, hideFrom = "lg",
|
|
|
257
347
|
{/* React 19 knows `popoverTarget`; React 18 only passes the lowercase attribute through as is. */}
|
|
258
348
|
<button
|
|
259
349
|
type="button"
|
|
350
|
+
data-more=""
|
|
260
351
|
{...(embedded ? { "aria-expanded": open, "aria-controls": panelId, onClick: () => setOpen((o) => !o) } : { [POPOVER_TARGET]: panelId })}
|
|
261
|
-
className={tab(embedded ? open || moreActive : moreActive)}
|
|
352
|
+
className={tab(embedded ? open || moreActive : moreActive, slim)}
|
|
262
353
|
>
|
|
263
354
|
<MenuIcon />
|
|
264
|
-
<span className="
|
|
355
|
+
<span className={`${LABEL} ${slim ? "max-h-0 opacity-0" : "max-h-4 opacity-100"}`}>{t.more}</span>
|
|
265
356
|
<span className="sr-only"> {t.morePagesSr}{moreActive ? ` ${t.currentHere}` : ""}</span>
|
|
266
357
|
</button>
|
|
267
358
|
</li>
|
|
@@ -289,6 +380,10 @@ const CSS = `
|
|
|
289
380
|
.cg-more-panel{
|
|
290
381
|
position:fixed; inset:auto .75rem calc(5.75rem + env(safe-area-inset-bottom)) .75rem;
|
|
291
382
|
width:auto; margin:0; border:0; border-radius:26px; padding:.5rem;
|
|
383
|
+
/* long list (many pages): the panel stops below the top of the screen and
|
|
384
|
+
scrolls inside, instead of running off the top with the first items lost */
|
|
385
|
+
max-height:calc(100dvh - 5.75rem - env(safe-area-inset-bottom) - .75rem);
|
|
386
|
+
overflow-y:auto; overscroll-behavior:contain; scrollbar-width:thin;
|
|
292
387
|
background:var(--color-card); color:var(--color-ink);
|
|
293
388
|
box-shadow:0 20px 48px -16px rgb(0 0 0 / .35), 0 1px 2px rgb(0 0 0 / .1);
|
|
294
389
|
opacity:0; transform:translateY(8px) scale(.97); transform-origin:88% 100%;
|
|
@@ -297,7 +392,7 @@ const CSS = `
|
|
|
297
392
|
.cg-more-panel:popover-open, .cg-more-panel--embedded{ opacity:1; transform:none; }
|
|
298
393
|
@starting-style{ .cg-more-panel:popover-open, .cg-more-panel--embedded{ opacity:0; transform:translateY(8px) scale(.97); } }
|
|
299
394
|
.cg-more-panel::backdrop{ background:transparent; }
|
|
300
|
-
.cg-more-panel--embedded{ position:absolute; z-index:50; }
|
|
395
|
+
.cg-more-panel--embedded{ position:absolute; z-index:50; max-height:calc(100% - 6.5rem); }
|
|
301
396
|
@media (prefers-reduced-motion:reduce){
|
|
302
397
|
.cg-more-panel{ transform:none; transition:opacity 120ms ease; }
|
|
303
398
|
@starting-style{ .cg-more-panel:popover-open, .cg-more-panel--embedded{ transform:none; } }
|
package/components/Callout.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
type IconType = ComponentType<{ size?: number; strokeWidth?: number; "aria-hidden"?: boolean | "true" }>;
|
|
3
|
+
type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
6
|
title: ReactNode;
|
package/components/FactStrip.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
type IconType = ComponentType<{ size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
3
|
+
type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
4
4
|
|
|
5
5
|
export type Fact = {
|
|
6
6
|
Icon: IconType;
|
package/components/Footer.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { LanguageSwitcher, type Language } from "./LanguageSwitcher";
|
|
|
3
3
|
import { List } from "./List";
|
|
4
4
|
|
|
5
5
|
/** lucide-style icon component (the footer sets the size). */
|
|
6
|
-
type IconType = ComponentType<{ size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
6
|
+
type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
7
7
|
|
|
8
8
|
export type FooterLink = { href: string; label: ReactNode };
|
|
9
9
|
/** A titled column of links, for footers with many pages ("Foundations", "Cards…"). */
|
package/components/InfoCard.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ComponentType, ElementType, ReactNode } from "react";
|
|
2
2
|
|
|
3
3
|
/** lucide-style icon component (the card sets the size). */
|
|
4
|
-
type IconType = ComponentType<{ size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
4
|
+
type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
5
5
|
|
|
6
6
|
export type InfoCardLink = {
|
|
7
7
|
href: string;
|
package/components/MediaCard.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useEffect, useRef, useState, type ComponentType, type ElementType, type ReactNode } from "react";
|
|
4
4
|
|
|
5
5
|
/** lucide-style icon component (the badge sets the size). */
|
|
6
|
-
type IconType = ComponentType<{ size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
6
|
+
type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
7
7
|
|
|
8
8
|
/** Event the card videos listen to when the person pauses/resumes them all
|
|
9
9
|
* (dispatch it on `window` after toggling the attribute below). */
|
|
@@ -39,7 +39,7 @@ export type ProfessionalModalLabels = {
|
|
|
39
39
|
|
|
40
40
|
/** Lucide's icon type is wider than MediaCardBadge's `IconType` under strict
|
|
41
41
|
* TS (propTypes allow a string `size`); this thin wrapper fits the slot. */
|
|
42
|
-
const PlayIcon = (p: { size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }) => <Play {...p} />;
|
|
42
|
+
const PlayIcon = (p: { size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }) => <Play {...p} />;
|
|
43
43
|
|
|
44
44
|
const EN: ProfessionalModalLabels = {
|
|
45
45
|
presentation: "Presentation",
|
|
@@ -21,7 +21,7 @@ type Props = {
|
|
|
21
21
|
actions?: ReactNode;
|
|
22
22
|
/** Social profiles at the foot (Instagram, LinkedIn, Lattes…): round icon
|
|
23
23
|
* links, each with its name for screen readers, opening in a new tab. */
|
|
24
|
-
social?: { label: string; href: string; Icon: ComponentType<{ size?: number; strokeWidth?: number; "aria-hidden"?: boolean | "true" }> }[];
|
|
24
|
+
social?: { label: string; href: string; Icon: ComponentType<{ size?: number | string; strokeWidth?: number | string; "aria-hidden"?: boolean | "true" | "false" }> }[];
|
|
25
25
|
/** sr-only after each social link. */
|
|
26
26
|
newTabLabel?: string;
|
|
27
27
|
/** `card`: on a white card. `plain` (default): straight on the page color. */
|
package/components/Timeline.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentType, ElementType, ReactNode } from "react";
|
|
2
2
|
|
|
3
|
-
type IconType = ComponentType<{ size?: number; strokeWidth?: number; className?: string; "aria-hidden"?: boolean | "true" }>;
|
|
3
|
+
type IconType = ComponentType<{ size?: number | string; strokeWidth?: number | string; className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
|
|
4
4
|
|
|
5
5
|
export type TimelineStep = {
|
|
6
6
|
Icon: IconType;
|
package/package.json
CHANGED