@ralorotech/duino-ui
Version:
UI library for Duino projects
1,389 lines (1,388 loc) • 81.8 kB
JavaScript
import React2, { createContext, useRef, useState, useEffect, useId, useCallback, useMemo, useContext, isValidElement, cloneElement } from 'react';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { createPortal } from 'react-dom';
// src/components/Button/Button.tsx
var Button = React2.forwardRef(
({
variant = "primary",
size = "md",
shape = "default",
block = false,
loading = false,
icon,
iconPosition = "start",
loadingText = "Cargando...",
danger = false,
href,
target,
className = "",
children,
disabled,
onClick,
...rest
}, ref) => {
const finalVariant = danger && variant !== "text" ? "danger" : variant;
const baseClass = "duino-btn";
const classes = [
baseClass,
`${baseClass}--${finalVariant}`,
`${baseClass}--${size}`,
shape !== "default" && `${baseClass}--${shape}`,
block && `${baseClass}--block`,
loading && `${baseClass}--loading`,
(disabled || loading) && `${baseClass}--disabled`,
!children && icon && `${baseClass}--icon-only`,
className
].filter(Boolean).join(" ");
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
loading && /* @__PURE__ */ jsx(
"span",
{
className: `${baseClass}__loading`,
role: "status",
"aria-label": loadingText,
children: /* @__PURE__ */ jsx("svg", { className: `${baseClass}__spinner`, viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
"circle",
{
className: `${baseClass}__spinner-circle`,
cx: "12",
cy: "12",
r: "10",
fill: "none",
strokeWidth: "2"
}
) })
}
),
icon && iconPosition === "start" && !loading && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon ${baseClass}__icon--start`, children: icon }),
children && /* @__PURE__ */ jsx("span", { className: `${baseClass}__text`, children }),
icon && iconPosition === "end" && !loading && /* @__PURE__ */ jsx("span", { className: `${baseClass}__icon ${baseClass}__icon--end`, children: icon })
] });
if (href) {
return /* @__PURE__ */ jsx(
"a",
{
ref,
href: disabled || loading ? void 0 : href,
target,
className: classes,
...disabled || loading ? { "aria-disabled": "true" } : {},
onClick: disabled || loading ? (e) => e.preventDefault() : onClick,
...rest,
children: content
}
);
}
return /* @__PURE__ */ jsx(
"button",
{
ref,
type: "button",
className: classes,
disabled: disabled || loading,
onClick,
...rest,
children: content
}
);
}
);
Button.displayName = "Button";
var useCollapseAnimation = (isOpen, contentRef) => {
const [height, setHeight] = useState("0px");
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
if (!contentRef.current) return;
const element = contentRef.current;
if (isOpen) {
setIsAnimating(true);
element.style.height = "auto";
const scrollHeight = element.scrollHeight;
element.style.height = "0px";
requestAnimationFrame(() => {
setHeight(`${scrollHeight}px`);
});
} else {
setIsAnimating(true);
setHeight("0px");
}
const timer = setTimeout(() => {
setIsAnimating(false);
if (isOpen) {
setHeight("auto");
}
}, 300);
return () => clearTimeout(timer);
}, [isOpen]);
return { height, isAnimating };
};
var CollapsePanel = ({
header,
children,
disabled = false,
extra,
showArrow = true,
className = "",
isActive = false,
onToggle,
size = "md",
variant = "default",
expandIcon,
expandIconPosition = "end",
collapsible = "header"
}) => {
const contentRef = useRef(null);
const { height, isAnimating } = useCollapseAnimation(isActive, contentRef);
const handleHeaderClick = () => {
if (disabled || collapsible === "disabled") return;
onToggle == null ? void 0 : onToggle();
};
const handleIconClick = (e) => {
e.stopPropagation();
if (disabled || collapsible === "disabled") return;
if (collapsible === "icon") {
onToggle == null ? void 0 : onToggle();
}
};
const renderExpandIcon = () => {
if (!showArrow) return null;
if (expandIcon) {
return expandIcon({ isActive, disabled });
}
return /* @__PURE__ */ jsx(
"svg",
{
className: "duino-collapse__arrow",
viewBox: "0 0 12 12",
fill: "currentColor",
children: /* @__PURE__ */ jsx("path", { d: "M6 8.825L1.175 4L2.35 2.825L6 6.475L9.65 2.825L10.825 4L6 8.825Z" })
}
);
};
const panelClasses = [
"duino-collapse__panel",
`duino-collapse__panel--${size}`,
`duino-collapse__panel--${variant}`,
isActive && "duino-collapse__panel--active",
disabled && "duino-collapse__panel--disabled",
isAnimating && "duino-collapse__panel--animating",
className
].filter(Boolean).join(" ");
const headerClasses = [
"duino-collapse__header",
collapsible !== "disabled" && !disabled && "duino-collapse__header--clickable"
].filter(Boolean).join(" ");
return /* @__PURE__ */ jsxs("div", { className: panelClasses, children: [
/* @__PURE__ */ jsxs(
"div",
{
className: headerClasses,
onClick: collapsible === "header" ? handleHeaderClick : void 0,
role: "button",
tabIndex: disabled || collapsible === "disabled" ? -1 : 0,
"aria-expanded": isActive ? "true" : "false",
"aria-disabled": disabled ? "true" : "false",
onKeyDown: (e) => {
if ((e.key === "Enter" || e.key === " ") && collapsible === "header") {
e.preventDefault();
handleHeaderClick();
}
},
children: [
expandIconPosition === "start" && /* @__PURE__ */ jsx(
"div",
{
className: "duino-collapse__icon duino-collapse__icon--start",
onClick: collapsible === "icon" ? handleIconClick : void 0,
children: renderExpandIcon()
}
),
/* @__PURE__ */ jsx("div", { className: "duino-collapse__header-content", children: header }),
extra && /* @__PURE__ */ jsx("div", { className: "duino-collapse__extra", children: extra }),
expandIconPosition === "end" && /* @__PURE__ */ jsx(
"div",
{
className: "duino-collapse__icon duino-collapse__icon--end",
onClick: collapsible === "icon" ? handleIconClick : void 0,
children: renderExpandIcon()
}
)
]
}
),
/* @__PURE__ */ jsx(
"div",
{
className: "duino-collapse__content",
style: { height },
ref: contentRef,
children: /* @__PURE__ */ jsx("div", { className: "duino-collapse__body", children: (isActive || !isAnimating) && children })
}
)
] });
};
var Collapse = ({
items,
children,
activeKey: controlledActiveKey,
defaultActiveKey,
accordion = false,
collapsible = "header",
destroyInactivePanel = false,
size = "md",
variant = "default",
bordered = false,
ghost = false,
className = "",
onChange,
expandIcon,
expandIconPosition = "end"
}) => {
const [internalActiveKey, setInternalActiveKey] = useState(
controlledActiveKey !== void 0 ? controlledActiveKey : defaultActiveKey || (accordion ? "" : [])
);
const currentActiveKey = controlledActiveKey !== void 0 ? controlledActiveKey : internalActiveKey;
const handlePanelToggle = (key) => {
let newActiveKey;
if (accordion) {
newActiveKey = currentActiveKey === key ? "" : key;
} else {
const currentArray = Array.isArray(currentActiveKey) ? currentActiveKey : [];
if (currentArray.includes(key)) {
newActiveKey = currentArray.filter((k) => k !== key);
} else {
newActiveKey = [...currentArray, key];
}
}
if (controlledActiveKey === void 0) {
setInternalActiveKey(newActiveKey);
}
onChange == null ? void 0 : onChange(newActiveKey);
};
const isKeyActive = (key) => {
if (accordion) {
return currentActiveKey === key;
} else {
return Array.isArray(currentActiveKey) && currentActiveKey.includes(key);
}
};
const collapseClasses = [
"duino-collapse",
`duino-collapse--${size}`,
`duino-collapse--${variant}`,
bordered && "duino-collapse--bordered",
ghost && "duino-collapse--ghost",
accordion && "duino-collapse--accordion",
className
].filter(Boolean).join(" ");
if (items && items.length > 0) {
return /* @__PURE__ */ jsx("div", { className: collapseClasses, children: items.map((item) => /* @__PURE__ */ jsx(
CollapsePanel,
{
header: item.label,
disabled: item.disabled,
extra: item.extra,
showArrow: item.showArrow,
className: item.className,
isActive: isKeyActive(item.key),
onToggle: () => handlePanelToggle(item.key),
size,
variant,
expandIcon,
expandIconPosition,
collapsible,
children: (isKeyActive(item.key) || !destroyInactivePanel) && item.children
},
item.key
)) });
}
return /* @__PURE__ */ jsx("div", { className: collapseClasses, children: React2.Children.map(children, (child, index) => {
if (!React2.isValidElement(child)) return child;
const key = child.key || index;
return React2.cloneElement(child, {
isActive: isKeyActive(String(key)),
onToggle: () => handlePanelToggle(String(key)),
size,
variant,
expandIcon,
expandIconPosition,
collapsible
});
}) });
};
Collapse.displayName = "Collapse";
CollapsePanel.displayName = "CollapsePanel";
var CircleSpinner = ({ size }) => /* @__PURE__ */ jsx("svg", { className: `duino-spin__svg duino-spin__svg--${size}`, viewBox: "0 0 24 24", children: /* @__PURE__ */ jsx(
"circle",
{
className: "duino-spin__circle",
cx: "12",
cy: "12",
r: "10",
fill: "none",
strokeWidth: "2"
}
) });
var DotsSpinner = ({ size }) => /* @__PURE__ */ jsxs("div", { className: `duino-spin__dots duino-spin__dots--${size}`, children: [
/* @__PURE__ */ jsx("div", { className: "duino-spin__dot duino-spin__dot--1" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__dot duino-spin__dot--2" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__dot duino-spin__dot--3" })
] });
var PulseSpinner = ({ size }) => /* @__PURE__ */ jsxs("div", { className: `duino-spin__pulse duino-spin__pulse--${size}`, children: [
/* @__PURE__ */ jsx("div", { className: "duino-spin__pulse-ring duino-spin__pulse-ring--1" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__pulse-ring duino-spin__pulse-ring--2" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__pulse-ring duino-spin__pulse-ring--3" })
] });
var BarsSpinner = ({ size }) => /* @__PURE__ */ jsxs("div", { className: `duino-spin__bars duino-spin__bars--${size}`, children: [
/* @__PURE__ */ jsx("div", { className: "duino-spin__bar duino-spin__bar--1" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__bar duino-spin__bar--2" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__bar duino-spin__bar--3" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__bar duino-spin__bar--4" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__bar duino-spin__bar--5" })
] });
var RingSpinner = ({ size }) => /* @__PURE__ */ jsxs("div", { className: `duino-spin__ring duino-spin__ring--${size}`, children: [
/* @__PURE__ */ jsx("div", { className: "duino-spin__ring-segment duino-spin__ring-segment--1" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__ring-segment duino-spin__ring-segment--2" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__ring-segment duino-spin__ring-segment--3" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__ring-segment duino-spin__ring-segment--4" })
] });
var WaveSpinner = ({ size }) => /* @__PURE__ */ jsxs("div", { className: `duino-spin__wave duino-spin__wave--${size}`, children: [
/* @__PURE__ */ jsx("div", { className: "duino-spin__wave-bar duino-spin__wave-bar--1" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__wave-bar duino-spin__wave-bar--2" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__wave-bar duino-spin__wave-bar--3" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__wave-bar duino-spin__wave-bar--4" }),
/* @__PURE__ */ jsx("div", { className: "duino-spin__wave-bar duino-spin__wave-bar--5" })
] });
var Spin = ({
spinning = true,
size = "md",
type = "circle",
children,
tip,
className = "",
style,
wrapperClassName = "",
indicator,
delay = 0,
"aria-label": ariaLabel = "Cargando...",
id
}) => {
const [showSpinner, setShowSpinner] = React2.useState(delay === 0);
React2.useEffect(() => {
if (delay > 0 && spinning) {
const timer = setTimeout(() => setShowSpinner(true), delay);
return () => clearTimeout(timer);
} else {
setShowSpinner(spinning);
}
}, [spinning, delay]);
const renderSpinner = () => {
if (indicator) {
return indicator;
}
switch (type) {
case "dots":
return /* @__PURE__ */ jsx(DotsSpinner, { size });
case "pulse":
return /* @__PURE__ */ jsx(PulseSpinner, { size });
case "bars":
return /* @__PURE__ */ jsx(BarsSpinner, { size });
case "ring":
return /* @__PURE__ */ jsx(RingSpinner, { size });
case "wave":
return /* @__PURE__ */ jsx(WaveSpinner, { size });
case "circle":
default:
return /* @__PURE__ */ jsx(CircleSpinner, { size });
}
};
const spinClasses = [
"duino-spin",
`duino-spin--${size}`,
`duino-spin--${type}`,
showSpinner && spinning && "duino-spin--spinning",
className
].filter(Boolean).join(" ");
if (!children) {
return /* @__PURE__ */ jsx(
"div",
{
className: spinClasses,
style,
role: "status",
"aria-label": ariaLabel,
"aria-live": "polite",
id,
children: showSpinner && spinning && /* @__PURE__ */ jsxs(Fragment, { children: [
renderSpinner(),
tip && /* @__PURE__ */ jsx("div", { className: "duino-spin__tip", children: tip })
] })
}
);
}
const wrapperClasses = [
"duino-spin-wrapper",
showSpinner && spinning && "duino-spin-wrapper--spinning",
wrapperClassName
].filter(Boolean).join(" ");
return /* @__PURE__ */ jsxs("div", { className: wrapperClasses, style, children: [
showSpinner && spinning && /* @__PURE__ */ jsx("div", { className: "duino-spin-overlay", children: /* @__PURE__ */ jsxs("div", { className: spinClasses, role: "status", "aria-label": ariaLabel, id, children: [
renderSpinner(),
tip && /* @__PURE__ */ jsx("div", { className: "duino-spin__tip", children: tip })
] }) }),
/* @__PURE__ */ jsx("div", { className: showSpinner && spinning ? "duino-spin-content--blurred" : "duino-spin-content", children })
] });
};
Spin.displayName = "Spin";
var useIntersectionObserver = (elementRef, options) => {
const [isIntersecting, setIsIntersecting] = useState(false);
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const observer = new IntersectionObserver(([entry]) => {
setIsIntersecting(entry.isIntersecting);
}, options);
observer.observe(element);
return () => {
observer.unobserve(element);
observer.disconnect();
};
}, [elementRef, options]);
return isIntersecting;
};
var Image = ({
src,
alt,
width,
height,
lazy = true,
preview = false,
fallback,
placeholder,
fit = "cover",
shape = "rounded",
bordered = false,
shadow = false,
loading: customLoading,
error: customError,
onLoad,
onError,
onPreview,
rootMargin = "50px",
threshold = 0.1,
caption,
className = "",
style,
...rest
}) => {
const [imageState, setImageState] = useState("loading");
const [showPreview, setShowPreview] = useState(false);
const imageRef = useRef(null);
const imgRef = useRef(null);
const isInView = useIntersectionObserver(imageRef, {
rootMargin,
threshold
});
const shouldLoad = !lazy || isInView;
const handleLoad = (event) => {
setImageState("loaded");
onLoad == null ? void 0 : onLoad(event);
};
const handleError = (event) => {
setImageState("error");
onError == null ? void 0 : onError(event);
};
const handlePreviewClick = () => {
if (preview && imageState === "loaded") {
setShowPreview(true);
onPreview == null ? void 0 : onPreview();
}
};
useEffect(() => {
const handleKeyDown = (e) => {
if (e.key === "Escape" && showPreview) {
setShowPreview(false);
}
};
if (showPreview) {
document.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
}
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "";
};
}, [showPreview]);
const imageClasses = [
"duino-image",
`duino-image--${fit}`,
`duino-image--${shape}`,
bordered && "duino-image--bordered",
shadow && "duino-image--shadow",
preview && imageState === "loaded" && "duino-image--preview",
className
].filter(Boolean).join(" ");
const renderLoadingContent = () => {
if (customLoading) {
return customLoading;
}
return /* @__PURE__ */ jsx("div", { className: "duino-image__loading", children: /* @__PURE__ */ jsx(Spin, { size: "sm", type: "circle" }) });
};
const renderErrorContent = () => {
if (customError) {
return customError;
}
if (fallback) {
return fallback;
}
return /* @__PURE__ */ jsxs("div", { className: "duino-image__error", children: [
/* @__PURE__ */ jsx("svg", { className: "duino-image__error-icon", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }),
/* @__PURE__ */ jsx("span", { className: "duino-image__error-text", children: "Error al cargar imagen" })
] });
};
const renderPlaceholder = () => {
if (placeholder) {
return placeholder;
}
return /* @__PURE__ */ jsx("div", { className: "duino-image__placeholder", children: /* @__PURE__ */ jsx("svg", { className: "duino-image__placeholder-icon", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z" }) }) });
};
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs(
"div",
{
ref: imageRef,
className: imageClasses,
style: {
width,
height,
...style
},
onClick: handlePreviewClick,
children: [
!shouldLoad && renderPlaceholder(),
shouldLoad && /* @__PURE__ */ jsxs(Fragment, { children: [
imageState === "loading" && renderLoadingContent(),
imageState === "error" && renderErrorContent(),
/* @__PURE__ */ jsx(
"img",
{
ref: imgRef,
src,
alt,
className: `duino-image__img duino-image__img--${imageState}`,
onLoad: handleLoad,
onError: handleError,
...rest
}
),
preview && imageState === "loaded" && /* @__PURE__ */ jsx("div", { className: "duino-image__preview-overlay", children: /* @__PURE__ */ jsx("svg", { className: "duino-image__preview-icon", viewBox: "0 0 24 24", fill: "currentColor", children: /* @__PURE__ */ jsx("path", { d: "M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" }) }) })
] }),
caption && /* @__PURE__ */ jsx("div", { className: "duino-image__caption", children: caption })
]
}
),
showPreview && /* @__PURE__ */ jsx(
"div",
{
className: "duino-image__preview-modal",
onClick: () => setShowPreview(false),
children: /* @__PURE__ */ jsxs("div", { className: "duino-image__preview-content", children: [
/* @__PURE__ */ jsx(
"button",
{
className: "duino-image__preview-close",
onClick: () => setShowPreview(false),
"aria-label": "Cerrar preview",
children: "\xD7"
}
),
/* @__PURE__ */ jsx(
"img",
{
src,
alt,
className: "duino-image__preview-img"
}
),
caption && /* @__PURE__ */ jsx("div", { className: "duino-image__preview-caption", children: caption })
] })
}
)
] });
};
Image.displayName = "Image";
var Modal = ({
open,
title,
children,
onOk,
onCancel,
okText = "OK",
cancelText = "Cancel",
maskClosable = true,
centered = true,
width = 520,
destroyOnClose = false,
footer,
className = ""
}) => {
const titleId = useId();
const containerRef = useRef(null);
const previouslyFocused = useRef(null);
useEffect(() => {
var _a, _b;
if (open) {
previouslyFocused.current = document.activeElement;
document.body.classList.add("is-modal-open");
setTimeout(() => {
var _a2;
(_a2 = containerRef.current) == null ? void 0 : _a2.focus();
}, 0);
} else {
document.body.classList.remove("is-modal-open");
(_b = (_a = previouslyFocused.current) == null ? void 0 : _a.focus) == null ? void 0 : _b.call(_a);
}
return () => {
document.body.classList.remove("is-modal-open");
};
}, [open]);
useEffect(() => {
if (!open) return;
const onKey = (e) => {
var _a;
if (e.key === "Escape") onCancel == null ? void 0 : onCancel();
if (e.key === "Tab") {
const focusables = (_a = containerRef.current) == null ? void 0 : _a.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (!focusables || focusables.length === 0) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
const active = document.activeElement;
if (e.shiftKey && active === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && active === last) {
e.preventDefault();
first.focus();
}
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open, onCancel]);
if (!open && destroyOnClose) return null;
const handleMaskClick = (e) => {
if (!maskClosable) return;
if (e.target === e.currentTarget) onCancel == null ? void 0 : onCancel();
};
const styleWidth = typeof width === "number" ? `${width}px` : width != null ? width : void 0;
const modal = /* @__PURE__ */ jsxs(
"div",
{
className: `duino-modal ${open ? "duino-modal--open" : ""} ${centered ? "duino-modal--centered" : ""}`,
onMouseDown: handleMaskClick,
children: [
/* @__PURE__ */ jsx("div", { className: "duino-modal__mask" }),
/* @__PURE__ */ jsxs(
"div",
{
className: `duino-modal__container ${className}`,
role: "dialog",
"aria-modal": "true",
"aria-labelledby": title ? titleId : void 0,
tabIndex: -1,
ref: containerRef,
style: { width: styleWidth },
onMouseDown: (e) => {
e.stopPropagation();
},
children: [
/* @__PURE__ */ jsxs("div", { className: "duino-modal__header", children: [
title && /* @__PURE__ */ jsx("h2", { id: titleId, className: "duino-modal__title", children: title }),
/* @__PURE__ */ jsx(
"button",
{
type: "button",
className: "duino-modal__close",
"aria-label": "Close",
onClick: onCancel,
children: "\xD7"
}
)
] }),
/* @__PURE__ */ jsx("div", { className: "duino-modal__body", children: open || !destroyOnClose ? children : null }),
footer !== null && /* @__PURE__ */ jsx("div", { className: "duino-modal__footer", children: footer != null ? footer : /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
"button",
{
type: "button",
className: "duino-btn duino-btn--ghost",
onClick: onCancel,
children: cancelText
}
),
/* @__PURE__ */ jsx(
"button",
{
type: "button",
className: "duino-btn duino-btn--primary",
onClick: onOk,
children: okText
}
)
] }) })
]
}
)
]
}
);
return createPortal(modal, document.body);
};
var MessageCtx = createContext(null);
function MessageProvider({
children,
placement = "top-right",
maxCount = 5
}) {
const [items, setItems] = useState([]);
const timersRef = useRef(/* @__PURE__ */ new Map());
const remove = useCallback((id) => {
setItems((prev) => prev.filter((m) => m.id !== id));
const t = timersRef.current.get(id);
if (t) {
window.clearTimeout(t);
timersRef.current.delete(id);
}
}, []);
const add = useCallback((opts) => {
var _a, _b, _c, _d;
const id = Math.random().toString(36).slice(2);
const msg = { id, type: (_a = opts.type) != null ? _a : "info", content: opts.content, duration: (_b = opts.duration) != null ? _b : 2500, closable: (_c = opts.closable) != null ? _c : false };
setItems((prev) => {
const next = [...prev, msg];
if (next.length > maxCount) next.shift();
return next;
});
if (((_d = msg.duration) != null ? _d : 0) > 0) {
const t = window.setTimeout(() => remove(id), msg.duration);
timersRef.current.set(id, t);
}
return {
id,
close: () => remove(id)
};
}, [maxCount, remove]);
const portal = useMemo(() => {
const root = typeof document !== "undefined" ? document.body : null;
if (!root) return null;
return createPortal(
/* @__PURE__ */ jsx("div", { className: `duino-message duino-message--${placement}`, role: "status", "aria-live": "polite", children: items.map((m) => {
var _a;
return /* @__PURE__ */ jsxs("div", { className: `duino-message__item duino-message__item--${m.type}`, children: [
/* @__PURE__ */ jsx("span", { className: "duino-message__icon", "aria-hidden": "true", children: m.type === "success" ? "\u2713" : m.type === "error" ? "\u2715" : m.type === "warning" ? "!" : m.type === "loading" ? "\u27F3" : "i" }),
/* @__PURE__ */ jsx("div", { className: "duino-message__content", children: m.content }),
(m.closable || ((_a = m.duration) != null ? _a : 0) <= 0) && /* @__PURE__ */ jsx(
"button",
{
type: "button",
className: "duino-message__close",
onClick: () => remove(m.id),
"aria-label": "Close notification",
children: "\xD7"
}
)
] }, m.id);
}) }),
root
);
}, [items, placement, remove]);
const ctxValue = useMemo(() => ({ add, remove, placement, maxCount }), [add, remove, placement, maxCount]);
return /* @__PURE__ */ jsxs(MessageCtx.Provider, { value: ctxValue, children: [
children,
portal
] });
}
function useMessage() {
const ctx = useContext(MessageCtx);
if (!ctx) {
throw new Error("useMessage must be used within <MessageProvider>");
}
const open = ctx.add;
return {
open,
info: (content, opts) => open({ type: "info", content, ...opts }),
success: (content, opts) => open({ type: "success", content, ...opts }),
warning: (content, opts) => open({ type: "warning", content, ...opts }),
error: (content, opts) => open({ type: "error", content, ...opts }),
loading: (content, opts) => {
var _a;
return open({ type: "loading", content, ...opts, duration: (_a = opts == null ? void 0 : opts.duration) != null ? _a : 0 });
}
// por defecto persistente
};
}
var usePopoverPosition = (triggerRef, popoverRef, placement, visible, autoAdjustOverflow = true) => {
const [position, setPosition] = useState({});
const [actualPlacement, setActualPlacement] = useState(placement);
const calculatePosition = useCallback(() => {
if (!triggerRef.current || !popoverRef.current || !visible) return;
const trigger = triggerRef.current.getBoundingClientRect();
const popover = popoverRef.current.getBoundingClientRect();
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
const spacing = 8;
const arrowSize = 8;
let top = 0;
let left = 0;
let currentPlacement = placement;
const calculateForPlacement = (targetPlacement) => {
const [mainSide, alignment] = targetPlacement.split("-");
switch (mainSide) {
case "top":
top = trigger.top - popover.height - spacing - arrowSize;
left = alignment === "start" ? trigger.left : alignment === "end" ? trigger.right - popover.width : trigger.left + trigger.width / 2 - popover.width / 2;
break;
case "bottom":
top = trigger.bottom + spacing + arrowSize;
left = alignment === "start" ? trigger.left : alignment === "end" ? trigger.right - popover.width : trigger.left + trigger.width / 2 - popover.width / 2;
break;
case "left":
left = trigger.left - popover.width - spacing - arrowSize;
top = alignment === "start" ? trigger.top : alignment === "end" ? trigger.bottom - popover.height : trigger.top + trigger.height / 2 - popover.height / 2;
break;
case "right":
left = trigger.right + spacing + arrowSize;
top = alignment === "start" ? trigger.top : alignment === "end" ? trigger.bottom - popover.height : trigger.top + trigger.height / 2 - popover.height / 2;
break;
}
return { top, left };
};
const { top: calcTop, left: calcLeft } = calculateForPlacement(placement);
top = calcTop;
left = calcLeft;
if (autoAdjustOverflow) {
const wouldOverflow = {
top: top < spacing,
bottom: top + popover.height > viewport.height - spacing,
left: left < spacing,
right: left + popover.width > viewport.width - spacing
};
if (wouldOverflow.top || wouldOverflow.bottom || wouldOverflow.left || wouldOverflow.right) {
const alternativePlacements = [];
if (placement.startsWith("top")) {
alternativePlacements.push("bottom", "bottom-start", "bottom-end");
} else if (placement.startsWith("bottom")) {
alternativePlacements.push("top", "top-start", "top-end");
} else if (placement.startsWith("left")) {
alternativePlacements.push("right", "right-start", "right-end");
} else if (placement.startsWith("right")) {
alternativePlacements.push("left", "left-start", "left-end");
}
for (const altPlacement of alternativePlacements) {
const { top: altTop, left: altLeft } = calculateForPlacement(altPlacement);
const altWouldOverflow = {
top: altTop < spacing,
bottom: altTop + popover.height > viewport.height - spacing,
left: altLeft < spacing,
right: altLeft + popover.width > viewport.width - spacing
};
if (!altWouldOverflow.top && !altWouldOverflow.bottom && !altWouldOverflow.left && !altWouldOverflow.right) {
top = altTop;
left = altLeft;
currentPlacement = altPlacement;
break;
}
}
}
if (left < spacing) left = spacing;
if (left + popover.width > viewport.width - spacing) {
left = viewport.width - popover.width - spacing;
}
if (top < spacing) top = spacing;
if (top + popover.height > viewport.height - spacing) {
top = viewport.height - popover.height - spacing;
}
}
setPosition({
position: "fixed",
top: Math.round(top),
left: Math.round(left),
zIndex: 1050
});
setActualPlacement(currentPlacement);
}, [triggerRef, popoverRef, placement, visible, autoAdjustOverflow]);
useEffect(() => {
if (visible) {
const timer = setTimeout(calculatePosition, 0);
return () => clearTimeout(timer);
}
}, [visible, calculatePosition]);
useEffect(() => {
if (visible) {
window.addEventListener("scroll", calculatePosition, true);
window.addEventListener("resize", calculatePosition);
return () => {
window.removeEventListener("scroll", calculatePosition, true);
window.removeEventListener("resize", calculatePosition);
};
}
}, [visible, calculatePosition]);
return { position, actualPlacement };
};
var useClickOutside = (triggerRef, popoverRef, handler, enabled) => {
useEffect(() => {
if (!enabled) return;
const handleClickOutside = (event) => {
var _a, _b;
const target = event.target;
if (((_a = triggerRef.current) == null ? void 0 : _a.contains(target)) || ((_b = popoverRef.current) == null ? void 0 : _b.contains(target))) {
return;
}
handler();
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [triggerRef, popoverRef, handler, enabled]);
};
var Popover = ({
content,
title,
children,
trigger = "hover",
placement = "top",
visible: controlledVisible,
defaultVisible = false,
disabled = false,
className = "",
overlayClassName = "",
arrowPointAtCenter = false,
mouseEnterDelay = 100,
mouseLeaveDelay = 100,
onVisibleChange,
getPopupContainer,
destroyTooltipOnHide = false,
autoAdjustOverflow = true,
"aria-label": ariaLabel,
id
}) => {
const [internalVisible, setInternalVisible] = useState(defaultVisible);
const triggerRef = useRef(null);
const popoverRef = useRef(null);
const enterTimerRef = useRef(null);
const leaveTimerRef = useRef(null);
const isVisible = controlledVisible !== void 0 ? controlledVisible : internalVisible;
const triggers = Array.isArray(trigger) ? trigger : [trigger];
const { position, actualPlacement } = usePopoverPosition(
triggerRef,
popoverRef,
placement,
isVisible,
autoAdjustOverflow
);
const handleVisibleChange = useCallback((newVisible) => {
if (disabled) return;
if (controlledVisible === void 0) {
setInternalVisible(newVisible);
}
onVisibleChange == null ? void 0 : onVisibleChange(newVisible);
}, [disabled, controlledVisible, onVisibleChange]);
const showPopover = useCallback(() => {
if (enterTimerRef.current) {
clearTimeout(enterTimerRef.current);
}
if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current);
}
if (!isVisible) {
enterTimerRef.current = setTimeout(() => {
handleVisibleChange(true);
}, mouseEnterDelay);
}
}, [isVisible, handleVisibleChange, mouseEnterDelay]);
const hidePopover = useCallback(() => {
if (enterTimerRef.current) {
clearTimeout(enterTimerRef.current);
}
if (leaveTimerRef.current) {
clearTimeout(leaveTimerRef.current);
}
if (isVisible) {
leaveTimerRef.current = setTimeout(() => {
handleVisibleChange(false);
}, mouseLeaveDelay);
}
}, [isVisible, handleVisibleChange, mouseLeaveDelay]);
const togglePopover = useCallback(() => {
handleVisibleChange(!isVisible);
}, [isVisible, handleVisibleChange]);
useClickOutside(
triggerRef,
popoverRef,
() => handleVisibleChange(false),
isVisible && triggers.includes("click")
);
const eventHandlers = {};
if (triggers.includes("hover")) {
eventHandlers.onMouseEnter = showPopover;
eventHandlers.onMouseLeave = hidePopover;
}
if (triggers.includes("click")) {
eventHandlers.onClick = togglePopover;
}
if (triggers.includes("focus")) {
eventHandlers.onFocus = showPopover;
eventHandlers.onBlur = hidePopover;
}
if (triggers.includes("contextmenu")) {
eventHandlers.onContextMenu = (e) => {
e.preventDefault();
togglePopover();
};
}
useEffect(() => {
return () => {
if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
};
}, []);
const triggerElement = isValidElement(children) ? cloneElement(children, {
ref: (node) => {
triggerRef.current = node;
const originalRef = children.ref;
if (typeof originalRef === "function") {
originalRef(node);
} else if (originalRef && typeof originalRef === "object") {
originalRef.current = node;
}
},
...eventHandlers
}) : /* @__PURE__ */ jsx("span", { ref: triggerRef, ...eventHandlers, children });
const popoverContent = (isVisible || !destroyTooltipOnHide) && /* @__PURE__ */ jsxs(
"div",
{
ref: popoverRef,
className: `duino-popover ${overlayClassName}`,
style: {
...position,
visibility: isVisible ? "visible" : "hidden",
opacity: isVisible ? 1 : 0
},
role: "tooltip",
"aria-label": ariaLabel,
id,
children: [
/* @__PURE__ */ jsxs("div", { className: `duino-popover__content duino-popover__content--${actualPlacement}`, children: [
/* @__PURE__ */ jsx("div", { className: "duino-popover__arrow" }),
/* @__PURE__ */ jsxs("div", { className: "duino-popover__inner", children: [
title && /* @__PURE__ */ jsx("div", { className: "duino-popover__title", children: title }),
/* @__PURE__ */ jsx("div", { className: "duino-popover__body", children: content })
] })
] }),
triggers.includes("hover") && /* @__PURE__ */ jsx(
"div",
{
className: "duino-popover__hover-bridge",
onMouseEnter: showPopover,
onMouseLeave: hidePopover
}
)
]
}
);
return /* @__PURE__ */ jsxs(Fragment, { children: [
triggerElement,
getPopupContainer ? createPortal(popoverContent, getPopupContainer()) : createPortal(popoverContent, document.body)
] });
};
Popover.displayName = "Popover";
var useClickOutside2 = (ref, handler) => {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler();
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
};
var Select = ({
value: controlledValue,
defaultValue,
placeholder = "Seleccionar...",
disabled = false,
loading = false,
clearable = false,
options = [],
multiple = false,
maxTagCount = 3,
maxTagPlaceholder = (omitted) => `+${omitted.length} m\xE1s`,
searchable = false,
searchPlaceholder = "Buscar...",
notFoundContent = "No se encontraron opciones",
filterOption,
size = "md",
variant = "default",
className = "",
dropdownClassName = "",
error = false,
success = false,
onChange,
onSearch,
onFocus,
onBlur,
onDropdownVisibleChange,
dropdownMaxHeight = 300,
dropdownMatchSelectWidth = true,
getPopupContainer,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
id
}) => {
const [internalValue, setInternalValue] = useState(
controlledValue !== void 0 ? controlledValue : defaultValue || (multiple ? [] : "")
);
const [isOpen, setIsOpen] = useState(false);
const [searchValue, setSearchValue] = useState("");
const [focusedIndex, setFocusedIndex] = useState(-1);
const selectRef = useRef(null);
const dropdownRef = useRef(null);
const searchInputRef = useRef(null);
const currentValue = controlledValue !== void 0 ? controlledValue : internalValue;
useClickOutside2(selectRef, () => {
if (isOpen) {
setIsOpen(false);
onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(false);
}
});
const filteredOptions = useMemo(() => {
if (!searchable || !searchValue) return options;
const defaultFilter = (input, option) => {
const label = typeof option.label === "string" ? option.label : String(option.value);
return label.toLowerCase().includes(input.toLowerCase());
};
const filter = filterOption || defaultFilter;
return options.filter((option) => filter(searchValue, option));
}, [options, searchValue, searchable, filterOption]);
const groupedOptions = useMemo(() => {
const groups = {};
const ungrouped = [];
filteredOptions.forEach((option) => {
if (option.group) {
if (!groups[option.group]) {
groups[option.group] = [];
}
groups[option.group].push(option);
} else {
ungrouped.push(option);
}
});
return { groups, ungrouped };
}, [filteredOptions]);
const selectedOptions = useMemo(() => {
if (multiple && Array.isArray(currentValue)) {
return options.filter((option) => currentValue.includes(option.value));
} else if (!multiple && currentValue !== "" && currentValue !== void 0) {
return options.filter((option) => option.value === currentValue);
}
return [];
}, [currentValue, options, multiple]);
const handleOptionClick = (option) => {
if (option.disabled) return;
let newValue;
let selectedOption;
if (multiple) {
const currentArray = Array.isArray(currentValue) ? currentValue : [];
if (currentArray.includes(option.value)) {
newValue = currentArray.filter((v) => v !== option.value);
} else {
newValue = [...currentArray, option.value];
}
selectedOption = options.filter((opt) => newValue.includes(opt.value));
} else {
newValue = option.value;
selectedOption = option;
setIsOpen(false);
onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(false);
}
if (controlledValue === void 0) {
setInternalValue(newValue);
}
onChange == null ? void 0 : onChange(newValue, selectedOption);
};
const handleTagRemove = (optionValue, e) => {
e.stopPropagation();
if (!multiple || !Array.isArray(currentValue)) return;
const newValue = currentValue.filter((v) => v !== optionValue);
const selectedOption = options.filter((opt) => newValue.includes(opt.value));
if (controlledValue === void 0) {
setInternalValue(newValue);
}
onChange == null ? void 0 : onChange(newValue, selectedOption);
};
const handleClear = (e) => {
e.stopPropagation();
const newValue = multiple ? [] : "";
if (controlledValue === void 0) {
setInternalValue(newValue);
}
onChange == null ? void 0 : onChange(newValue, multiple ? [] : {});
};
const handleSearch = (value) => {
setSearchValue(value);
setFocusedIndex(-1);
onSearch == null ? void 0 : onSearch(value);
};
const handleKeyDown = (e) => {
if (disabled) return;
switch (e.key) {
case "Enter":
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(true);
} else if (focusedIndex >= 0 && filteredOptions[focusedIndex]) {
handleOptionClick(filteredOptions[focusedIndex]);
}
break;
case "Escape":
if (isOpen) {
setIsOpen(false);
onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(false);
}
break;
case "ArrowDown":
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
onDropdownVisibleChange == null ? void 0 : onDropdownVisibleChange(true);
} else {
setFocusedIndex(
(prev) => prev < filteredOptions.length - 1 ? prev + 1 : 0
);
}
break;
case "ArrowUp":
e.preventDefault();
if (isOpen) {
setFocusedIndex(
(prev) => prev > 0 ? prev - 1 : filteredOptions.length - 1
);
}
break;
}
};
const renderTags = () => {
if (!multiple || !Array.isArray(currentValue) || currentValue.length === 0) {
return null;
}
const visibleTags = selectedOptions.slice(0, maxTagCount);
const hiddenTags = selectedOptions.slice(maxTagCount);
return /* @__PURE__ */ jsxs("div", { className: "duino-select__tags", children: [
visibleTags.map((option) => /* @__PURE__ */ jsxs("span", { className: "duino-select__tag", children: [
/* @__PURE__ */ jsx("span", { className: "duino-select__tag-content", children: option.label }),
/* @__PURE__ */ jsx(
"button",
{
type: "button",
className: "duino-select__tag-close",
onClick: (e) => handleTagRemove(option.value, e),
"aria-label": `Remover ${option.label}`,
children: "\xD7"
}
)
] }, option.value)),
hiddenTags.length > 0 && /* @__PURE__ */ jsx("span", { className: "duino-select__tag duino-select__tag--more", children: maxTagPlaceholder(hiddenTags.map((opt) => opt.value)) })
] });
};
const renderSelectorContent = () => {
if (multiple && Array.isArray(currentValue) && currentValue.length > 0) {
return renderTags();
}
if (!multiple && currentValue !== "" && currentValue !== void 0) {
const selected = selectedOptions[0];
return selected ? /* @__PURE__ */ jsxs("div", { className: "duino-select__single-value", children: [
selected.icon && /* @__PURE__ */ jsx("span", { className: "duino-select__option-icon", children: selected.icon }),
/* @__PURE__ */ jsx("span", { children: selected.label })
] }) : null;
}
return /* @__PURE__ */ jsx("span", { className: "duino-select__placeholder", children: placeholder });
};
const selectClasses = [
"duino-select",
`duino-select--${size}`,
`duino-select--${variant}`,
isOpen && "duino-select--open",
disabled && "duino-select--disabled",
error && "duino-select--error",
success && "duino-select--success",
loading && "duino-select--loading",
multiple && "duino-select--multiple",
className
].filter(Boolean).join(" ");
const dropdownClasses = [
"duino-select__dropdown",
dropdownClassName
].filter(Boolean).join(" ");
const [dropdownStyle, setDropdownStyle] = useState({});
useEffect(() => {
if (isOpen && selectRef.current) {
const rect = selectRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - rect.bottom;
const spaceAbove = rect.top;
const dropdownHeight = Math.min(dropdownMaxHeight, 300);
const showAbove = spaceBelow < dropdownHeight && spaceAbove > spaceBelow;
setDropdownStyle({
position: "fixed",
left: rect.left,
width: dropdownMatchSelectWidth ? rect.width : "auto",
minWidth: dropdownMatchSelectWidth ? rect.width : 200,
maxHeight: dropdownMaxHeight,
zIndex: 1050,
...showAbove ? {
bottom: viewportHeight - rect.top
} : {
top: rect.bottom
}
});
}
}, [isOpen, dropdownMaxHeight, dropdownMatchSelectWidth]);
useEffect(() => {
if (isOpen && searchable && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [isOpen, searchable]);
const dropdownContent = isOpen && /* @__PURE__ */ jsxs(
"div",
{
ref: dropdownRef,
className: dropdownClasses,
style: dropdownStyle,
id: `${id || "select"}-dropdown`,
role: "listbox",
children: [
searchable && /* @__PURE__ */ jsx("div", { className: "duino-select__search", children: /* @__PURE__ */ jsx(
"input",
{
ref: searchInputRef,
type: "text",
className: "duino-select__search-input",
placeholder: searchPlaceholder,
value: searchValue,
onChange: (e) => handleSearch(e.target.value),
"aria-label": "Buscar opciones"
}
) }),
/* @__PURE__ */ jsx("div", { className: "duino-select__options", children: filteredOptions.length === 0 ? /* @__PURE__ */ jsx("div", { className: "duino-select__empty", children: notFoundContent }) : /* @__PURE__ */ jsxs(Fragment, { children: [
groupedOptions.ungrouped.map((option, index) => {
const isSelected