@ship-it-ui/ui 0.0.10 → 0.0.12

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 CHANGED
@@ -3251,27 +3251,42 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3251
3251
  aspectRatio = 16 / 10,
3252
3252
  showDots = true,
3253
3253
  showArrows = true,
3254
+ loop = false,
3254
3255
  className,
3255
3256
  "aria-label": ariaLabel = "Carousel",
3256
3257
  ...props
3257
3258
  }, ref) {
3259
+ const N = items.length;
3260
+ const loopMode = !loop ? null : loop === true ? "circular" : loop;
3261
+ const isLooping = loopMode !== null && N > 1;
3258
3262
  const [active, setActive] = useControllableState({
3259
3263
  value: indexProp,
3260
3264
  defaultValue: defaultIndex ?? 0,
3261
3265
  onChange: onIndexChange
3262
3266
  });
3263
3267
  const viewportRef = (0, import_react52.useRef)(null);
3268
+ const internalScrollRef = (0, import_react52.useRef)(false);
3269
+ const wrapInProgressRef = (0, import_react52.useRef)(false);
3270
+ const activeIdx = active ?? 0;
3271
+ const domIndexFor = (0, import_react52.useCallback)((real) => isLooping ? real + 1 : real, [isLooping]);
3264
3272
  const goTo = (0, import_react52.useCallback)(
3265
3273
  (i) => {
3266
- const clamped = Math.max(0, Math.min(items.length - 1, i));
3267
- setActive(clamped);
3274
+ const next = isLooping ? (i % N + N) % N : Math.max(0, Math.min(N - 1, i));
3275
+ setActive(next);
3268
3276
  const node = viewportRef.current;
3269
3277
  if (node) {
3270
- const slide = node.children[clamped];
3271
- slide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
3278
+ const isNextWrap = loopMode === "circular" && activeIdx === N - 1 && i === activeIdx + 1;
3279
+ const isPrevWrap = loopMode === "circular" && activeIdx === 0 && i === activeIdx - 1;
3280
+ const targetDom = isNextWrap ? N + 1 : isPrevWrap ? 0 : domIndexFor(next);
3281
+ const slide = node.children[targetDom];
3282
+ if (slide) {
3283
+ internalScrollRef.current = true;
3284
+ if (isNextWrap || isPrevWrap) wrapInProgressRef.current = true;
3285
+ slide.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
3286
+ }
3272
3287
  }
3273
3288
  },
3274
- [items.length, setActive]
3289
+ [N, isLooping, loopMode, domIndexFor, setActive, activeIdx]
3275
3290
  );
3276
3291
  (0, import_react52.useEffect)(() => {
3277
3292
  const node = viewportRef.current;
@@ -3279,13 +3294,64 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3279
3294
  const onScroll = () => {
3280
3295
  const width = node.clientWidth;
3281
3296
  if (width === 0) return;
3282
- const i = Math.round(node.scrollLeft / width);
3283
- if (i !== active) setActive(i);
3297
+ const domIdx = Math.round(node.scrollLeft / width);
3298
+ if (!isLooping) {
3299
+ if (domIdx !== activeIdx) setActive(domIdx);
3300
+ return;
3301
+ }
3302
+ if (domIdx === 0) {
3303
+ if (wrapInProgressRef.current && node.scrollLeft > 1) return;
3304
+ const realTwin = node.children[N];
3305
+ if (realTwin) {
3306
+ internalScrollRef.current = true;
3307
+ realTwin.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3308
+ }
3309
+ if (activeIdx !== N - 1) setActive(N - 1);
3310
+ wrapInProgressRef.current = false;
3311
+ return;
3312
+ }
3313
+ if (domIdx === N + 1) {
3314
+ if (wrapInProgressRef.current && node.scrollLeft < (N + 1) * width - 1) return;
3315
+ const realTwin = node.children[1];
3316
+ if (realTwin) {
3317
+ internalScrollRef.current = true;
3318
+ realTwin.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3319
+ }
3320
+ if (activeIdx !== 0) setActive(0);
3321
+ wrapInProgressRef.current = false;
3322
+ return;
3323
+ }
3324
+ if (wrapInProgressRef.current) return;
3325
+ const realIdx = domIdx - 1;
3326
+ if (realIdx !== activeIdx) setActive(realIdx);
3284
3327
  };
3285
3328
  node.addEventListener("scroll", onScroll, { passive: true });
3286
3329
  return () => node.removeEventListener("scroll", onScroll);
3287
- }, [active, setActive]);
3288
- const activeIdx = active ?? 0;
3330
+ }, [activeIdx, isLooping, N, setActive]);
3331
+ (0, import_react52.useEffect)(() => {
3332
+ if (internalScrollRef.current) {
3333
+ internalScrollRef.current = false;
3334
+ return;
3335
+ }
3336
+ const node = viewportRef.current;
3337
+ if (!node) return;
3338
+ const width = node.clientWidth;
3339
+ if (width === 0) return;
3340
+ const targetDom = domIndexFor(activeIdx);
3341
+ const currentDom = Math.round(node.scrollLeft / width);
3342
+ if (currentDom === targetDom) return;
3343
+ const slide = node.children[targetDom];
3344
+ slide?.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3345
+ }, [activeIdx, domIndexFor]);
3346
+ (0, import_react52.useLayoutEffect)(() => {
3347
+ if (!isLooping) return;
3348
+ const node = viewportRef.current;
3349
+ if (!node) return;
3350
+ const slide = node.children[domIndexFor(activeIdx)];
3351
+ if (!slide) return;
3352
+ internalScrollRef.current = true;
3353
+ slide.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3354
+ }, [isLooping]);
3289
3355
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3290
3356
  "div",
