aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
247 lines (244 loc) • 8.52 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { ChevronsLeft, ChevronLeft, MoreHorizontal, ChevronRight, ChevronsRight } from 'lucide-react';
import React from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { announceToScreenReader, createPaginationA11y } from '../../utils/a11y.js';
/**
* GlassPagination component
* A glassmorphism pagination component
*/
const GlassPagination = ({
currentPage,
totalPages,
onPageChange,
maxPageButtons = 7,
showFirstLast = true,
showPrevNext = true,
size = "md",
className,
disabled = false,
loading = false,
"aria-label": ariaLabel = "Pagination",
announcePageChanges = true
}) => {
const getPageNumbers = () => {
const pages = [];
const halfMax = Math.floor(maxPageButtons / 2);
if (totalPages <= maxPageButtons) {
// Show all pages if total is less than max buttons
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
} else {
// Always show first page
pages.push(1);
// Calculate start and end of middle section
let start = Math.max(2, currentPage - halfMax);
let end = Math.min(totalPages - 1, currentPage + halfMax);
// Adjust if we're near the beginning
if (currentPage <= halfMax + 1) {
end = Math.min(totalPages - 1, maxPageButtons - 1);
}
// Adjust if we're near the end
else if (currentPage >= totalPages - halfMax) {
start = Math.max(2, totalPages - maxPageButtons + 2);
}
// Add ellipsis after first page if needed
if (start > 2) {
pages.push("...");
}
// Add middle pages
for (let i = start; i <= end; i++) {
pages.push(i);
}
// Add ellipsis before last page if needed
if (end < totalPages - 1) {
pages.push("...");
}
// Always show last page
if (totalPages > 1) {
pages.push(totalPages);
}
}
return pages;
};
const handlePageChange = page => {
if (page >= 1 && page <= totalPages && page !== currentPage && !disabled && !loading) {
onPageChange(page);
// Announce page change to screen readers
if (announcePageChanges) {
announceToScreenReader(`Moved to page ${page} of ${totalPages}`, "polite");
}
}
};
const pageNumbers = getPageNumbers();
// Ink highlight for active page number
const pagesRef = React.useRef(null);
const pageRefMap = React.useRef(new Map());
const [ink, setInk] = React.useState({
left: 0,
width: 0
});
const updateInk = React.useCallback(() => {
const activeEl = pageRefMap.current.get(currentPage);
const container = pagesRef.current;
if (!activeEl || !container) return;
const cr = container.getBoundingClientRect();
const ar = activeEl.getBoundingClientRect();
setInk({
left: ar.left - cr.left,
width: ar.width
});
}, [currentPage]);
const registerPageRef = (page, el) => {
const map = pageRefMap.current;
if (el) map.set(page, el);else map.delete(page);
// Defer measure to next frame
requestAnimationFrame(updateInk);
};
React.useEffect(() => {
updateInk();
const onResize = () => updateInk();
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [updateInk]);
return jsx("nav", {
"data-glass-component": true,
"aria-label": ariaLabel,
role: "navigation",
children: jsxs(OptimizedGlassCore, {
intent: "neutral",
elevation: "level2",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
className: cn("inline-flex items-center glass-gap-1 glass-p-1 glass-backdrop-blur-md ring-1 ring-white/10 bg-white/5", disabled && "opacity-50 pointer-events-none", className),
"aria-busy": loading || undefined,
children: [showFirstLast && totalPages > 3 && jsx(GlassPaginationItem, {
onClick: () => handlePageChange(1),
disabled: currentPage === 1 || disabled || loading,
size: size,
ariaLabel: "First page",
children: jsx(ChevronsLeft, {
className: 'w-4 h-4'
})
}), showPrevNext && jsx(GlassPaginationItem, {
onClick: () => handlePageChange(currentPage - 1),
disabled: currentPage === 1 || disabled || loading,
size: size,
ariaLabel: "Previous page",
children: jsx(ChevronLeft, {
className: 'w-4 h-4'
})
}), jsxs("div", {
ref: pagesRef,
className: 'relative glass-inline-flex glass-items-center glass-gap-1',
children: [jsx("div", {
className: 'absolute bottom-0 h-0-5 glass-surface-primary transition-all duration-200',
style: {
left: ink.left,
width: ink.width
}
}), pageNumbers.map((page, index) => jsx(React.Fragment, {
children: page === "..." ? jsx(GlassPaginationItem, {
disabled: true,
size: size,
children: jsx(MoreHorizontal, {
className: 'w-4 h-4'
})
}) : jsx(GlassPaginationItem, {
isActive: page === currentPage,
onClick: () => handlePageChange(page),
disabled: disabled || loading,
size: size,
ariaLabel: `Page ${page}`,
innerRef: el => registerPageRef(page, el),
children: page
})
}, index))]
}), showPrevNext && jsx(GlassPaginationItem, {
onClick: () => handlePageChange(currentPage + 1),
disabled: currentPage === totalPages || disabled || loading,
size: size,
ariaLabel: "Next page",
children: jsx(ChevronRight, {
className: 'w-4 h-4'
})
}), showFirstLast && totalPages > 3 && jsx(GlassPaginationItem, {
onClick: () => handlePageChange(totalPages),
disabled: currentPage === totalPages || disabled || loading,
size: size,
ariaLabel: "Last page",
children: jsx(ChevronsRight, {
className: 'w-4 h-4'
})
}), loading && jsx("div", {
className: "glass-ml-2",
children: jsx("div", {
className: 'w-4 h-4 glass-border-2 glass-border-white/30 glass-border-t-white/60 glass-radius-full animate-spin'
})
})]
})
});
};
/**
* GlassPaginationItem component
* Individual pagination item/button
*/
const GlassPaginationItem = ({
children,
isActive = false,
disabled = false,
onClick,
size = "md",
className,
ariaLabel,
innerRef
}) => {
// Create accessibility attributes
const a11yProps = createPaginationA11y({
label: ariaLabel,
current: isActive,
disabled
});
const sizeClasses = {
sm: "h-8 w-8 glass-text-sm",
md: "h-10 w-10 glass-text-base",
lg: "h-12 w-12 glass-text-lg"
};
return jsx(MotionFramer, {
preset: "none",
children: jsx(OptimizedGlassCore, {
intent: "neutral",
elevation: isActive ? "level3" : "level1",
intensity: "medium",
depth: 2,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
liftOnHover: true,
press: true,
ref: innerRef,
className: cn("relative flex items-center justify-center font-medium glass-radius-md", "glass-backdrop-blur-md border border-white/20", "transition-all duration-200 glass-sheen", "focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-transparent", "disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:transform-none", sizeClasses[size], {
"bg-black/40 glass-text-primary shadow-lg ring-1 ring-white/30 border-white/30": isActive,
"bg-black/20 hover:bg-black/30 glass-text-primary/80 hover:glass-text-primary hover:-translate-y-0.5 border-white/20 hover:border-white/30": !isActive && !disabled
}, className),
onClick: onClick,
...a11yProps,
children: children
})
});
};
export { GlassPagination, GlassPaginationItem };
//# sourceMappingURL=GlassPagination.js.map