@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.cjs +77 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -13
- package/dist/index.d.ts +23 -13
- package/dist/index.js +77 -17
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
* `
|
|
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. "
|
|
1596
|
+
/** Listing title — e.g. "Sun-soaked cabin in Marin". */
|
|
1587
1597
|
title: ReactNode;
|
|
1588
|
-
/** Optional eyebrow text above the title (location,
|
|
1598
|
+
/** Optional eyebrow text above the title (location, listing type). */
|
|
1589
1599
|
eyebrow?: ReactNode;
|
|
1590
|
-
/** Headline price (e.g. `
|
|
1600
|
+
/** Headline price (e.g. `189`). */
|
|
1591
1601
|
price: ReactNode;
|
|
1592
|
-
/** Price unit suffix (e.g. `/
|
|
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. `/
|
|
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. "
|
|
1757
|
+
/** Listing title — e.g. "Sun-soaked cabin in Marin". */
|
|
1748
1758
|
title: ReactNode;
|
|
1749
|
-
/** Optional eyebrow above the title —
|
|
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. `$
|
|
1767
|
+
/** Headline price (e.g. `$189`). */
|
|
1758
1768
|
price: ReactNode;
|
|
1759
|
-
/** Suffix after the price (e.g. `/
|
|
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
|
-
*
|
|
1246
|
-
*
|
|
1247
|
-
* `
|
|
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. "
|
|
1596
|
+
/** Listing title — e.g. "Sun-soaked cabin in Marin". */
|
|
1587
1597
|
title: ReactNode;
|
|
1588
|
-
/** Optional eyebrow text above the title (location,
|
|
1598
|
+
/** Optional eyebrow text above the title (location, listing type). */
|
|
1589
1599
|
eyebrow?: ReactNode;
|
|
1590
|
-
/** Headline price (e.g. `
|
|
1600
|
+
/** Headline price (e.g. `189`). */
|
|
1591
1601
|
price: ReactNode;
|
|
1592
|
-
/** Price unit suffix (e.g. `/
|
|
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. `/
|
|
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. "
|
|
1757
|
+
/** Listing title — e.g. "Sun-soaked cabin in Marin". */
|
|
1748
1758
|
title: ReactNode;
|
|
1749
|
-
/** Optional eyebrow above the title —
|
|
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. `$
|
|
1767
|
+
/** Headline price (e.g. `$189`). */
|
|
1758
1768
|
price: ReactNode;
|
|
1759
|
-
/** Suffix after the price (e.g. `/
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
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
|
}
|