3291
3357
  {
@@ -3297,34 +3363,58 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3297
3363
  ...props,
3298
3364
  children: [
3299
3365
  /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "relative overflow-hidden rounded-md", children: [
3300
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3366
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3301
3367
  "div",
3302
3368
  {
3303
3369
  ref: viewportRef,
3304
3370
  className: "flex w-full snap-x snap-mandatory overflow-x-auto scroll-smooth [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
3305
3371
  "aria-live": "polite",
3306
- children: items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3307
- "div",
3308
- {
3309
- className: "w-full shrink-0 snap-start",
3310
- style: { aspectRatio: String(aspectRatio) },
3311
- role: "group",
3312
- "aria-roledescription": "slide",
3313
- "aria-label": `${i + 1} of ${items.length}`,
3314
- children: renderItem(item, i)
3315
- },
3316
- i
3317
- ))
3372
+ children: [
3373
+ isLooping && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3374
+ "div",
3375
+ {
3376
+ "aria-hidden": "true",
3377
+ tabIndex: -1,
3378
+ className: "w-full shrink-0 snap-start",
3379
+ style: { aspectRatio: String(aspectRatio) },
3380
+ children: renderItem(items[N - 1], N - 1)
3381
+ },
3382
+ "clone-start"
3383
+ ),
3384
+ items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3385
+ "div",
3386
+ {
3387
+ className: "w-full shrink-0 snap-start",
3388
+ style: { aspectRatio: String(aspectRatio) },
3389
+ role: "group",
3390
+ "aria-roledescription": "slide",
3391
+ "aria-label": `${i + 1} of ${N}`,
3392
+ children: renderItem(item, i)
3393
+ },
3394
+ i
3395
+ )),
3396
+ isLooping && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3397
+ "div",
3398
+ {
3399
+ "aria-hidden": "true",
3400
+ tabIndex: -1,
3401
+ className: "w-full shrink-0 snap-start",
3402
+ style: { aspectRatio: String(aspectRatio) },
3403
+ children: renderItem(items[0], 0)
3404
+ },
3405
+ "clone-end"
3406
+ )
3407
+ ]
3318
3408
  }
3319
3409
  ),
3320
- showArrows && items.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3410
+ showArrows && N > 1 && /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3321
3411
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3322
3412
  "button",
