@planetaexo/design-system 0.93.1 → 0.94.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.
package/dist/index.d.cts CHANGED
@@ -5429,10 +5429,13 @@ interface ReviewsSpotlightProps {
5429
5429
  className?: string;
5430
5430
  }
5431
5431
  /**
5432
- * ReviewsSpotlight — a single rotating testimonial. One large serif quote with
5433
- * Trustpilot stars, auto-advancing through `items` with clickable dots and a
5434
- * short crossfade. Honours `prefers-reduced-motion` (no auto-advance, instant
5435
- * swap). The dark band matches the rest of {@link NewHome}.
5432
+ * ReviewsSpotlight — a swipeable row of testimonials. Each review is a
5433
+ * full-width slide on a native horizontal scroll-snap track, so on touch it
5434
+ * can be flicked one review at a time; on desktop the dots advance it. One
5435
+ * large serif quote with Trustpilot stars, auto-advancing through `items` with
5436
+ * the pagination dots synced to scroll position. Autoplay pauses on
5437
+ * hover / focus / active swipe and is skipped entirely under
5438
+ * `prefers-reduced-motion`. The dark band matches the rest of {@link NewHome}.
5436
5439
  */
5437
5440
  declare function ReviewsSpotlight({ eyebrow, title, link, items, intervalMs, className, }: ReviewsSpotlightProps): react_jsx_runtime.JSX.Element | null;
5438
5441
 
package/dist/index.d.ts CHANGED
@@ -5429,10 +5429,13 @@ interface ReviewsSpotlightProps {
5429
5429
  className?: string;
5430
5430
  }
5431
5431
  /**
5432
- * ReviewsSpotlight — a single rotating testimonial. One large serif quote with
5433
- * Trustpilot stars, auto-advancing through `items` with clickable dots and a
5434
- * short crossfade. Honours `prefers-reduced-motion` (no auto-advance, instant
5435
- * swap). The dark band matches the rest of {@link NewHome}.
5432
+ * ReviewsSpotlight — a swipeable row of testimonials. Each review is a
5433
+ * full-width slide on a native horizontal scroll-snap track, so on touch it
5434
+ * can be flicked one review at a time; on desktop the dots advance it. One
5435
+ * large serif quote with Trustpilot stars, auto-advancing through `items` with
5436
+ * the pagination dots synced to scroll position. Autoplay pauses on
5437
+ * hover / focus / active swipe and is skipped entirely under
5438
+ * `prefers-reduced-motion`. The dark band matches the rest of {@link NewHome}.
5436
5439
  */
5437
5440
  declare function ReviewsSpotlight({ eyebrow, title, link, items, intervalMs, className, }: ReviewsSpotlightProps): react_jsx_runtime.JSX.Element | null;
5438
5441
 
