@breathe-ds/ui 0.6.2 → 0.6.4
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.
|
@@ -7,8 +7,9 @@ import { Byline } from "./Byline";
|
|
|
7
7
|
import { SidePanel } from "./SidePanel";
|
|
8
8
|
|
|
9
9
|
type Labels = {
|
|
10
|
-
/** sr-only text of the play button: "Watch: {description}".
|
|
11
|
-
|
|
10
|
+
/** sr-only text of the play button: "Watch: {description}". A function, or a
|
|
11
|
+
* string with `{description}` (a string can cross from a server component). */
|
|
12
|
+
watch: ((description: string) => string) | string;
|
|
12
13
|
/** sr-only after the outbound link. */
|
|
13
14
|
newTab: string;
|
|
14
15
|
/** Button under a clamped quote that opens the whole testimonial. */
|
|
@@ -180,6 +181,7 @@ const CLAMP: Record<number, string> = { 3: "line-clamp-3", 4: "line-clamp-4", 5:
|
|
|
180
181
|
|
|
181
182
|
export function TestimonialCard({ quote, author, authorPhoto, authorAvatar, authors, link, video, photo, mediaPlacement = "card", role, joiners, panel, excerpt, excerptMarks = true, clampLines, as: Root = "div", labels, className = "" }: Props) {
|
|
182
183
|
const t = { ...EN, ...labels };
|
|
184
|
+
const watch = (d: string) => (typeof t.watch === "function" ? t.watch(d) : t.watch.replace("{description}", d));
|
|
183
185
|
const people = authors ?? [{ name: author ?? "", photo: authorPhoto, avatar: authorAvatar }];
|
|
184
186
|
const cardPhoto = mediaPlacement === "card" ? photo : undefined;
|
|
185
187
|
const cardVideo = mediaPlacement === "card" ? video : undefined;
|
|
@@ -208,10 +210,13 @@ export function TestimonialCard({ quote, author, authorPhoto, authorAvatar, auth
|
|
|
208
210
|
joiners={joiners}
|
|
209
211
|
/>
|
|
210
212
|
);
|
|
213
|
+
// the whole card opens the panel (stretched link: the button's ::after covers the card);
|
|
214
|
+
// the video and the source link sit above it (relative z-[1]) and keep their own click
|
|
215
|
+
const opens = overflows || !!panel;
|
|
211
216
|
return (
|
|
212
|
-
<Root className={`${cardVideo || cardPhoto ? "grid grid-cols-1 sm:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]" : "flex"} overflow-hidden rounded-[28px] bg-card ${className}`}>
|
|
217
|
+
<Root className={`${opens ? "group/card relative" : ""} ${cardVideo || cardPhoto ? "grid grid-cols-1 sm:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]" : "flex"} overflow-hidden rounded-[28px] bg-card ${className}`}>
|
|
213
218
|
{cardVideo ? (
|
|
214
|
-
<TapVideo {...cardVideo} label={
|
|
219
|
+
<TapVideo {...cardVideo} label={watch(cardVideo.description)} frameClass="relative z-[1] aspect-video overflow-hidden bg-ink sm:aspect-auto sm:min-h-[16rem]" />
|
|
215
220
|
) : cardPhoto ? (
|
|
216
221
|
<div className="relative aspect-[4/3] overflow-hidden bg-tint sm:aspect-auto sm:min-h-[16rem]">
|
|
217
222
|
{cardPhoto.media ?? (
|
|
@@ -240,24 +245,24 @@ export function TestimonialCard({ quote, author, authorPhoto, authorAvatar, auth
|
|
|
240
245
|
{quote}
|
|
241
246
|
</blockquote>
|
|
242
247
|
)}
|
|
243
|
-
{
|
|
248
|
+
{opens && (
|
|
244
249
|
<button
|
|
245
250
|
type="button"
|
|
246
251
|
onClick={() => setOpen(true)}
|
|
247
|
-
className="mt-3 inline-flex min-h-11 items-center gap-1 text-left font-semibold text-ink underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ink"
|
|
252
|
+
className="mt-3 inline-flex min-h-11 items-center gap-1 text-left font-semibold text-ink underline decoration-ink/40 underline-offset-4 transition-[text-decoration-color] duration-150 after:absolute after:inset-0 after:rounded-[28px] after:content-[''] group-hover/card:decoration-ink focus-visible:outline-none focus-visible:after:outline-2 focus-visible:after:outline-offset-2 focus-visible:after:outline-ink motion-reduce:transition-none"
|
|
248
253
|
>
|
|
249
254
|
{video && !cardVideo ? t.watchFull : t.readMore}
|
|
250
255
|
</button>
|
|
251
256
|
)}
|
|
252
257
|
</div>
|
|
253
|
-
<figcaption>{byline}</figcaption>
|
|
258
|
+
<figcaption className={link ? "relative z-[1]" : undefined}>{byline}</figcaption>
|
|
254
259
|
</figure>
|
|
255
260
|
{(clampLines || panel) && (
|
|
256
261
|
<SidePanel open={open} onClose={() => setOpen(false)} title={names} labels={{ close: t.close }}>
|
|
257
262
|
<article className="flex flex-col gap-6 px-6 pt-2 pb-10 md:px-8">
|
|
258
263
|
{panel && video ? (
|
|
259
264
|
<div className="overflow-hidden rounded-2xl">
|
|
260
|
-
<TapVideo {...video} label={
|
|
265
|
+
<TapVideo {...video} label={watch(video.description)} frameClass="relative aspect-video overflow-hidden bg-ink" />
|
|
261
266
|
</div>
|
|
262
267
|
) : panel && photo ? (
|
|
263
268
|
<div className="overflow-hidden rounded-2xl bg-tint">
|
|
@@ -65,8 +65,9 @@ export type VideoInviteCardProps = {
|
|
|
65
65
|
video?: InviteVideo;
|
|
66
66
|
/** `plain` (default): ink text straight over the video, for footage with a light
|
|
67
67
|
* wall behind the title. `pill`: the title on a white pill, legible over any
|
|
68
|
-
* frame (e.g. when the face reaches the top of the crop).
|
|
69
|
-
|
|
68
|
+
* frame (e.g. when the face reaches the top of the crop). `blur`: no fill, only
|
|
69
|
+
* the frosted blur of the video behind, white text with a soft shadow. */
|
|
70
|
+
captionStyle?: "plain" | "pill" | "blur";
|
|
70
71
|
/** Where the title sits on the idle loop: `top` (default) or `bottom` — when the
|
|
71
72
|
* face fills the top of the frame. The play circle stays bottom-right. */
|
|
72
73
|
captionAt?: "top" | "bottom";
|
|
@@ -99,6 +100,14 @@ function PlayGlyph({ size = 22 }: { size?: number }) {
|
|
|
99
100
|
* (`rounded-2xl` in the DS scale); the consumer sets aspect/min-height via
|
|
100
101
|
* `className`.
|
|
101
102
|
*/
|
|
103
|
+
const CAPTION = {
|
|
104
|
+
plain: "text-ink [text-shadow:0_1px_2px_rgba(255,255,255,0.5)]",
|
|
105
|
+
pill: "text-ink rounded-2xl bg-card/90 px-3 py-2 backdrop-blur-sm",
|
|
106
|
+
// a 20% ink veil under the blur: white measured 5.1–5.5:1 over ttcab's loop
|
|
107
|
+
// (4.2–4.7:1 with blur alone); the shadow holds it on the lightest frames
|
|
108
|
+
blur: "text-white rounded-2xl bg-ink/20 px-3 py-2 backdrop-blur-md [text-shadow:0_1px_3px_rgba(0,0,0,0.55)]",
|
|
109
|
+
} as const;
|
|
110
|
+
|
|
102
111
|
export function VideoInviteCard({ title, description, loop, video, captionStyle = "plain", captionAt = "top", onPlay, onEnded, labels, className = "" }: VideoInviteCardProps) {
|
|
103
112
|
const [inviting, setInviting] = useState(false);
|
|
104
113
|
const t = { ...EN, ...labels };
|
|
@@ -108,7 +117,7 @@ export function VideoInviteCard({ title, description, loop, video, captionStyle
|
|
|
108
117
|
median on the loop, 5.86:1 on the invitation). The light text-shadow
|
|
109
118
|
holds it on the darker frames. */
|
|
110
119
|
const caption = (
|
|
111
|
-
<p className={`inline-block text-sm font-medium leading-snug
|
|
120
|
+
<p className={`inline-block text-sm font-medium leading-snug ${CAPTION[captionStyle]}`}>
|
|
112
121
|
<span className="block">{title}</span>
|
|
113
122
|
{description && <span className="block font-normal">{description}</span>}
|
|
114
123
|
</p>
|
package/package.json
CHANGED