3323
3413
  {
3324
3414
  type: "button",
3325
3415
  "aria-label": "Previous slide",
3326
3416
  onClick: () => goTo(activeIdx - 1),
3327
- disabled: activeIdx === 0,
3417
+ disabled: !isLooping && activeIdx === 0,
3328
3418
  className: "bg-panel/85 border-border text-text hover:bg-panel absolute top-1/2 left-2 inline-grid h-9 w-9 -translate-y-1/2 cursor-pointer place-items-center rounded-full border shadow-md backdrop-blur disabled:cursor-not-allowed disabled:opacity-40",
3329
3419
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_icons5.IconGlyph, { name: "caretLeft", size: 16 })
3330
3420
  }
@@ -3335,13 +3425,13 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3335
3425
  type: "button",
3336
3426
  "aria-label": "Next slide",
3337
3427
  onClick: () => goTo(activeIdx + 1),
3338
- disabled: activeIdx === items.length - 1,
3428
+ disabled: !isLooping && activeIdx === N - 1,
3339
3429
  className: "bg-panel/85 border-border text-text hover:bg-panel absolute top-1/2 right-2 inline-grid h-9 w-9 -translate-y-1/2 cursor-pointer place-items-center rounded-full border shadow-md backdrop-blur disabled:cursor-not-allowed disabled:opacity-40",
3340
3430
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_icons5.IconGlyph, { name: "caretRight", size: 16 })
3341
3431
  }
3342
3432
  )
3343
3433
  ] }),