package/dist/index.js CHANGED
@@ -20210,10 +20210,18 @@ function ReviewsSpotlight({
20210
20210
  intervalMs = 7e3,
20211
20211
  className
20212
20212
  }) {
20213
+ const trackRef = React20.useRef(null);
20213
20214
  const [index, setIndex] = React20.useState(0);
20214
- const [visible, setVisible] = React20.useState(true);
20215
20215
  const reduced = React20.useRef(false);
20216
- const swapRef = React20.useRef(null);
20216
+ const hoverRef = React20.useRef(false);
20217
+ const focusRef = React20.useRef(false);
20218
+ const interactRef = React20.useRef(false);
20219
+ const programmaticRef = React20.useRef(false);
20220
+ const rafRef = React20.useRef(null);
20221
+ const interactTimer = React20.useRef(null);
20222
+ const programmaticTimer = React20.useRef(
20223
+ null
20224
+ );
20217
20225
  React20.useEffect(() => {
20218
20226
  var _a;
20219
20227
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
@@ -20227,56 +20235,107 @@ function ReviewsSpotlight({
20227
20235
  return (_a2 = mq.removeEventListener) == null ? void 0 : _a2.call(mq, "change", onChange);
20228
20236
  };
20229
20237
  }, []);
20230
- const goTo = React20.useCallback((resolve) => {
20231
- if (reduced.current) {
20232
- setIndex((i) => resolve(i));
20233
- return;
20238
+ const scrollToIndex = React20.useCallback((i) => {
20239
+ const el = trackRef.current;
20240
+ if (!el) return;
20241
+ const smooth = !reduced.current;
20242
+ programmaticRef.current = true;
20243
+ el.scrollTo({ left: i * el.clientWidth, behavior: smooth ? "smooth" : "auto" });
20244
+ if (programmaticTimer.current) clearTimeout(programmaticTimer.current);
20245
+ programmaticTimer.current = setTimeout(
20246
+ () => {
20247
+ programmaticRef.current = false;
20248
+ },
20249
+ smooth ? 600 : 50
20250
+ );
20251
+ }, []);
20252
+ const onScroll = React20.useCallback(() => {
20253
+ const el = trackRef.current;
20254
+ if (!el) return;
20255
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
20256
+ rafRef.current = requestAnimationFrame(() => {
20257
+ const i = Math.round(el.scrollLeft / Math.max(1, el.clientWidth));
20258
+ setIndex((prev) => prev === i ? prev : i);
20259
+ });
20260
+ if (!programmaticRef.current) {
20261
+ interactRef.current = true;
20262
+ if (interactTimer.current) clearTimeout(interactTimer.current);
20263
+ interactTimer.current = setTimeout(() => {
20264
+ interactRef.current = false;
20265
+ }, 1500);
20234
20266
  }
20235
- setVisible(false);
20236
- swapRef.current = setTimeout(() => {
20237
- setIndex((i) => resolve(i));
20238
- setVisible(true);
20239
- }, 260);
20240
20267
  }, []);
20241
20268
  React20.useEffect(() => {
20242
20269
  if (items.length <= 1 || intervalMs <= 0) return;
20243
20270
  const id = setInterval(() => {
20244
- if (!reduced.current) goTo((i) => (i + 1) % items.length);
20271
+ if (reduced.current) return;
20272
+ if (hoverRef.current || focusRef.current || interactRef.current) return;
20273
+ const el = trackRef.current;
20274
+ if (!el) return;
20275
+ const cur = Math.round(el.scrollLeft / Math.max(1, el.clientWidth));
20276
+ scrollToIndex((cur + 1) % items.length);
20245
20277
  }, intervalMs);
20246
- return () => {
20247
- clearInterval(id);
20248
- if (swapRef.current) clearTimeout(swapRef.current);
20249
- };
20250
- }, [items.length, intervalMs, goTo]);
20278
+ return () => clearInterval(id);
20279
+ }, [items.length, intervalMs, scrollToIndex]);
20280
+ React20.useEffect(
20281
+ () => () => {
20282
+ if (rafRef.current) cancelAnimationFrame(rafRef.current);
20283
+ if (interactTimer.current) clearTimeout(interactTimer.current);
20284
+ if (programmaticTimer.current) clearTimeout(programmaticTimer.current);
20285
+ },
20286
+ []
20287
+ );
20251
20288
  if (!items.length) return null;
20252
- const safe = Math.min(index, items.length - 1);
20253
- const review = items[safe];
20289
+ const active = Math.min(index, items.length - 1);
20254
20290
  return /* @__PURE__ */ jsx(
20255
20291
  "section",
20256
20292
  {
20257
20293
  className: cn(SURFACE_PRIMARY_9002, "py-24 text-white sm:py-32", className),
20294
+ onMouseEnter: () => {
20295
+ hoverRef.current = true;
20296
+ },
20297
+ onMouseLeave: () => {
20298
+ hoverRef.current = false;
20299
+ },
20300
+ onFocus: () => {
20301
+ focusRef.current = true;
20302
+ },
20303
+ onBlur: () => {
20304
+ focusRef.current = false;
20305
+ },
20258
20306
  children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-3xl px-6 text-center sm:px-8", children: [
20259
20307
  eyebrow && /* @__PURE__ */ jsx("p", { className: "font-ui text-xs font-bold uppercase tracking-[0.22em] text-primary", children: eyebrow }),
20260
20308
  /* @__PURE__ */ jsx("h2", { className: "sr-only", children: title }),
20261
- /* @__PURE__ */ jsxs(
20309
+ /* @__PURE__ */ jsx(
20262
20310
  "div",
20263
20311
  {
20264
- className: cn(
20265
- "transition-opacity duration-300",
20266
- visible ? "opacity-100" : "opacity-0"
20267
- ),
20268
- children: [
20269
- /* @__PURE__ */ jsx("div", { className: "mt-8 flex justify-center", children: /* @__PURE__ */ jsx(TrustpilotStars, { stars: review.stars, className: "h-7 w-7" }) }),
20270
- /* @__PURE__ */ jsxs("blockquote", { className: "mx-auto mt-8 max-w-[20ch] font-sans text-3xl font-normal italic leading-[1.3] text-white sm:text-4xl", children: [
20271
- "\u201C",
20272
- review.quote,
20273
- "\u201D"
20274
- ] }),
20275
- (review.author || review.location) && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
20276
- review.author && /* @__PURE__ */ jsx("p", { className: "font-ui text-base font-bold tracking-wide text-white", children: review.author }),
20277
- review.location && /* @__PURE__ */ jsx("p", { className: "mt-1 font-sans text-sm text-white/55", children: review.location })
20278
- ] })
20279
- ]
20312
+ ref: trackRef,
20313
+ onScroll,
20314
+ "aria-roledescription": "carousel",
20315
+ "aria-label": title,
20316
+ className: "flex snap-x snap-mandatory overflow-x-auto scrollbar-none",
20317
+ children: items.map((review, i) => /* @__PURE__ */ jsxs(
20318
+ "div",
20319
+ {
20320
+ role: "group",
20321
+ "aria-roledescription": "slide",
20322
+ "aria-label": `Review ${i + 1} of ${items.length}`,
20323
+ className: "w-full shrink-0 snap-start",
20324
+ children: [
20325
+ /* @__PURE__ */ jsx("div", { className: "mt-8 flex justify-center", children: /* @__PURE__ */ jsx(TrustpilotStars, { stars: review.stars, className: "h-7 w-7" }) }),
20326
+ /* @__PURE__ */ jsxs("blockquote", { className: "mx-auto mt-8 flex min-h-[15rem] max-w-[20ch] items-center justify-center font-sans text-3xl font-normal italic leading-[1.3] text-white sm:min-h-[18rem] sm:text-4xl", children: [
20327
+ "\u201C",
20328
+ review.quote,
20329
+ "\u201D"
20330
+ ] }),
20331
+ (review.author || review.location) && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
20332
+ review.author && /* @__PURE__ */ jsx("p", { className: "font-ui text-base font-bold tracking-wide text-white", children: review.author }),
20333
+ review.location && /* @__PURE__ */ jsx("p", { className: "mt-1 font-sans text-sm text-white/55", children: review.location })
20334
+ ] })
20335
+ ]
20336
+ },
20337
+ i
20338
+ ))
20280
20339
  }
20281
20340
  ),
20282
20341
  items.length > 1 && /* @__PURE__ */ jsx("div", { className: "mt-10 flex justify-center gap-2.5", children: items.map((_, i) => /* @__PURE__ */ jsx(
@@ -20284,11 +20343,11 @@ function ReviewsSpotlight({
20284
20343
  {
20285
20344
  type: "button",
20286
20345
  "aria-label": `Show review ${i + 1} of ${items.length}`,
20287
- "aria-current": i === safe,
20288
- onClick: () => goTo(() => i),
20346
+ "aria-current": i === active,
20347
+ onClick: () => scrollToIndex(i),
20289
20348
  className: cn(
20290
20349
  "h-2 w-2 rounded-full transition-colors",
20291
- i === safe ? "bg-primary" : "bg-white/25 hover:bg-white/45"
20350
+ i === active ? "bg-primary" : "bg-white/25 hover:bg-white/45"
20292
20351
  )
20293
20352
  },
20294
20353
  i