@ship-it-ui/ui 0.0.11 → 0.0.13

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
@@ -1242,11 +1242,21 @@ interface CarouselProps<T = unknown> extends Omit<HTMLAttributes<HTMLDivElement>
1242
1242
  showArrows?: boolean;
1243
1243
  /**
1244
1244
  * Wrap arrows / dots / native swipe past the boundaries. Default `false`.
1245
- * When `true`, hidden clones of the first / last slide bracket the real
1246
- * items so swipe past the end jumps invisibly to the other side.
1247
- * `onIndexChange` still only emits real indices in `0..items.length - 1`.
1245
+ *
1246
+ * Variants:
1247
+ * - `"circular"` (or `true`): boundary arrow clicks smooth-scroll a
1248
+ * single slide width through a hidden clone of the opposite end, then
1249
+ * invisibly snap to the real twin. Feels like an endless reel — the
1250
+ * motion is always one slide, regardless of strip length.
1251
+ * - `"sweep"`: boundary arrow clicks smooth-scroll the full distance
1252
+ * across the strip back to the real first / last slide. The
1253
+ * transition reads as a wide arc across every item between.
1254
+ *
1255
+ * Native swipe past the edge always uses the clone-snap (independent of
1256
+ * variant). `onIndexChange` only emits real indices in
1257
+ * `0..items.length - 1`.
1248
1258
  */
1249
- loop?: boolean;
1259
+ loop?: boolean | 'circular' | 'sweep';
1250
1260
  /** Accessible label for the carousel region. */
1251
1261
  'aria-label'?: string;
1252
1262
  }
@@ -1583,13 +1593,13 @@ interface ListingCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'childre
1583
1593
  * browsing expects looping. Pass `false` to restore stop-at-end.
1584
1594
  */
1585
1595
  loop?: boolean;
1586
- /** Listing title — e.g. "2023 Tesla Model 3". */
1596
+ /** Listing title — e.g. "Sun-soaked cabin in Marin". */
1587
1597
  title: ReactNode;
1588
- /** Optional eyebrow text above the title (location, vehicle type). */
1598
+ /** Optional eyebrow text above the title (location, listing type). */
1589
1599
  eyebrow?: ReactNode;
1590
- /** Headline price (e.g. `89`). */
1600
+ /** Headline price (e.g. `189`). */
1591
1601
  price: ReactNode;
1592
- /** Price unit suffix (e.g. `/day`). */
1602
+ /** Price unit suffix (e.g. `/night`). */
1593
1603
  priceUnit?: ReactNode;
1594
1604
  /** Original price for sale strike-through. */
1595
1605
  originalPrice?: ReactNode;
@@ -1681,7 +1691,7 @@ interface ListingCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'childre
1681
1691
  footer: string;
1682
1692
  /** Price text. */
1683
1693
  price: string;
1684
- /** Price unit (e.g. `/day`). */
1694
+ /** Price unit (e.g. `/night`). */
1685
1695
  priceUnit: string;
1686
1696
  /** CTA button (spec variant). */
1687
1697
  cta: string;