3344
- showDots && items.length > 1 && /*
3434
+ showDots && N > 1 && /*
3345
3435
  * Plain `<button>` + `aria-current` rather than the tabs pattern
3346
3436
  * (`role="tablist" / "tab"`). The APG carousel pattern recommends
3347
3437
  * this lighter semantic; the viewport's `aria-live="polite"`
@@ -4401,19 +4491,28 @@ var Lightbox = (0, import_react59.forwardRef)(function Lightbox2({
4401
4491
  index,
4402
4492
  defaultIndex,
4403
4493
  onIndexChange,
4494
+ loop = false,
4404
4495
  title = "Photo viewer"
4405
4496
  }, ref) {
4497
+ const N = items.length;
4498
+ const isLooping = loop && N > 1;
4406
4499
  const [active, setActive] = useControllableState({
4407
4500
  value: index,
4408
4501
  defaultValue: defaultIndex ?? 0,
4409
4502
  onChange: onIndexChange
4410
4503
  });
4411
4504
  const goPrev = (0, import_react59.useCallback)(() => {
4412
- setActive((prev) => Math.max(0, (prev ?? 0) - 1));
4413
- }, [setActive]);
4505
+ setActive((prev) => {
4506
+ const p = prev ?? 0;
4507
+ return isLooping ? (p - 1 + N) % N : Math.max(0, p - 1);
4508
+ });
4509
+ }, [setActive, isLooping, N]);
4414
4510
  const goNext = (0, import_react59.useCallback)(() => {
4415
- setActive((prev) => Math.min(items.length - 1, (prev ?? 0) + 1));
4416
- }, [items.length, setActive]);
4511
+ setActive((prev) => {
4512
+ const p = prev ?? 0;
4513
+ return isLooping ? (p + 1) % N : Math.min(N - 1, p + 1);
4514
+ });
4515
+ }, [setActive, isLooping, N]);
4417
4516
  const onKey = (0, import_react59.useCallback)(
4418
4517
  (e) => {
4419
4518
  if (e.key === "ArrowLeft") {
@@ -4453,7 +4552,7 @@ var Lightbox = (0, import_react59.forwardRef)(function Lightbox2({
4453
4552
  type: "button",
4454
4553
  "aria-label": "Previous photo",
4455
4554
  onClick: goPrev,
4456
- disabled: activeIdx === 0,
4555
+ disabled: !isLooping && activeIdx === 0,
4457
4556
  className: "absolute top-1/2 left-4 inline-grid h-11 w-11 -translate-y-1/2 cursor-pointer place-items-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-40",
4458
4557
  children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_icons7.IconGlyph, { name: "caretLeft", size: 20 })
4459
4558
  }
@@ -4464,7 +4563,7 @@ var Lightbox = (0, import_react59.forwardRef)(function Lightbox2({
4464
4563
  type: "button",
4465
4564
  "aria-label": "Next photo",
4466
4565
  onClick: goNext,
4467
- disabled: activeIdx === items.length - 1,
4566
+ disabled: !isLooping && activeIdx === N - 1,
4468
4567
  className: "absolute top-1/2 right-4 inline-grid h-11 w-11 -translate-y-1/2 cursor-pointer place-items-center rounded-full bg-white/10 text-white hover:bg-white/20 disabled:cursor-not-allowed disabled:opacity-40",
4469
4568
  children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_icons7.IconGlyph, { name: "caretRight", size: 20 })
4470
4569
  }
@@ -4517,6 +4616,7 @@ var ListingCard = (0, import_react60.forwardRef)(function ListingCard2({
4517
4616
  variant = "default",
4518
4617
  photos,
4519
4618
  renderPhoto,
4619
+ loop = true,
4520
4620
  onClick,
4521
4621
  hoverEffect,
4522
4622
  title,
@@ -4563,6 +4663,7 @@ var ListingCard = (0, import_react60.forwardRef)(function ListingCard2({
4563
4663
  Carousel,
4564
4664
  {
4565
4665
  items: photos,
4666
+ loop,
4566
4667
  ...isSpec ? {
4567
4668
  index: photoIndex,
4568
4669
  onIndexChange: setPhotoIndex,
@@ -4803,6 +4904,7 @@ var ListingDetail = (0, import_react61.forwardRef)(function ListingDetail2({
4803
4904
  onOpenChange,
4804
4905
  photos,
4805
4906
  renderPhoto,
4907
+ loop = true,
4806
4908
  title,
4807
4909
  eyebrow,
4808
4910
  description,
@@ -4872,6 +4974,7 @@ var ListingDetail = (0, import_react61.forwardRef)(function ListingDetail2({
4872
4974
  items: photos,
4873
4975
  index: galleryIndex,
4874
4976
  onIndexChange: setGalleryIndex,
4977
+ loop,
4875
4978
  ...isSpec ? { showDots: false } : {},
4876
4979
  "aria-label": typeof title === "string" ? `${title} photos` : "Listing photos",
4877
4980
  renderItem: (src, i) => renderPhoto ? renderPhoto(src, i, "gallery") : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
@@ -5117,6 +5220,7 @@ var ListingDetail = (0, import_react61.forwardRef)(function ListingDetail2({
5117
5220
  items: photos,
5118
5221
  index: galleryIndex,
5119
5222
  onIndexChange: setGalleryIndex,
5223
+ loop,
5120
5224
  title: lightboxTitle,
5121
5225
  renderItem: (src, i) => renderPhoto ? renderPhoto(src, i, "lightbox") : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)("img", { src, alt: "", className: "max-h-[88vh] max-w-[92vw] object-contain" })
5122
5226
  }
@@ -7408,11 +7512,9 @@ var Tree = (0, import_react88.forwardRef)(function Tree2({
7408
7512
  return out;
7409
7513
  }, [items, expandedSet]);
7410
7514
  const [activeId, setActiveId] = (0, import_react88.useState)(null);
7411
- (0, import_react88.useEffect)(() => {
7412
- if (activeId && !flatVisible.some((f) => f.id === activeId)) {
7413
- setActiveId(null);
7414
- }
7415
- }, [activeId, flatVisible]);
7515
+ if (activeId && !flatVisible.some((f) => f.id === activeId)) {
7516
+ setActiveId(null);
7517
+ }
7416
7518
  const tabStopId = (0, import_react88.useMemo)(() => {
7417
7519
  if (activeId && flatVisible.some((f) => f.id === activeId)) return activeId;
7418
7520
  if (value && flatVisible.some((f) => f.id === value)) return value;