aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
493 lines (489 loc) • 19.6 kB
JavaScript
'use client';
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
import { forwardRef, useState, useCallback, useEffect } 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 { useA11yId } from '../../utils/a11y.js';
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
const defaultRules = ["img-alt", "heading-order", "color-contrast", "aria-label", "keyboard-navigation", "focus-visible", "semantic-html", "lang-attribute", "aria-describedby", "aria-labelledby"];
const GlassA11yAuditor = /*#__PURE__*/forwardRef(({
children,
showPanel = true,
autoAudit = false,
rules = defaultRules,
className = "",
onAuditComplete,
onIssueClick,
respectMotionPreference = true,
id,
...props
}, ref) => {
const [isAuditing, setIsAuditing] = useState(false);
const [auditResult, setAuditResult] = useState(null);
const [selectedIssue, setSelectedIssue] = useState(null);
const [filter, setFilter] = useState("all");
const [highlightedElement, setHighlightedElement] = useState(null);
useReducedMotion();
const componentId = id || useA11yId("a11y-auditor");
// Run accessibility audit
const runAudit = useCallback(async () => {
setIsAuditing(true);
try {
const issues = [];
// Check images for alt text
if (rules.includes("img-alt")) {
const images = document.querySelectorAll("img");
images.forEach((img, index) => {
if (!img.getAttribute("alt") && !img.getAttribute("aria-label")) {
issues.push({
id: `img-alt-${index}`,
type: "error",
rule: "img-alt",
message: "Image missing alt text",
element: "img",
code: img.outerHTML.substring(0, 100) + "...",
suggestion: "Add alt attribute describing the image content",
wcag: "1.1.1 Non-text Content"
});
}
});
}
// Check heading hierarchy
if (rules.includes("heading-order")) {
const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
let lastLevel = 0;
headings.forEach((heading, index) => {
const level = parseInt(heading.tagName.charAt(1));
if (level > lastLevel + 1 && lastLevel !== 0) {
issues.push({
id: `heading-order-${index}`,
type: "warning",
rule: "heading-order",
message: `Skipped heading level: went from H${lastLevel} to H${level}`,
element: heading.tagName.toLowerCase(),
code: heading.outerHTML,
suggestion: "Ensure heading hierarchy is sequential",
wcag: "1.3.1 Info and Relationships"
});
}
lastLevel = level;
});
}
// Check ARIA labels
if (rules.includes("aria-label")) {
const interactiveElements = document.querySelectorAll("button, input, select, textarea, [role]");
interactiveElements.forEach((element, index) => {
const hasLabel = element.getAttribute("aria-label") || element.getAttribute("aria-labelledby") || element.textContent?.trim() || element.getAttribute("placeholder");
if (!hasLabel && !element.getAttribute("aria-hidden")) {
issues.push({
id: `aria-label-${index}`,
type: "error",
rule: "aria-label",
message: "Interactive element missing accessible label",
element: element.tagName.toLowerCase(),
code: element.outerHTML.substring(0, 100) + "...",
suggestion: "Add aria-label, aria-labelledby, or visible text",
wcag: "4.1.2 Name, Role, Value"
});
}
});
}
// Check focus visibility
if (rules.includes("focus-visible")) {
const focusableElements = document.querySelectorAll("button, input, select, textarea, a, [tabindex]");
focusableElements.forEach((element, index) => {
const computedStyle = window.getComputedStyle(element);
const hasVisibleFocus = computedStyle.outline !== "none" || computedStyle.boxShadow !== "none" || element.classList.contains("focus-visible");
if (!hasVisibleFocus) {
issues.push({
id: `focus-visible-${index}`,
type: "warning",
rule: "focus-visible",
message: "Element may not have visible focus indicator",
element: element.tagName.toLowerCase(),
code: element.outerHTML.substring(0, 100) + "...",
suggestion: "Ensure focus is visible when navigating with keyboard",
wcag: "2.4.7 Focus Visible"
});
}
});
}
// Check semantic HTML
if (rules.includes("semantic-html")) {
const divs = document.querySelectorAll("div[role]");
divs.forEach((div, index) => {
const role = div.getAttribute("role");
const semanticAlternatives = {
button: "button",
heading: "h1-h6",
list: "ul/ol",
listitem: "li",
navigation: "nav",
main: "main",
article: "article",
section: "section"
};
if (role && semanticAlternatives[role]) {
issues.push({
id: `semantic-html-${index}`,
type: "info",
rule: "semantic-html",
message: `Consider using semantic ${semanticAlternatives[role]} instead of div[role="${role}"]`,
element: "div",
code: div.outerHTML.substring(0, 100) + "...",
suggestion: `Replace with <${semanticAlternatives[role]}> element`,
wcag: "1.3.1 Info and Relationships"
});
}
});
}
// Check language attribute
if (rules.includes("lang-attribute")) {
const html = document.documentElement;
if (!html.getAttribute("lang")) {
issues.push({
id: "lang-attribute",
type: "error",
rule: "lang-attribute",
message: "Missing lang attribute on html element",
element: "html",
suggestion: "Add lang attribute to specify document language",
wcag: "3.1.1 Language of Page"
});
}
}
// Calculate score (0-100)
const errorWeight = 10;
const warningWeight = 5;
const infoWeight = 1;
const errors = issues.filter(i => i.type === "error").length;
const warnings = issues.filter(i => i.type === "warning").length;
const infos = issues.filter(i => i.type === "info").length;
const totalWeight = errors * errorWeight + warnings * warningWeight + infos * infoWeight;
const score = Math.max(0, 100 - totalWeight);
const result = {
score,
issues,
summary: {
errors,
warnings,
info: infos
},
timestamp: Date.now()
};
setAuditResult(result);
onAuditComplete?.(result);
} catch (error) {
if (process.env.NODE_ENV === "development") {
console.error("A11y audit failed:", error);
}
} finally {
setIsAuditing(false);
}
}, [rules, onAuditComplete]);
// Auto-run audit
useEffect(() => {
if (autoAudit) {
runAudit();
}
}, [autoAudit, runAudit]);
// Filter issues
const filteredIssues = auditResult?.issues.filter(issue => filter === "all" || issue.type === filter) || [];
// Handle issue selection
const handleIssueClick = issue => {
setSelectedIssue(issue);
onIssueClick?.(issue);
// Try to highlight the element
if (issue.element && issue.code) {
// Element highlighting with scroll-to-view functionality
const elements = document.querySelectorAll(issue.element);
if (elements.length > 0) {
setHighlightedElement(elements[0]);
elements[0].scrollIntoView({
behavior: "smooth",
block: "center"
});
// Add temporary highlight
elements[0].classList.add("a11y-highlight");
setTimeout(() => {
elements[0].classList.remove("a11y-highlight");
setHighlightedElement(null);
}, 3000);
}
}
};
// Get score color
const getScoreColor = score => {
if (score >= 90) return "text-green-400";
if (score >= 70) return "text-yellow-400";
return "text-red-400";
};
// Get issue type color
const getIssueTypeColor = type => {
switch (type) {
case "error":
return "text-red-400 bg-red-500/10 border-red-500/20";
case "warning":
return "text-yellow-400 bg-yellow-500/10 border-yellow-500/20";
case "info":
return "text-blue-400 bg-blue-500/10 border-blue-500/20";
}
};
if (!showPanel) {
return jsx(Fragment, {
children: children
});
}
return jsx(MotionFramer, {
preset: "fadeIn",
className: `flex flex-col h-full ${className}`,
children: jsxs("div", {
ref: ref,
id: componentId,
className: "glass-flex glass-flex-col glass-h-full",
role: "application",
"aria-label": "Accessibility auditing tool",
"aria-describedby": `${componentId}-description`,
...props,
children: [jsx("div", {
id: `${componentId}-description`,
className: 'sr-only',
children: "Accessibility audit tool for analyzing and improving web accessibility"
}), jsxs(OptimizedGlassCore, {
className: 'glass-p-4 mb-4',
intensity: "medium",
elevation: "level1",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-4',
children: [jsx("h3", {
className: 'glass-text-lg font-semibold text-primary',
children: "Accessibility Audit"
}), jsx("button", {
onClick: runAudit,
disabled: isAuditing,
className: 'glass-px-4 glass-py-2 glass-surface-blue/20 glass-text-secondary glass-radius-md hover:glass-surface-blue/30 disabled:opacity-50 disabled:cursor-not-allowed transition-colors glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard',
"aria-label": isAuditing ? "Auditing accessibility issues" : "Run accessibility audit",
children: isAuditing ? "Auditing..." : "Run Audit"
})]
}), auditResult && jsxs("div", {
className: 'glass-grid glass-grid-cols-2 md:grid-cols-4 glass-gap-4',
children: [jsxs("div", {
className: 'text-center',
children: [jsx("div", {
className: `glass-text-2xl font-bold ${getScoreColor(auditResult.score)}`,
children: auditResult.score
}), jsx("div", {
className: 'glass-text-sm text-primary/70',
children: "Score"
})]
}), jsxs("div", {
className: 'text-center',
children: [jsx("div", {
className: 'glass-text-2xl font-bold text-primary',
children: auditResult.summary.errors
}), jsx("div", {
className: 'glass-text-sm text-primary/70',
children: "Errors"
})]
}), jsxs("div", {
className: 'text-center',
children: [jsx("div", {
className: 'glass-text-2xl font-bold text-primary',
children: auditResult.summary.warnings
}), jsx("div", {
className: 'glass-text-sm text-primary/70',
children: "Warnings"
})]
}), jsxs("div", {
className: 'text-center',
children: [jsx("div", {
className: 'glass-text-2xl font-bold text-primary',
children: auditResult.summary.info
}), jsx("div", {
className: 'glass-text-sm text-primary/70',
children: "Info"
})]
})]
})]
}), jsxs("div", {
className: "glass-flex glass-flex-1 glass-gap-4",
children: [jsxs(OptimizedGlassCore, {
className: "glass-flex-1 glass-p-4",
blur: "medium",
elevation: "level1",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-justify-between mb-4',
children: [jsx("h4", {
className: 'text-md font-semibold text-primary',
children: "Issues"
}), jsx("div", {
className: "glass-flex glass-gap-2",
children: ["all", "error", "warning", "info"].map(type => jsx("button", {
onClick: e => setFilter(type),
className: `glass-px-3 glass-py-1 glass-text-xs glass-radius-md capitalize transition-colors ${filter === type ? "bg-white/20 glass-text-primary" : "glass-text-primary/70 hover:glass-text-primary hover:bg-white/10"}`,
children: type
}, type))
})]
}), jsxs("div", {
className: 'glass-gap-2 max-h-96 overflow-y-auto',
children: [filteredIssues.map(issue => jsx("button", {
onClick: e => handleIssueClick(issue),
className: `w-full text-left glass-p-3 glass-radius-md border transition-colors ${getIssueTypeColor(issue.type)} ${selectedIssue?.id === issue.id ? "ring-2 ring-white/50" : ""}`,
children: jsx("div", {
className: "glass-flex glass-items-start glass-justify-between",
children: jsxs("div", {
className: "glass-flex-1",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 mb-1',
children: [jsx("span", {
className: 'glass-text-sm font-medium',
children: issue.rule
}), jsxs("span", {
className: 'glass-text-xs opacity-70',
children: ["WCAG ", issue.wcag]
})]
}), jsx("p", {
className: 'glass-text-sm opacity-90',
children: issue.message
}), issue.element && jsx("code", {
className: 'glass-text-xs opacity-70 glass-surface-dark/20 glass-px-1 glass-py-0.5 glass-radius-md glass-mt-1 inline-block',
children: issue.element
})]
})
})
}, issue.id)), filteredIssues.length === 0 && jsx("div", {
className: 'text-center glass-py-8 text-primary/50',
children: auditResult ? "No issues found!" : "Run audit to check accessibility"
})]
})]
}), selectedIssue && jsxs(OptimizedGlassCore, {
className: 'w-80 glass-p-4',
blur: "medium",
elevation: "level1",
children: [jsx("h4", {
className: 'text-md font-semibold text-primary mb-4',
children: "Issue Details"
}), jsxs("div", {
className: "glass-gap-4",
children: [jsxs("div", {
children: [jsx("label", {
className: 'block glass-text-sm text-primary/70 mb-1',
children: "Rule"
}), jsx("div", {
className: 'glass-text-sm font-medium text-primary',
children: selectedIssue.rule
})]
}), jsxs("div", {
children: [jsx("label", {
className: 'block glass-text-sm text-primary/70 mb-1',
children: "Type"
}), jsx("span", {
className: `glass-px-2 glass-py-1 glass-text-xs glass-radius-md capitalize ${getIssueTypeColor(selectedIssue.type)}`,
children: selectedIssue.type
})]
}), jsxs("div", {
children: [jsx("label", {
className: 'block glass-text-sm text-primary/70 mb-1',
children: "WCAG Guideline"
}), jsx("div", {
className: 'glass-text-sm text-primary',
children: selectedIssue.wcag
})]
}), jsxs("div", {
children: [jsx("label", {
className: 'block glass-text-sm text-primary/70 mb-1',
children: "Message"
}), jsx("p", {
className: 'glass-text-sm text-primary',
children: selectedIssue.message
})]
}), selectedIssue.suggestion && jsxs("div", {
children: [jsx("label", {
className: 'block glass-text-sm text-primary/70 mb-1',
children: "Suggestion"
}), jsx("p", {
className: "glass-text-sm glass-text-secondary",
children: selectedIssue.suggestion
})]
}), selectedIssue.code && jsxs("div", {
children: [jsx("label", {
className: 'block glass-text-sm text-primary/70 mb-1',
children: "Element Code"
}), jsx("pre", {
className: 'glass-text-xs glass-surface-dark/20 glass-p-2 glass-radius-md overflow-x-auto',
children: jsx("code", {
children: selectedIssue.code
})
})]
})]
})]
})]
}), jsx("style", {
children: `
.a11y-highlight {
outline: 2px solid #ff6b6b !important;
outline-offset: 2px !important;
animation: a11y-pulse 1s ease-in-out infinite alternate;
}
@keyframes a11y-pulse {
from { outline-color: #ff6b6b; }
to { outline-color: #ff4444; }
}
`
})]
})
});
});
GlassA11yAuditor.displayName = "GlassA11yAuditor";
// Hook for programmatic audit
const useA11yAudit = () => {
const [result, setResult] = useState(null);
const [isAuditing, setIsAuditing] = useState(false);
const audit = useCallback(async (rules = defaultRules) => {
setIsAuditing(true);
try {
// Simplified audit for programmatic use
const issues = [];
// Programmatic accessibility checks
const images = document.querySelectorAll("img");
images.forEach((img, index) => {
if (!img.getAttribute("alt")) {
issues.push({
id: `img-${index}`,
type: "error",
rule: "img-alt",
message: "Image missing alt text",
element: "img",
wcag: "1.1.1"
});
}
});
const score = Math.max(0, 100 - issues.length * 10);
const auditResult = {
score,
issues,
summary: {
errors: issues.filter(i => i.type === "error").length,
warnings: issues.filter(i => i.type === "warning").length,
info: issues.filter(i => i.type === "info").length
},
timestamp: Date.now()
};
setResult(auditResult);
return auditResult;
} finally {
setIsAuditing(false);
}
}, []);
return {
result,
isAuditing,
audit
};
};
export { GlassA11yAuditor, useA11yAudit };
//# sourceMappingURL=GlassA11yAuditor.js.map