aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
317 lines (314 loc) • 11.8 kB
JavaScript
'use client';
import { jsx, jsxs } from 'react/jsx-runtime';
import { GlassButton } from '../button/GlassButton.js';
import { cn } from '../../lib/utilsComprehensive.js';
import React, { forwardRef, useState, createContext, useContext } 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';
const TabsContext = /*#__PURE__*/createContext(null);
const useTabsContext = () => {
const context = useContext(TabsContext);
if (!context) {
throw new Error("Tabs components must be used within a GlassTabs component");
}
return context;
};
const GlassTabs = /*#__PURE__*/forwardRef(({
value,
defaultValue,
onValueChange,
orientation = "horizontal",
variant = "default",
activationMode = "automatic",
className,
children,
...props
}, ref) => {
const [internalValue, setInternalValue] = useState(defaultValue || "");
const currentValue = value ?? internalValue;
const listRef = React.useRef(null);
const triggerMapRef = React.useRef(new Map());
const [ink, setInk] = React.useState({
left: 0,
width: 0
});
const updateInk = React.useCallback(() => {
if (!listRef.current) return;
const active = triggerMapRef.current.get(currentValue);
if (!active) return;
const lr = listRef.current.getBoundingClientRect();
const ar = active.getBoundingClientRect();
setInk({
left: ar.left - lr.left,
width: ar.width
});
}, [currentValue]);
const scheduleInkUpdate = React.useCallback(() => {
if (typeof window === "undefined" || process.env.JEST_WORKER_ID) {
updateInk();
return;
}
return window.requestAnimationFrame(updateInk);
}, [updateInk]);
const registerTrigger = (val, el) => {
const map = triggerMapRef.current;
if (el) map.set(val, el);else map.delete(val);
// Update ink when triggers mount/unmount
scheduleInkUpdate();
};
React.useEffect(() => {
updateInk();
}, [updateInk]);
React.useEffect(() => {
const handle = () => {
updateInk();
};
window.addEventListener("resize", handle);
return () => window.removeEventListener("resize", handle);
}, [updateInk]);
const handleValueChange = newValue => {
if (!value) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
};
const contextValue = {
value: currentValue,
onValueChange: handleValueChange,
orientation,
variant,
activationMode,
registerTrigger,
listRef,
ink
};
return jsx(TabsContext.Provider, {
"data-glass-component": true,
value: contextValue,
children: jsx("div", {
ref: ref,
className: cn("glass-tabs", orientation === "vertical" ? "flex glass-gap-6" : "w-full", className),
"data-orientation": orientation,
...props,
children: children
})
});
});
GlassTabs.displayName = "GlassTabs";
const GlassTabsList = /*#__PURE__*/forwardRef(({
className,
loop = true,
children,
...props
}, ref) => {
const {
orientation,
variant,
listRef,
ink
} = useTabsContext();
const variantStyles = {
default: cn("glass-tabs-list", "glass-radius-xl glass-p-1 glass-gap-2"),
pills: cn("glass-tabs-list-pills", "rounded-2xl glass-p-1.5 glass-gap-1"),
underline: cn("glass-tabs-list-underline", "border-b border-border/20", "gap-8 glass-px-1"),
minimal: cn("glass-tabs-list-minimal", "glass-gap-4")
};
const shouldUseGlass = variant === "default" || variant === "pills";
const commonA11y = {
role: "tablist",
"aria-orientation": orientation,
onKeyDown: e => {
const isHorizontal = orientation === "horizontal";
const prevKey = isHorizontal ? "ArrowLeft" : "ArrowUp";
const nextKey = isHorizontal ? "ArrowRight" : "ArrowDown";
if (e.key !== prevKey && e.key !== nextKey && e.key !== "Home" && e.key !== "End") return;
const container = e.currentTarget;
const tabs = Array.from(container.querySelectorAll('[role="tab"]'));
if (tabs.length === 0) return;
const current = document.activeElement;
let index = Math.max(0, tabs.findIndex(t => t === current));
if (e.key === prevKey) index = index > 0 ? index - 1 : loop ? tabs.length - 1 : 0;
if (e.key === nextKey) index = index < tabs.length - 1 ? index + 1 : loop ? 0 : tabs.length - 1;
if (e.key === "Home") index = 0;
if (e.key === "End") index = tabs.length - 1;
const next = tabs[index];
next?.focus();
e.preventDefault();
}
};
return shouldUseGlass ? jsxs(OptimizedGlassCore, {
ref: node => {
listRef.current = node;
if (typeof ref === "function") ref(node);else if (ref) ref.current = node;
},
variant: "ethereal",
elevation: "level1",
intensity: "subtle",
depth: 1,
tint: "neutral",
border: "subtle",
animation: "none",
performanceMode: "medium",
className: cn("inline-flex items-center justify-start", orientation === "horizontal" ? "flex-row" : "flex-col", variantStyles[variant], variant === "pills" ? "rounded-2xl" : "glass-radius-xl", className),
...commonA11y,
...props,
children: [variant === "underline" && orientation === "horizontal" && jsx("div", {
className: 'absolute bottom-0 left-0 right-0 h-0-5',
children: jsx("div", {
className: 'absolute glass-h-full glass-surface-primary transition-all duration-200',
style: {
left: ink.left,
width: ink.width
}
})
}), children]
}) : jsxs("div", {
ref: node => {
listRef.current = node;
if (typeof ref === "function") ref(node);else if (ref) ref.current = node;
},
className: cn("inline-flex items-center justify-start", orientation === "horizontal" ? "flex-row" : "flex-col", variantStyles[variant], className),
...commonA11y,
...props,
children: [jsx("div", {
className: 'relative glass-w-full'
}), children]
});
});
GlassTabsList.displayName = "GlassTabsList";
const GlassTabsTrigger = /*#__PURE__*/forwardRef(({
value,
icon,
badge,
disabled = false,
className,
children,
onClick,
...props
}, ref) => {
const {
value: selectedValue,
onValueChange,
variant,
activationMode,
registerTrigger
} = useTabsContext();
const isSelected = selectedValue === value;
const handleClick = event => {
if (!disabled) {
onValueChange(value);
onClick?.(event);
}
};
const handleKeyDown = event => {
const tabButton = event.currentTarget;
const tablist = tabButton.closest('[role="tablist"]');
if (!tablist) return;
const isHorizontal = tablist.getAttribute("aria-orientation") !== "vertical";
const prevKey = isHorizontal ? "ArrowLeft" : "ArrowUp";
const nextKey = isHorizontal ? "ArrowRight" : "ArrowDown";
const tabs = Array.from(tablist.querySelectorAll('[role="tab"]'));
const index = Math.max(0, tabs.findIndex(t => t === tabButton));
let targetIndex = index;
if (event.key === prevKey) targetIndex = index > 0 ? index - 1 : tabs.length - 1;
if (event.key === nextKey) targetIndex = index < tabs.length - 1 ? index + 1 : 0;
if (event.key === "Home") targetIndex = 0;
if (event.key === "End") targetIndex = tabs.length - 1;
if (targetIndex !== index) {
const next = tabs[targetIndex];
next?.focus();
if (activationMode === "automatic") {
onValueChange(next?.getAttribute("data-value") || "");
}
event.preventDefault();
}
if ((event.key === "Enter" || event.key === " ") && !disabled) {
onValueChange(value);
event.preventDefault();
}
};
const baseStyles = cn("inline-flex items-center justify-center glass-gap-2", "whitespace-nowrap glass-radius-lg glass-px-3 glass-py-2", "glass-text-sm font-medium transition-all duration-200", "focus-visible:outline-none focus-visible:ring-2", "focus-visible:ring-primary focus-visible:ring-offset-2", "disabled:pointer-events-none disabled:opacity-50");
const variantStyles = {
default: cn(isSelected ? "bg-background/90 text-foreground shadow-md border border-border/20" : "glass-text-secondary hover:text-foreground hover:bg-muted/50"),
pills: cn(isSelected ? "bg-primary text-primary-foreground shadow-lg" : "glass-text-secondary hover:text-foreground hover:bg-background/50"),
underline: cn("relative glass-px-1 glass-py-3 rounded-none", isSelected ? "text-primary after:absolute after:bottom-0 after:left-0 after:right-0 after:h-0.5 after:bg-primary" : "glass-text-secondary hover:text-foreground"),
minimal: cn("glass-px-2 glass-py-1 glass-radius-md", isSelected ? "text-primary bg-primary/10" : "glass-text-secondary hover:text-foreground hover:bg-muted/30")
};
// Convert Booleanish ARIA attributes to boolean
const buttonProps = {
...props,
"aria-pressed": props["aria-pressed"] === "true" ? true : props["aria-pressed"] === "false" ? false : props["aria-pressed"] === "mixed" ? undefined : props["aria-pressed"],
"aria-expanded": props["aria-expanded"] === "true" ? true : props["aria-expanded"] === "false" ? false : props["aria-expanded"]
};
return jsx(MotionFramer, {
preset: "scaleIn",
className: 'relative',
children: jsxs(GlassButton, {
ref: node => {
if (typeof ref === "function") ref(node);else if (ref) ref.current = node;
registerTrigger(value, node);
},
role: "tab",
id: `trigger-${value}`,
"aria-selected": !!isSelected,
"aria-controls": `content-${value}`,
"data-value": value,
"data-state": isSelected ? "active" : "inactive",
className: cn(baseStyles, variantStyles[variant], className),
disabled: disabled,
onClick: handleClick,
onKeyDown: handleKeyDown,
...buttonProps,
children: [icon && jsx("span", {
className: 'shrink-0',
children: icon
}), children && jsx("span", {
className: 'truncate',
children: children
}), badge && jsx("span", {
className: cn("glass-ml-2 glass-radius-full glass-px-2 glass-py-0.5 glass-text-xs", isSelected ? "bg-primary-foreground/20 text-primary-foreground" : "bg-background/50 glass-text-secondary"),
children: badge
}), variant === "underline" && isSelected && jsx(MotionFramer, {
preset: "slideUp",
className: 'absolute bottom-0 left-0 right-0 h-0-5 glass-surface-primary'
})]
})
});
});
GlassTabsTrigger.displayName = "GlassTabsTrigger";
const GlassTabsContent = /*#__PURE__*/forwardRef(({
value,
forceMount = false,
className,
children,
...props
}, ref) => {
const {
value: selectedValue
} = useTabsContext();
const isSelected = selectedValue === value;
if (!isSelected && !forceMount) {
return null;
}
return jsx(MotionFramer, {
preset: "fadeIn",
className: cn("glass-tabs-content", "mt-6 focus-visible:outline-none", "focus-visible:ring-2 focus-visible:ring-primary", "focus-visible:ring-offset-2", !isSelected && "hidden", className),
children: jsx("div", {
ref: ref,
role: "tabpanel",
"aria-labelledby": `trigger-${value}`,
id: `content-${value}`,
tabIndex: 0,
"data-state": isSelected ? "active" : "inactive",
...props,
children: children
})
});
});
GlassTabsContent.displayName = "GlassTabsContent";
export { GlassTabs, GlassTabsContent, GlassTabsList, GlassTabsTrigger, GlassTabs as Tabs, GlassTabsContent as TabsContent, GlassTabsList as TabsList, GlassTabsTrigger as TabsTrigger };
//# sourceMappingURL=GlassTabs.js.map