@ship-it-ui/ui 0.0.10 → 0.0.11

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,36 @@ 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 isLooping = loop && N > 1;
3258
3261
  const [active, setActive] = useControllableState({
3259
3262
  value: indexProp,
3260
3263
  defaultValue: defaultIndex ?? 0,
3261
3264
  onChange: onIndexChange
3262
3265
  });
3263
3266
  const viewportRef = (0, import_react52.useRef)(null);
3267
+ const internalScrollRef = (0, import_react52.useRef)(false);
3268
+ const activeIdx = active ?? 0;
3269
+ const domIndexFor = (0, import_react52.useCallback)((real) => isLooping ? real + 1 : real, [isLooping]);
3264
3270
  const goTo = (0, import_react52.useCallback)(
3265
3271
  (i) => {
3266
- const clamped = Math.max(0, Math.min(items.length - 1, i));
3267
- setActive(clamped);
3272
+ const next = isLooping ? (i % N + N) % N : Math.max(0, Math.min(N - 1, i));
3273
+ setActive(next);
3268
3274
  const node = viewportRef.current;
3269
3275
  if (node) {
3270
- const slide = node.children[clamped];
3271
- slide?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
3276
+ const slide = node.children[domIndexFor(next)];
3277
+ if (slide) {
3278
+ internalScrollRef.current = true;
3279
+ slide.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "start" });
3280
+ }
3272
3281
  }
3273
3282
  },
3274
- [items.length, setActive]
3283
+ [N, isLooping, domIndexFor, setActive]
3275
3284
  );
3276
3285
  (0, import_react52.useEffect)(() => {
3277
3286
  const node = viewportRef.current;
@@ -3279,13 +3288,59 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3279
3288
  const onScroll = () => {
3280
3289
  const width = node.clientWidth;
3281
3290
  if (width === 0) return;
3282
- const i = Math.round(node.scrollLeft / width);
3283
- if (i !== active) setActive(i);
3291
+ const domIdx = Math.round(node.scrollLeft / width);
3292
+ if (!isLooping) {
3293
+ if (domIdx !== activeIdx) setActive(domIdx);
3294
+ return;
3295
+ }
3296
+ if (domIdx === 0) {
3297
+ const realTwin = node.children[N];
3298
+ if (realTwin) {
3299
+ internalScrollRef.current = true;
3300
+ realTwin.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3301
+ }
3302
+ if (activeIdx !== N - 1) setActive(N - 1);
3303
+ return;
3304
+ }
3305
+ if (domIdx === N + 1) {
3306
+ const realTwin = node.children[1];
3307
+ if (realTwin) {
3308
+ internalScrollRef.current = true;
3309
+ realTwin.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3310
+ }
3311
+ if (activeIdx !== 0) setActive(0);
3312
+ return;
3313
+ }
3314
+ const realIdx = domIdx - 1;
3315
+ if (realIdx !== activeIdx) setActive(realIdx);
3284
3316
  };
3285
3317
  node.addEventListener("scroll", onScroll, { passive: true });
3286
3318
  return () => node.removeEventListener("scroll", onScroll);
3287
- }, [active, setActive]);
3288
- const activeIdx = active ?? 0;
3319
+ }, [activeIdx, isLooping, N, setActive]);
3320
+ (0, import_react52.useEffect)(() => {
3321
+ if (internalScrollRef.current) {
3322
+ internalScrollRef.current = false;
3323
+ return;
3324
+ }
3325
+ const node = viewportRef.current;
3326
+ if (!node) return;
3327
+ const width = node.clientWidth;
3328
+ if (width === 0) return;
3329
+ const targetDom = domIndexFor(activeIdx);
3330
+ const currentDom = Math.round(node.scrollLeft / width);
3331
+ if (currentDom === targetDom) return;
3332
+ const slide = node.children[targetDom];
3333
+ slide?.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3334
+ }, [activeIdx, domIndexFor]);
3335
+ (0, import_react52.useLayoutEffect)(() => {
3336
+ if (!isLooping) return;
3337
+ const node = viewportRef.current;
3338
+ if (!node) return;
3339
+ const slide = node.children[domIndexFor(activeIdx)];
3340
+ if (!slide) return;
3341
+ internalScrollRef.current = true;
3342
+ slide.scrollIntoView({ behavior: "instant", block: "nearest", inline: "start" });
3343
+ }, [isLooping]);
3289
3344
  return /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3290
3345
  "div",
