@planetaexo/design-system 0.93.0 → 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
@@ -4174,6 +4174,14 @@ interface CategoryPage2Props {
4174
4174
  title: string;
4175
4175
  intro?: React.ReactNode;
4176
4176
  heroImage?: string;
4177
+ /**
4178
+ * Responsive variants of `heroImage` for the LCP `<img>` — same
4179
+ * `srcSet`/`sizes` contract as `TripHeader`, so phones fetch a viewport-
4180
+ * sized cover instead of the full-width desktop one. Optional; the `<img>`
4181
+ * falls back to the single `heroImage` src when they are absent.
4182
+ */
4183
+ heroImageSrcSet?: string;
4184
+ heroImageSizes?: string;
4177
4185
  /**
4178
4186
  * Optional hero background video. Plays muted/looping over the hero with
4179
4187
  * `heroImage` as the instant poster (crossfades out once the video is ready).
@@ -4280,7 +4288,7 @@ interface CategoryPage2Props {
4280
4288
  filterLabels?: FilterPanelLabels;
4281
4289
  className?: string;
4282
4290
  }
4283
- declare function CategoryPage2({ title, intro, heroImage, videoUrl, trustpilotMini, breadcrumb, siteHeader, heroRightSlot, popularTours, popularToursTitle, popularToursEyebrow, trips, tripsTitle, tripsEyebrow, filterGroups, sortOptions, defaultSort, tripsInitialCount, tripListingSlot, trustpilot, reviewsTitle, reviewsSubtitle, blogPosts, aboutTitle, aboutContent, blogIntro, travelGuideHref, travelGuideLabel, blogPostsTitle, blogPostsViewAllHref, faqs, faqsTitle, faqInitialCount, gallery, galleryTitle, galleryLightbox, loadMoreLabel, showLessLabel, seeMoreLabel, viewAllPostsLabel, cardLabels, filterLabels, footerBadges, footer, className, }: CategoryPage2Props): react_jsx_runtime.JSX.Element;
4291
+ declare function CategoryPage2({ title, intro, heroImage, heroImageSrcSet, heroImageSizes, videoUrl, trustpilotMini, breadcrumb, siteHeader, heroRightSlot, popularTours, popularToursTitle, popularToursEyebrow, trips, tripsTitle, tripsEyebrow, filterGroups, sortOptions, defaultSort, tripsInitialCount, tripListingSlot, trustpilot, reviewsTitle, reviewsSubtitle, blogPosts, aboutTitle, aboutContent, blogIntro, travelGuideHref, travelGuideLabel, blogPostsTitle, blogPostsViewAllHref, faqs, faqsTitle, faqInitialCount, gallery, galleryTitle, galleryLightbox, loadMoreLabel, showLessLabel, seeMoreLabel, viewAllPostsLabel, cardLabels, filterLabels, footerBadges, footer, className, }: CategoryPage2Props): react_jsx_runtime.JSX.Element;
4284
4292
 
4285
4293
  type ActivityCardSize = "sm" | "md" | "lg";
4286
4294
  interface ActivityCardProps {
@@ -5421,10 +5429,13 @@ interface ReviewsSpotlightProps {
5421
5429
  className?: string;
5422
5430
  }
5423
5431
  /**
5424
- * ReviewsSpotlight — a single rotating testimonial. One large serif quote with
5425
- * Trustpilot stars, auto-advancing through `items` with clickable dots and a
5426
- * short crossfade. Honours `prefers-reduced-motion` (no auto-advance, instant
5427
- * 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}.
5428
5439
  */
5429
5440
  declare function ReviewsSpotlight({ eyebrow, title, link, items, intervalMs, className, }: ReviewsSpotlightProps): react_jsx_runtime.JSX.Element | null;
5430
5441
 
package/dist/index.d.ts CHANGED
@@ -4174,6 +4174,14 @@ interface CategoryPage2Props {
4174
4174
  title: string;
4175
4175
  intro?: React.ReactNode;
4176
4176
  heroImage?: string;
4177
+ /**
4178
+ * Responsive variants of `heroImage` for the LCP `<img>` — same
4179
+ * `srcSet`/`sizes` contract as `TripHeader`, so phones fetch a viewport-
4180
+ * sized cover instead of the full-width desktop one. Optional; the `<img>`
4181
+ * falls back to the single `heroImage` src when they are absent.
4182
+ */
4183
+ heroImageSrcSet?: string;
4184
+ heroImageSizes?: string;
4177
4185
  /**
4178
4186
  * Optional hero background video. Plays muted/looping over the hero with
4179
4187
  * `heroImage` as the instant poster (crossfades out once the video is ready).
@@ -4280,7 +4288,7 @@ interface CategoryPage2Props {
4280
4288
  filterLabels?: FilterPanelLabels;
4281
4289
  className?: string;
4282
4290
  }
4283
- declare function CategoryPage2({ title, intro, heroImage, videoUrl, trustpilotMini, breadcrumb, siteHeader, heroRightSlot, popularTours, popularToursTitle, popularToursEyebrow, trips, tripsTitle, tripsEyebrow, filterGroups, sortOptions, defaultSort, tripsInitialCount, tripListingSlot, trustpilot, reviewsTitle, reviewsSubtitle, blogPosts, aboutTitle, aboutContent, blogIntro, travelGuideHref, travelGuideLabel, blogPostsTitle, blogPostsViewAllHref, faqs, faqsTitle, faqInitialCount, gallery, galleryTitle, galleryLightbox, loadMoreLabel, showLessLabel, seeMoreLabel, viewAllPostsLabel, cardLabels, filterLabels, footerBadges, footer, className, }: CategoryPage2Props): react_jsx_runtime.JSX.Element;
4291
+ declare function CategoryPage2({ title, intro, heroImage, heroImageSrcSet, heroImageSizes, videoUrl, trustpilotMini, breadcrumb, siteHeader, heroRightSlot, popularTours, popularToursTitle, popularToursEyebrow, trips, tripsTitle, tripsEyebrow, filterGroups, sortOptions, defaultSort, tripsInitialCount, tripListingSlot, trustpilot, reviewsTitle, reviewsSubtitle, blogPosts, aboutTitle, aboutContent, blogIntro, travelGuideHref, travelGuideLabel, blogPostsTitle, blogPostsViewAllHref, faqs, faqsTitle, faqInitialCount, gallery, galleryTitle, galleryLightbox, loadMoreLabel, showLessLabel, seeMoreLabel, viewAllPostsLabel, cardLabels, filterLabels, footerBadges, footer, className, }: CategoryPage2Props): react_jsx_runtime.JSX.Element;
4284
4292
 
4285
4293
  type ActivityCardSize = "sm" | "md" | "lg";
4286
4294
  interface ActivityCardProps {
@@ -5421,10 +5429,13 @@ interface ReviewsSpotlightProps {
5421
5429
  className?: string;
5422
5430
  }
5423
5431
  /**
5424
- * ReviewsSpotlight — a single rotating testimonial. One large serif quote with
5425
- * Trustpilot stars, auto-advancing through `items` with clickable dots and a
5426
- * short crossfade. Honours `prefers-reduced-motion` (no auto-advance, instant
5427
- * 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}.
5428
5439
  */
5429
5440
  declare function ReviewsSpotlight({ eyebrow, title, link, items, intervalMs, className, }: ReviewsSpotlightProps): react_jsx_runtime.JSX.Element | null;
5430
5441
 
package/dist/index.js CHANGED
@@ -16430,6 +16430,8 @@ function CategoryPage2({
16430
16430
  title,
16431
16431
  intro,
16432
16432
  heroImage,
16433
+ heroImageSrcSet,
16434
+ heroImageSizes,
16433
16435
  videoUrl,
16434
16436
  trustpilotMini,
16435
16437
  breadcrumb,
@@ -16565,6 +16567,8 @@ function CategoryPage2({
16565
16567
  "img",
16566
16568
  {
16567
16569
  src: heroImage,
16570
+ srcSet: heroImageSrcSet,
16571
+ sizes: heroImageSizes,
16568
16572
  alt: "",
16569
16573
  "aria-hidden": true,
16570
16574
  fetchPriority: "high",
@@ -20206,10 +20210,18 @@ function ReviewsSpotlight({
20206
20210
  intervalMs = 7e3,
20207
20211
  className
20208
20212
  }) {
20213
+ const trackRef = React20.useRef(null);
20209
20214
  const [index, setIndex] = React20.useState(0);
20210
- const [visible, setVisible] = React20.useState(true);
20211
20215
  const reduced = React20.useRef(false);
20212
- 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
+ );
20213
20225
  React20.useEffect(() => {
20214
20226
  var _a;
20215
20227
  const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
@@ -20223,56 +20235,107 @@ function ReviewsSpotlight({
20223
20235
  return (_a2 = mq.removeEventListener) == null ? void 0 : _a2.call(mq, "change", onChange);
20224
20236
  };
20225
20237
  }, []);
20226
- const goTo = React20.useCallback((resolve) => {
20227
- if (reduced.current) {
20228
- setIndex((i) => resolve(i));
20229
- 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);
20230
20266
  }
20231
- setVisible(false);
20232
- swapRef.current = setTimeout(() => {
20233
- setIndex((i) => resolve(i));
20234
- setVisible(true);
20235
- }, 260);
20236
20267
  }, []);
20237
20268
  React20.useEffect(() => {
20238
20269
  if (items.length <= 1 || intervalMs <= 0) return;
20239
20270
  const id = setInterval(() => {
20240
- 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);
20241
20277
  }, intervalMs);
20242
- return () => {
20243
- clearInterval(id);
20244
- if (swapRef.current) clearTimeout(swapRef.current);
20245
- };
20246
- }, [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
+ );
20247
20288
  if (!items.length) return null;
20248
- const safe = Math.min(index, items.length - 1);
20249
- const review = items[safe];
20289
+ const active = Math.min(index, items.length - 1);
20250
20290
  return /* @__PURE__ */ jsx(
20251
20291
  "section",
20252
20292
  {
20253
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
+ },
20254
20306
  children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-3xl px-6 text-center sm:px-8", children: [
20255
20307
  eyebrow && /* @__PURE__ */ jsx("p", { className: "font-ui text-xs font-bold uppercase tracking-[0.22em] text-primary", children: eyebrow }),
20256
20308
  /* @__PURE__ */ jsx("h2", { className: "sr-only", children: title }),
20257
- /* @__PURE__ */ jsxs(
20309
+ /* @__PURE__ */ jsx(
20258
20310
  "div",
20259
20311
  {
20260
- className: cn(
20261
- "transition-opacity duration-300",
20262
- visible ? "opacity-100" : "opacity-0"
20263
- ),
20264
- children: [
20265
- /* @__PURE__ */ jsx("div", { className: "mt-8 flex justify-center", children: /* @__PURE__ */ jsx(TrustpilotStars, { stars: review.stars, className: "h-7 w-7" }) }),
20266
- /* @__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: [
20267
- "\u201C",
20268
- review.quote,
20269
- "\u201D"
20270
- ] }),
20271
- (review.author || review.location) && /* @__PURE__ */ jsxs("div", { className: "mt-6", children: [
20272
- review.author && /* @__PURE__ */ jsx("p", { className: "font-ui text-base font-bold tracking-wide text-white", children: review.author }),
20273
- review.location && /* @__PURE__ */ jsx("p", { className: "mt-1 font-sans text-sm text-white/55", children: review.location })
20274
- ] })
20275
- ]
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
+ ))
20276
20339
  }
20277
20340
  ),
20278
20341
  items.length > 1 && /* @__PURE__ */ jsx("div", { className: "mt-10 flex justify-center gap-2.5", children: items.map((_, i) => /* @__PURE__ */ jsx(
@@ -20280,11 +20343,11 @@ function ReviewsSpotlight({
20280
20343
  {
20281
20344
  type: "button",
20282
20345
  "aria-label": `Show review ${i + 1} of ${items.length}`,
20283
- "aria-current": i === safe,
20284
- onClick: () => goTo(() => i),
20346
+ "aria-current": i === active,
20347
+ onClick: () => scrollToIndex(i),
20285
20348
  className: cn(
20286
20349
  "h-2 w-2 rounded-full transition-colors",
20287
- i === safe ? "bg-primary" : "bg-white/25 hover:bg-white/45"
20350
+ i === active ? "bg-primary" : "bg-white/25 hover:bg-white/45"
20288
20351
  )
20289
20352
  },
20290
20353
  i