@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
- watch: (description: string) => string;
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={t.watch(cardVideo.description)} />
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
- {(overflows || panel) && (
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={t.watch(video.description)} frameClass="relative aspect-video overflow-hidden bg-ink" />
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
- captionStyle?: "plain" | "pill";
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 text-ink ${captionStyle === "pill" ? "rounded-2xl bg-card/90 px-3 py-2 backdrop-blur-sm" : "[text-shadow:0_1px_2px_rgba(255,255,255,0.5)]"}`}>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@breathe-ds/ui",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",