UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

493 lines (489 loc) 19.6 kB
'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