@underverse-ui/underverse 0.1.33 → 0.1.35

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
@@ -214,6 +214,7 @@ var Button = (0, import_react.forwardRef)(
214
214
  lockMs = 600,
215
215
  asContainer = false,
216
216
  noWrap = true,
217
+ noHoverOverlay = false,
217
218
  ...rest
218
219
  }, ref) => {
219
220
  const baseStyles = asContainer ? "relative inline-flex justify-center rounded-md font-medium transition-colors duration-150 ease-soft outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:translate-y-px" : "relative inline-flex items-center justify-center gap-2 rounded-md font-medium overflow-hidden transition-colors duration-150 ease-soft outline-none focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background active:translate-y-px";
@@ -275,7 +276,7 @@ var Button = (0, import_react.forwardRef)(
275
276
  "aria-label": rest["aria-label"] || title,
276
277
  ...rest,
277
278
  children: [
278
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inset-0 bg-gradient-to-r from-primary-foreground/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200" }),
279
+ !noHoverOverlay && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute inset-0 bg-gradient-to-r from-primary-foreground/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-200" }),
279
280
  loading2 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
280
281
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SpinnerIcon, { className: "w-4 h-4 animate-spin" }),
281
282
  loadingText && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ml-2", "aria-live": "polite", children: loadingText }),
@@ -6897,57 +6898,296 @@ function ImageUpload({
6897
6898
  var React27 = __toESM(require("react"), 1);
6898
6899
  var import_lucide_react19 = require("lucide-react");
6899
6900
  var import_jsx_runtime35 = require("react/jsx-runtime");
6900
- function Carousel({ children, autoScroll = true, autoScrollInterval = 5e3 }) {
6901
+ function Carousel({
6902
+ children,
6903
+ autoScroll = true,
6904
+ autoScrollInterval = 5e3,
6905
+ animation = "slide",
6906
+ orientation = "horizontal",
6907
+ showArrows = true,
6908
+ showDots = true,
6909
+ showProgress = false,
6910
+ showThumbnails = false,
6911
+ loop = true,
6912
+ slidesToShow = 1,
6913
+ slidesToScroll = 1,
6914
+ className,
6915
+ containerClassName,
6916
+ slideClassName,
6917
+ onSlideChange,
6918
+ thumbnailRenderer,
6919
+ ariaLabel = "Carousel"
6920
+ }) {
6901
6921
  const [currentIndex, setCurrentIndex] = React27.useState(0);
6902
- const totalSlides = React27.Children.count(children);
6903
6922
  const [isPaused, setIsPaused] = React27.useState(false);
6923
+ const [isDragging, setIsDragging] = React27.useState(false);
6924
+ const [startPos, setStartPos] = React27.useState(0);
6925
+ const [currentTranslate, setCurrentTranslate] = React27.useState(0);
6926
+ const [prevTranslate, setPrevTranslate] = React27.useState(0);
6927
+ const [progress, setProgress] = React27.useState(0);
6928
+ const carouselRef = React27.useRef(null);
6929
+ const progressIntervalRef = React27.useRef(null);
6930
+ const totalSlides = React27.Children.count(children);
6931
+ const maxIndex = Math.max(0, totalSlides - slidesToShow);
6932
+ const isHorizontal = orientation === "horizontal";
6904
6933
  const scrollPrev = React27.useCallback(() => {
6905
- setCurrentIndex((prev) => prev > 0 ? prev - 1 : totalSlides - 1);
6906
- }, [totalSlides]);
6934
+ setCurrentIndex((prev) => {
6935
+ if (prev === 0) {
6936
+ return loop ? maxIndex : 0;
6937
+ }
6938
+ return Math.max(0, prev - slidesToScroll);
6939
+ });
6940
+ }, [loop, maxIndex, slidesToScroll]);
6907
6941
  const scrollNext = React27.useCallback(() => {
6908
- setCurrentIndex((prev) => prev < totalSlides - 1 ? prev + 1 : 0);
6909
- }, [totalSlides]);
6942
+ setCurrentIndex((prev) => {
6943
+ if (prev >= maxIndex) {
6944
+ return loop ? 0 : maxIndex;
6945
+ }
6946
+ return Math.min(maxIndex, prev + slidesToScroll);
6947
+ });
6948
+ }, [loop, maxIndex, slidesToScroll]);
6949
+ const scrollTo = React27.useCallback(
6950
+ (index) => {
6951
+ setCurrentIndex(Math.min(maxIndex, Math.max(0, index)));
6952
+ },
6953
+ [maxIndex]
6954
+ );
6910
6955
  React27.useEffect(() => {
6911
- if (!autoScroll || isPaused || totalSlides <= 1) return;
6912
- const interval = setInterval(() => {
6913
- scrollNext();
6914
- }, autoScrollInterval);
6915
- return () => clearInterval(interval);
6916
- }, [autoScroll, isPaused, totalSlides, autoScrollInterval, scrollNext]);
6917
- return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)("div", { className: "relative w-full overflow-hidden", onMouseEnter: () => setIsPaused(true), onMouseLeave: () => setIsPaused(false), children: [
6918
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex transition-transform duration-500 ease-in-out", style: { transform: `translateX(-${currentIndex * 100}%)` }, children: React27.Children.map(children, (child, idx) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "flex-shrink-0 w-full h-full", children: child }, idx)) }),
6919
- totalSlides > 1 && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
6920
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
6921
- Button_default,
6922
- {
6923
- onClick: scrollPrev,
6924
- variant: "outline",
6925
- size: "icon",
6926
- icon: import_lucide_react19.ArrowLeft,
6927
- className: "absolute left-4 top-1/2 -translate-y-1/2 hover:-translate-y-1/2 z-10 rounded-full will-change-transform bg-background/80 hover:bg-background border-border/50 hover:border-border text-foreground"
6928
- }
6929
- ),
6930
- /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
6931
- Button_default,
6932
- {
6933
- onClick: scrollNext,
6934
- variant: "outline",
6935
- size: "icon",
6936
- icon: import_lucide_react19.ArrowRight,
6937
- className: "absolute right-4 top-1/2 -translate-y-1/2 hover:-translate-y-1/2 z-10 rounded-full will-change-transform bg-background/80 hover:bg-background border-border/50 hover:border-border text-foreground"
6956
+ const handleKeyDown = (e) => {
6957
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
6958
+ e.preventDefault();
6959
+ scrollPrev();
6960
+ } else if (e.key === "ArrowRight" || e.key === "ArrowDown") {
6961
+ e.preventDefault();
6962
+ scrollNext();
6963
+ } else if (e.key === "Home") {
6964
+ e.preventDefault();
6965
+ scrollTo(0);
6966
+ } else if (e.key === "End") {
6967
+ e.preventDefault();
6968
+ scrollTo(maxIndex);
6969
+ }
6970
+ };
6971
+ const carousel = carouselRef.current;
6972
+ if (carousel) {
6973
+ carousel.addEventListener("keydown", handleKeyDown);
6974
+ return () => carousel.removeEventListener("keydown", handleKeyDown);
6975
+ }
6976
+ }, [scrollPrev, scrollNext, scrollTo, maxIndex]);
6977
+ React27.useEffect(() => {
6978
+ if (!autoScroll || isPaused || totalSlides <= slidesToShow) {
6979
+ setProgress(0);
6980
+ if (progressIntervalRef.current) {
6981
+ clearInterval(progressIntervalRef.current);
6982
+ }
6983
+ return;
6984
+ }
6985
+ setProgress(0);
6986
+ const progressStep = 100 / (autoScrollInterval / 50);
6987
+ progressIntervalRef.current = setInterval(() => {
6988
+ setProgress((prev) => {
6989
+ if (prev >= 100) {
6990
+ scrollNext();
6991
+ return 0;
6938
6992
  }
6939
- )
6940
- ] }),
6941
- totalSlides > 1 && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2", children: Array.from({ length: totalSlides }, (_, idx) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
6942
- "button",
6943
- {
6944
- onClick: () => setCurrentIndex(idx),
6945
- className: `w-2 h-2 rounded-full transition-all ${idx === currentIndex ? "bg-primary w-6" : "bg-muted-foreground/50 hover:bg-muted-foreground/75"}`,
6946
- "aria-label": `Go to slide ${idx + 1}`
6947
- },
6948
- idx
6949
- )) })
6950
- ] });
6993
+ return prev + progressStep;
6994
+ });
6995
+ }, 50);
6996
+ return () => {
6997
+ if (progressIntervalRef.current) {
6998
+ clearInterval(progressIntervalRef.current);
6999
+ }
7000
+ };
7001
+ }, [autoScroll, isPaused, totalSlides, slidesToShow, autoScrollInterval, scrollNext]);
7002
+ const getPositionX = (event) => {
7003
+ return event.type.includes("mouse") ? event.pageX : event.touches[0].clientX;
7004
+ };
7005
+ const getPositionY = (event) => {
7006
+ return event.type.includes("mouse") ? event.pageY : event.touches[0].clientY;
7007
+ };
7008
+ const touchStart = (event) => {
7009
+ setIsDragging(true);
7010
+ const pos = isHorizontal ? getPositionX(event.nativeEvent) : getPositionY(event.nativeEvent);
7011
+ setStartPos(pos);
7012
+ setPrevTranslate(currentTranslate);
7013
+ };
7014
+ const touchMove = (event) => {
7015
+ if (!isDragging) return;
7016
+ const pos = isHorizontal ? getPositionX(event.nativeEvent) : getPositionY(event.nativeEvent);
7017
+ const currentPosition = pos;
7018
+ setCurrentTranslate(prevTranslate + currentPosition - startPos);
7019
+ };
7020
+ const touchEnd = () => {
7021
+ if (!isDragging) return;
7022
+ setIsDragging(false);
7023
+ const movedBy = currentTranslate - prevTranslate;
7024
+ const threshold = 50;
7025
+ if (movedBy < -threshold && currentIndex < maxIndex) {
7026
+ scrollNext();
7027
+ } else if (movedBy > threshold && currentIndex > 0) {
7028
+ scrollPrev();
7029
+ }
7030
+ setCurrentTranslate(0);
7031
+ setPrevTranslate(0);
7032
+ };
7033
+ React27.useEffect(() => {
7034
+ onSlideChange?.(currentIndex);
7035
+ }, [currentIndex, onSlideChange]);
7036
+ const getAnimationStyles = () => {
7037
+ const baseTransform = isHorizontal ? `translateX(-${currentIndex * (100 / slidesToShow)}%)` : `translateY(-${currentIndex * (100 / slidesToShow)}%)`;
7038
+ if (animation === "fade") {
7039
+ return {
7040
+ transition: "opacity 500ms ease-in-out"
7041
+ };
7042
+ }
7043
+ if (animation === "scale") {
7044
+ return {
7045
+ transform: baseTransform,
7046
+ transition: "transform 500ms ease-in-out, scale 500ms ease-in-out"
7047
+ };
7048
+ }
7049
+ return {
7050
+ transform: baseTransform,
7051
+ transition: isDragging ? "none" : "transform 500ms ease-in-out"
7052
+ };
7053
+ };
7054
+ const slideWidth = 100 / slidesToShow;
7055
+ return /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(
7056
+ "div",
7057
+ {
7058
+ ref: carouselRef,
7059
+ className: cn("relative w-full overflow-hidden focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 rounded-lg", className),
7060
+ onMouseEnter: () => setIsPaused(true),
7061
+ onMouseLeave: () => setIsPaused(false),
7062
+ role: "region",
7063
+ "aria-label": ariaLabel,
7064
+ "aria-roledescription": "carousel",
7065
+ tabIndex: 0,
7066
+ children: [
7067
+ showProgress && autoScroll && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "absolute top-0 left-0 right-0 h-1 bg-muted z-20", children: /* @__PURE__ */ (0, import_jsx_runtime35.jsx)("div", { className: "h-full bg-primary transition-all duration-50 ease-linear", style: { width: `${progress}%` } }) }),
7068
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7069
+ "div",
7070
+ {
7071
+ className: cn("flex", isHorizontal ? "flex-row" : "flex-col", containerClassName),
7072
+ style: getAnimationStyles(),
7073
+ onTouchStart: touchStart,
7074
+ onTouchMove: touchMove,
7075
+ onTouchEnd: touchEnd,
7076
+ onMouseDown: touchStart,
7077
+ onMouseMove: touchMove,
7078
+ onMouseUp: touchEnd,
7079
+ onMouseLeave: touchEnd,
7080
+ role: "group",
7081
+ "aria-atomic": "false",
7082
+ "aria-live": autoScroll ? "off" : "polite",
7083
+ children: React27.Children.map(children, (child, idx) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7084
+ "div",
7085
+ {
7086
+ className: cn(
7087
+ "flex-shrink-0",
7088
+ isHorizontal ? "h-full" : "w-full",
7089
+ animation === "fade" && idx !== currentIndex && "opacity-0",
7090
+ animation === "scale" && idx !== currentIndex && "scale-95",
7091
+ slideClassName
7092
+ ),
7093
+ style: {
7094
+ [isHorizontal ? "width" : "height"]: `${slideWidth}%`
7095
+ },
7096
+ role: "group",
7097
+ "aria-roledescription": "slide",
7098
+ "aria-label": `${idx + 1} of ${totalSlides}`,
7099
+ "aria-hidden": idx < currentIndex || idx >= currentIndex + slidesToShow,
7100
+ children: child
7101
+ },
7102
+ idx
7103
+ ))
7104
+ }
7105
+ ),
7106
+ showArrows && totalSlides > slidesToShow && /* @__PURE__ */ (0, import_jsx_runtime35.jsxs)(import_jsx_runtime35.Fragment, { children: [
7107
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7108
+ Button_default,
7109
+ {
7110
+ onClick: scrollPrev,
7111
+ variant: "ghost",
7112
+ size: "icon",
7113
+ icon: import_lucide_react19.ChevronLeft,
7114
+ noHoverOverlay: true,
7115
+ disabled: !loop && currentIndex === 0,
7116
+ className: cn(
7117
+ "absolute top-1/2 -translate-y-1/2 hover:-translate-y-1/2 active:-translate-y-1/2 z-10 rounded-full will-change-transform backdrop-blur-0 hover:backdrop-blur-0 hover:bg-transparent border-0",
7118
+ isHorizontal ? "left-4" : "top-4 left-1/2 -translate-x-1/2 rotate-90"
7119
+ ),
7120
+ "aria-label": "Previous slide"
7121
+ }
7122
+ ),
7123
+ /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7124
+ Button_default,
7125
+ {
7126
+ onClick: scrollNext,
7127
+ variant: "ghost",
7128
+ size: "icon",
7129
+ icon: import_lucide_react19.ChevronRight,
7130
+ noHoverOverlay: true,
7131
+ disabled: !loop && currentIndex >= maxIndex,
7132
+ className: cn(
7133
+ "absolute top-1/2 -translate-y-1/2 hover:-translate-y-1/2 active:-translate-y-1/2 z-10 rounded-full will-change-transform backdrop-blur-0 hover:backdrop-blur-0 hover:bg-transparent border-0",
7134
+ isHorizontal ? "right-4" : "bottom-4 left-1/2 -translate-x-1/2 rotate-90"
7135
+ ),
7136
+ "aria-label": "Next slide"
7137
+ }
7138
+ )
7139
+ ] }),
7140
+ showDots && totalSlides > slidesToShow && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7141
+ "div",
7142
+ {
7143
+ className: cn(
7144
+ "absolute flex gap-2 z-10",
7145
+ isHorizontal ? "bottom-4 left-1/2 -translate-x-1/2 flex-row" : "right-4 top-1/2 -translate-y-1/2 flex-col"
7146
+ ),
7147
+ role: "tablist",
7148
+ "aria-label": "Carousel pagination",
7149
+ children: Array.from({ length: maxIndex + 1 }, (_, idx) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7150
+ "button",
7151
+ {
7152
+ onClick: () => scrollTo(idx),
7153
+ className: cn(
7154
+ "rounded-full transition-all focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2",
7155
+ isHorizontal ? "w-2 h-2" : "w-2 h-2",
7156
+ idx === currentIndex ? `bg-primary ${isHorizontal ? "w-6" : "h-6"}` : "bg-muted-foreground/50 hover:bg-muted-foreground/75"
7157
+ ),
7158
+ "aria-label": `Go to slide ${idx + 1}`,
7159
+ "aria-selected": idx === currentIndex,
7160
+ role: "tab"
7161
+ },
7162
+ idx
7163
+ ))
7164
+ }
7165
+ ),
7166
+ showThumbnails && totalSlides > slidesToShow && /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7167
+ "div",
7168
+ {
7169
+ className: cn(
7170
+ "absolute bottom-0 left-0 right-0 flex gap-2 p-4 bg-gradient-to-t from-black/50 to-transparent overflow-x-auto",
7171
+ isHorizontal ? "flex-row" : "flex-col"
7172
+ ),
7173
+ children: React27.Children.map(children, (child, idx) => /* @__PURE__ */ (0, import_jsx_runtime35.jsx)(
7174
+ "button",
7175
+ {
7176
+ onClick: () => scrollTo(idx),
7177
+ className: cn(
7178
+ "flex-shrink-0 w-16 h-16 rounded-md overflow-hidden border-2 transition-all focus:outline-none focus:ring-2 focus:ring-primary",
7179
+ idx === currentIndex ? "border-primary scale-110" : "border-transparent opacity-70 hover:opacity-100"
7180
+ ),
7181
+ "aria-label": `Thumbnail ${idx + 1}`,
7182
+ children: thumbnailRenderer ? thumbnailRenderer(child, idx) : child
7183
+ },
7184
+ idx
7185
+ ))
7186
+ }
7187
+ )
7188
+ ]
7189
+ }
7190
+ );
6951
7191
  }
6952
7192
 
6953
7193
  // ../../components/ui/ClientOnly.tsx