@@ -1744,9 +1754,9 @@ interface ListingDetailProps {
1744
1754
  * drives both surfaces.
1745
1755
  */
1746
1756
  loop?: boolean;
1747
- /** Listing title — e.g. "2023 Tesla Model 3". */
1757
+ /** Listing title — e.g. "Sun-soaked cabin in Marin". */
1748
1758
  title: ReactNode;
1749
- /** Optional eyebrow above the title — vehicle type, location. */
1759
+ /** Optional eyebrow above the title — listing type, location. */
1750
1760
  eyebrow?: ReactNode;
1751
1761
  /** Long-form description body. */
1752
1762
  description?: ReactNode;
@@ -1754,9 +1764,9 @@ interface ListingDetailProps {
1754
1764
  rating?: number;
1755
1765
  /** Total review count, shown next to the rating. */
1756
1766
  reviewCount?: number;
1757
- /** Headline price (e.g. `$89`). */
1767
+ /** Headline price (e.g. `$189`). */
1758
1768
  price: ReactNode;
1759
- /** Suffix after the price (e.g. `/day`). */
1769
+ /** Suffix after the price (e.g. `/night`). */
1760
1770
  priceUnit?: ReactNode;
1761
1771
  /** Original price for a strike-through; renders only when set. */
1762
1772
  originalPrice?: ReactNode;
package/dist/index.d.ts CHANGED
@@ -1242,11 +1242,21 @@ interface CarouselProps<T = unknown> extends Omit<HTMLAttributes<HTMLDivElement>
1242
1242
  showArrows?: boolean;
1243
1243
  /**
1244
1244
  * Wrap arrows / dots / native swipe past the boundaries. Default `false`.
1245
- * When `true`, hidden clones of the first / last slide bracket the real
1246
- * items so swipe past the end jumps invisibly to the other side.
1247
- * `onIndexChange` still only emits real indices in `0..items.length - 1`.
1245
+ *
1246
+ * Variants:
1247
+ * - `"circular"` (or `true`): boundary arrow clicks smooth-scroll a
1248
+ * single slide width through a hidden clone of the opposite end, then
1249
+ * invisibly snap to the real twin. Feels like an endless reel — the
1250
+ * motion is always one slide, regardless of strip length.
1251
+ * - `"sweep"`: boundary arrow clicks smooth-scroll the full distance
1252
+ * across the strip back to the real first / last slide. The
1253
+ * transition reads as a wide arc across every item between.
1254
+ *
1255
+ * Native swipe past the edge always uses the clone-snap (independent of
1256
+ * variant). `onIndexChange` only emits real indices in
1257
+ * `0..items.length - 1`.
1248
1258
  */
1249
- loop?: boolean;
1259
+ loop?: boolean | 'circular' | 'sweep';
1250
1260
  /** Accessible label for the carousel region. */
1251
1261
  'aria-label'?: string;
1252
1262
  }
@@ -1583,13 +1593,13 @@ interface ListingCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'childre
1583
1593
  * browsing expects looping. Pass `false` to restore stop-at-end.
1584
1594
  */
1585
1595
  loop?: boolean;
1586
- /** Listing title — e.g. "2023 Tesla Model 3". */
1596
+ /** Listing title — e.g. "Sun-soaked cabin in Marin". */
1587
1597
  title: ReactNode;
1588
- /** Optional eyebrow text above the title (location, vehicle type). */
1598
+ /** Optional eyebrow text above the title (location, listing type). */
1589
1599
  eyebrow?: ReactNode;
1590
- /** Headline price (e.g. `89`). */
1600
+ /** Headline price (e.g. `189`). */
1591
1601
  price: ReactNode;
1592
- /** Price unit suffix (e.g. `/day`). */
1602
+ /** Price unit suffix (e.g. `/night`). */
1593
1603
  priceUnit?: ReactNode;
1594
1604
  /** Original price for sale strike-through. */
1595
1605
  originalPrice?: ReactNode;
@@ -1681,7 +1691,7 @@ interface ListingCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'childre
1681
1691
  footer: string;
1682
1692
  /** Price text. */
1683
1693
  price: string;
1684
- /** Price unit (e.g. `/day`). */
1694
+ /** Price unit (e.g. `/night`). */
1685
1695
  priceUnit: string;
1686
1696
  /** CTA button (spec variant). */
1687
1697
  cta: string;
@@ -1744,9 +1754,9 @@ interface ListingDetailProps {
1744
1754
  * drives both surfaces.
1745
1755
  */
1746
1756
  loop?: boolean;
1747
- /** Listing title — e.g. "2023 Tesla Model 3". */
1757
+ /** Listing title — e.g. "Sun-soaked cabin in Marin". */
1748
1758
  title: ReactNode;
1749
- /** Optional eyebrow above the title — vehicle type, location. */
1759
+ /** Optional eyebrow above the title — listing type, location. */
1750
1760
  eyebrow?: ReactNode;
1751
1761
  /** Long-form description body. */
1752
1762
  description?: ReactNode;
@@ -1754,9 +1764,9 @@ interface ListingDetailProps {
1754
1764
  rating?: number;
1755
1765
  /** Total review count, shown next to the rating. */
1756
1766
  reviewCount?: number;
1757
- /** Headline price (e.g. `$89`). */
1767
+ /** Headline price (e.g. `$189`). */
1758
1768
  price: ReactNode;
1759
- /** Suffix after the price (e.g. `/day`). */
1769
+ /** Suffix after the price (e.g. `/night`). */
1760
1770
  priceUnit?: ReactNode;
1761
1771
  /** Original price for a strike-through; renders only when set. */
1762
1772
  originalPrice?: ReactNode;
package/dist/index.js CHANGED
@@ -3100,7 +3100,8 @@ var Carousel = forwardRef44(function Carousel2({
3100
3100
  ...props
3101
3101
  }, ref) {
3102
3102
  const N = items.length;
3103
- const isLooping = loop && N > 1;
3103
+ const loopMode = !loop ? null : loop === true ? "circular" : loop;
3104
+ const isLooping = loopMode !== null && N > 1;
3104
3105
  const [active, setActive] = useControllableState({
3105
3106
  value: indexProp,
3106
3107
  defaultValue: defaultIndex ?? 0,
@@ -3108,6 +3109,8 @@ var Carousel = forwardRef44(function Carousel2({
3108
3109
  });
3109
3110
  const viewportRef = useRef8(null);
3110
3111
  const internalScrollRef = useRef8(false);
3112
+ const goToInProgressRef = useRef8(false);
3113
+ const wrapInFlightRef = useRef8(null);
3111
3114
  const activeIdx = active ?? 0;
3112
3115
  const domIndexFor = useCallback9((real) => isLooping ? real + 1 : real, [isLooping]);
3113
3116
  const goTo = useCallback9(
@@ -3116,14 +3119,36 @@ var Carousel = forwardRef44(function Carousel2({
3116
3119
  setActive(next);
3117
3120
  const node = viewportRef.current;
3118
3121
  if (node) {
3119
- const slide = node.children[domIndexFor(next)];
3122
+ const width = node.clientWidth;
3123
+ if (isLooping && wrapInFlightRef.current !== null && width > 0) {
3124
+ const rebaseTarget = wrapInFlightRef.current === N + 1 ? 0 : wrapInFlightRef.current === 0 ? N + 1 : null;
3125
+ if (rebaseTarget !== null) {
3126
+ const rebaseSlide = node.children[rebaseTarget];
3127
+ if (rebaseSlide) {
3128
+ internalScrollRef.current = true;
3129
+ rebaseSlide.scrollIntoView({
3130
+ behavior: "instant",
3131
+ block: "nearest",
3132
+ inline: "start"
3133
+ });
3134
+ }
3135
+ }
3136
+ wrapInFlightRef.current = null;
3137
+ }
3138
+ const isNextWrap = loopMode === "circular" && activeIdx === N - 1 && i === activeIdx + 1;
3139
+ const isPrevWrap = loopMode === "circular" && activeIdx === 0 && i === activeIdx - 1;
3140
+ const targetDom = isNextWrap ? N + 1 : isPrevWrap ? 0 : domIndexFor(next);
3141
+ const slide = node.children[targetDom];
3120
3142
  if (slide) {
3121
3143
  internalScrollRef.current = true;
3144
+ goToInProgressRef.current = true;
3145
+ if (isNextWrap) wrapInFlightRef.current = N + 1;
3146
+ else if (isPrevWrap) wrapInFlightRef.current = 0;
3122
3147
  slide.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
3123
3148
  }
3124
3149
  }
3125
3150
  },
3126
- [N, isLooping, domIndexFor, setActive]
3151
+ [N, isLooping, loopMode, domIndexFor, setActive, activeIdx]
3127
3152
  );
3128
3153
  useEffect8(() => {
3129
3154
  const node = viewportRef.current;
@@ -3133,32 +3158,54 @@ var Carousel = forwardRef44(function Carousel2({
3133
3158
  if (width === 0) return;
3134
3159
  const domIdx = Math.round(node.scrollLeft / width);
3135
3160
  if (!isLooping) {
3161
+ if (goToInProgressRef.current) {
3162
+ if (domIdx === activeIdx) goToInProgressRef.current = false;
3163
+ return;
3164
+ }
3136
3165
  if (domIdx !== activeIdx) setActive(domIdx);
3137
3166
  return;
3138
3167
  }
3139
3168
  if (domIdx === 0) {
3169
+ if (goToInProgressRef.current && node.scrollLeft > 1) return;
3140
3170
  const realTwin = node.children[N];
3141
3171
  if (realTwin) {
3142
3172
  internalScrollRef.current = true;
3143
3173
  realTwin.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3144
3174
  }
3145
3175
  if (activeIdx !== N - 1) setActive(N - 1);
3176
+ goToInProgressRef.current = false;
3177
+ wrapInFlightRef.current = null;
3146
3178
  return;
3147
3179
  }
3148
3180
  if (domIdx === N + 1) {
3181
+ if (goToInProgressRef.current && node.scrollLeft < (N + 1) * width - 1) return;
3149
3182
  const realTwin = node.children[1];
3150
3183
  if (realTwin) {
3151
3184
  internalScrollRef.current = true;
3152
3185
  realTwin.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3153
3186
  }
3154
3187
  if (activeIdx !== 0) setActive(0);
3188
+ goToInProgressRef.current = false;
3189
+ wrapInFlightRef.current = null;
3155
3190
  return;
3156
3191
  }
3157
3192
  const realIdx = domIdx - 1;
3193
+ if (goToInProgressRef.current) {
3194
+ if (realIdx === activeIdx) goToInProgressRef.current = false;
3195
+ return;
3196
+ }
3158
3197
  if (realIdx !== activeIdx) setActive(realIdx);
3159
3198
  };
3199
+ const onPointerDown = () => {
3200
+ goToInProgressRef.current = false;
3201
+ wrapInFlightRef.current = null;
3202
+ };
3160
3203
  node.addEventListener("scroll", onScroll, { passive: true });
3161
- return () => node.removeEventListener("scroll", onScroll);
3204
+ node.addEventListener("pointerdown", onPointerDown, { passive: true });
3205
+ return () => {
3206
+ node.removeEventListener("scroll", onScroll);
3207
+ node.removeEventListener("pointerdown", onPointerDown);
3208
+ };
3162
3209
  }, [activeIdx, isLooping, N, setActive]);
3163
3210
  useEffect8(() => {
3164
3211
  if (internalScrollRef.current) {
@@ -3294,19 +3341,32 @@ var Carousel = forwardRef44(function Carousel2({
3294
3341
  }
3295
3342
  )
3296
3343
  ] }),
3297
- renderThumbnail && /* @__PURE__ */ jsx45("div", { className: "mt-2 flex gap-2 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: items.map((item, i) => /* @__PURE__ */ jsx45(
3298
- "button",
3299
- {
3300
- type: "button",
3301
- "aria-label": `Show slide ${i + 1}`,
3302
- onClick: () => goTo(i),
3303
- className: cn(
3304
- "shrink-0 cursor-pointer overflow-hidden rounded transition-opacity",
3305
- i === activeIdx ? "ring-accent opacity-100 ring-2" : "opacity-60 hover:opacity-100"
3306
- ),
3307
- children: renderThumbnail(item, i)
3308
- },
3309
- i
3344
+ renderThumbnail && /* @__PURE__ */ jsx45("div", { className: "-mx-0.5 mt-1.5 flex gap-2 overflow-x-auto p-0.5 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", children: items.map((item, i) => (
3345
+ // The active ring is applied to the rendered thumbnail (the
3346
+ // button's first child) rather than the button itself, so it
3347
+ // traces whatever border-radius the consumer's thumbnail
3348
+ // already has. Picking a fixed radius here would always be
3349
+ // wrong for one consumer or another — `rounded-lg` thumbs got
3350
+ // a 4px-radius ring; future thumbs could be circular or
3351
+ // square. `box-shadow` (what `ring-2` compiles to) follows
3352
+ // the child's `border-radius` automatically, so this is
3353
+ // self-adjusting.
3354
+ /* @__PURE__ */ jsx45(
3355
+ "button",
3356
+ {
3357
+ type: "button",
3358
+ "aria-label": `Show slide ${i + 1}`,
3359
+ onClick: () => goTo(i),
3360
+ "data-active": i === activeIdx ? "true" : void 0,
3361
+ className: cn(
3362
+ "shrink-0 cursor-pointer transition-opacity",
3363
+ "[&[data-active]>*]:ring-accent [&[data-active]>*]:ring-2",
3364
+ i === activeIdx ? "opacity-100" : "opacity-60 hover:opacity-100"
3365
+ ),
3366
+ children: renderThumbnail(item, i)
3367
+ },
3368
+ i
3369
+ )
3310
3370
  )) })
3311
3371
  ]
3312
3372
  }