3291
3346
  {
@@ -3297,34 +3352,58 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3297
3352
  ...props,
3298
3353
  children: [
3299
3354
  /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)("div", { className: "relative overflow-hidden rounded-md", children: [
3300
- /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3355
+ /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(
3301
3356
  "div",
3302
3357
  {
3303
3358
  ref: viewportRef,
3304
3359
  className: "flex w-full snap-x snap-mandatory overflow-x-auto scroll-smooth [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
3305
3360
  "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
- ))
3361
+ children: [
3362
+ isLooping && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3363
+ "div",
3364
+ {
3365
+ "aria-hidden": "true",
3366
+ tabIndex: -1,
3367
+ className: "w-full shrink-0 snap-start",
3368
+ style: { aspectRatio: String(aspectRatio) },
3369
+ children: renderItem(items[N - 1], N - 1)
3370
+ },
3371
+ "clone-start"
3372
+ ),
3373
+ items.map((item, i) => /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3374
+ "div",
3375
+ {
3376
+ className: "w-full shrink-0 snap-start",
3377
+ style: { aspectRatio: String(aspectRatio) },
3378
+ role: "group",
3379
+ "aria-roledescription": "slide",
3380
+ "aria-label": `${i + 1} of ${N}`,
3381
+ children: renderItem(item, i)
3382
+ },
3383
+ i
3384
+ )),
3385
+ isLooping && /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3386
+ "div",
3387
+ {
3388
+ "aria-hidden": "true",
3389
+ tabIndex: -1,
3390
+ className: "w-full shrink-0 snap-start",
3391
+ style: { aspectRatio: String(aspectRatio) },
3392
+ children: renderItem(items[0], 0)
3393
+ },
3394
+ "clone-end"
3395
+ )
3396
+ ]
3318
3397
  }
3319
3398
  ),
3320
- showArrows && items.length > 1 && /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3399
+ showArrows && N > 1 && /* @__PURE__ */ (0, import_jsx_runtime45.jsxs)(import_jsx_runtime45.Fragment, { children: [
3321
3400
  /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(
3322
3401
  "button",
3323
3402
  {
3324
3403
  type: "button",
3325
3404
  "aria-label": "Previous slide",
3326
3405
  onClick: () => goTo(activeIdx - 1),
3327
- disabled: activeIdx === 0,
3406
+ disabled: !isLooping && activeIdx === 0,
3328
3407
  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
3408
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_icons5.IconGlyph, { name: "caretLeft", size: 16 })
3330
3409
  }
@@ -3335,13 +3414,13 @@ var Carousel = (0, import_react52.forwardRef)(function Carousel2({
3335
3414
  type: "button",
3336
3415
  "aria-label": "Next slide",
3337
3416
  onClick: () => goTo(activeIdx + 1),
3338
- disabled: activeIdx === items.length - 1,
3417
+ disabled: !isLooping && activeIdx === N - 1,
3339
3418
  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
3419
  children: /* @__PURE__ */ (0, import_jsx_runtime45.jsx)(import_icons5.IconGlyph, { name: "caretRight", size: 16 })
3341
3420
  }
3342
3421
  )
3343
3422
  ] }),
3344
- showDots && items.length > 1 && /*
3423
+ showDots && N > 1 && /*
3345
3424
  * Plain `<button>` + `aria-current` rather than the tabs pattern
3346
3425
  * (`role="tablist" / "tab"`). The APG carousel pattern recommends
3347
3426
  * this lighter semantic; the viewport's `aria-live="polite"`
@@ -4401,19 +4480,28 @@ var Lightbox = (0, import_react59.forwardRef)(function Lightbox2({
4401
4480
  index,
4402
4481
  defaultIndex,
4403
4482
  onIndexChange,
4483
+ loop = false,
4404
4484
  title = "Photo viewer"
4405
4485
  }, ref) {
4486
+ const N = items.length;
4487
+ const isLooping = loop && N > 1;
4406
4488
  const [active, setActive] = useControllableState({
4407
4489
  value: index,
4408
4490
  defaultValue: defaultIndex ?? 0,
4409
4491
  onChange: onIndexChange
4410
4492
  });
4411
4493
  const goPrev = (0, import_react59.useCallback)(() => {
4412
- setActive((prev) => Math.max(0, (prev ?? 0) - 1));
4413
- }, [setActive]);
4494
+ setActive((prev) => {
4495
+ const p = prev ?? 0;
4496
+ return isLooping ? (p - 1 + N) % N : Math.max(0, p - 1);
4497
+ });
4498
+ }, [setActive, isLooping, N]);
4414
4499
  const goNext = (0, import_react59.useCallback)(() => {
4415
- setActive((prev) => Math.min(items.length - 1, (prev ?? 0) + 1));
4416
- }, [items.length, setActive]);
4500
+ setActive((prev) => {
4501
+ const p = prev ?? 0;
4502
+ return isLooping ? (p + 1) % N : Math.min(N - 1, p + 1);
4503
+ });
4504
+ }, [setActive, isLooping, N]);
4417
4505
  const onKey = (0, import_react59.useCallback)(
4418
4506
  (e) => {
4419
4507
  if (e.key === "ArrowLeft") {
@@ -4453,7 +4541,7 @@ var Lightbox = (0, import_react59.forwardRef)(function Lightbox2({
4453
4541
  type: "button",
4454
4542
  "aria-label": "Previous photo",
4455
4543
  onClick: goPrev,
4456
- disabled: activeIdx === 0,
4544
+ disabled: !isLooping && activeIdx === 0,
4457
4545
  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
4546
  children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_icons7.IconGlyph, { name: "caretLeft", size: 20 })
4459
4547
  }
@@ -4464,7 +4552,7 @@ var Lightbox = (0, import_react59.forwardRef)(function Lightbox2({
4464
4552
  type: "button",
4465
4553
  "aria-label": "Next photo",
4466
4554
  onClick: goNext,
4467
- disabled: activeIdx === items.length - 1,
4555
+ disabled: !isLooping && activeIdx === N - 1,
4468
4556
  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
4557
  children: /* @__PURE__ */ (0, import_jsx_runtime52.jsx)(import_icons7.IconGlyph, { name: "caretRight", size: 20 })
4470
4558
  }
@@ -4517,6 +4605,7 @@ var ListingCard = (0, import_react60.forwardRef)(function ListingCard2({
4517
4605
  variant = "default",
4518
4606
  photos,
4519
4607
  renderPhoto,
4608
+ loop = true,
4520
4609
  onClick,
4521
4610
  hoverEffect,
4522
4611
  title,
@@ -4563,6 +4652,7 @@ var ListingCard = (0, import_react60.forwardRef)(function ListingCard2({
4563
4652
  Carousel,
4564
4653
  {
4565
4654
  items: photos,
4655
+ loop,
4566
4656
  ...isSpec ? {
4567
4657
  index: photoIndex,
4568
4658
  onIndexChange: setPhotoIndex,
@@ -4803,6 +4893,7 @@ var ListingDetail = (0, import_react61.forwardRef)(function ListingDetail2({
4803
4893
  onOpenChange,
4804
4894
  photos,
4805
4895
  renderPhoto,
4896
+ loop = true,
4806
4897
  title,
4807
4898
  eyebrow,
4808
4899
  description,
@@ -4872,6 +4963,7 @@ var ListingDetail = (0, import_react61.forwardRef)(function ListingDetail2({
4872
4963
  items: photos,
4873
4964
  index: galleryIndex,
4874
4965
  onIndexChange: setGalleryIndex,
4966
+ loop,
4875
4967
  ...isSpec ? { showDots: false } : {},
4876
4968
  "aria-label": typeof title === "string" ? `${title} photos` : "Listing photos",
4877
4969
  renderItem: (src, i) => renderPhoto ? renderPhoto(src, i, "gallery") : /* @__PURE__ */ (0, import_jsx_runtime54.jsx)(
@@ -5117,6 +5209,7 @@ var ListingDetail = (0, import_react61.forwardRef)(function ListingDetail2({
5117
5209
  items: photos,
5118
5210
  index: galleryIndex,
5119
5211
  onIndexChange: setGalleryIndex,
5212
+ loop,
5120
5213
  title: lightboxTitle,
5121
5214
  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
5215
  }
@@ -7408,11 +7501,9 @@ var Tree = (0, import_react88.forwardRef)(function Tree2({
7408
7501
  return out;
7409
7502
  }, [items, expandedSet]);
7410
7503
  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]);
7504
+ if (activeId && !flatVisible.some((f) => f.id === activeId)) {
7505
+ setActiveId(null);
7506
+ }
7416
7507
  const tabStopId = (0, import_react88.useMemo)(() => {
7417
7508
  if (activeId && flatVisible.some((f) => f.id === activeId)) return activeId;
7418
7509
  if (value && flatVisible.some((f) => f.id === value)) return value;