diff --git a/docs/accessibility-guidelines.md b/docs/accessibility-guidelines.md new file mode 100644 index 0000000..8619c25 --- /dev/null +++ b/docs/accessibility-guidelines.md @@ -0,0 +1,164 @@ +# AccessGuide.io - Accessibility Guidelines Reference + +*Compiled from [Access Guide](https://www.accessguide.io/) - a friendly introduction to digital accessibility based on WCAG 2.1* + +--- + +## 1. Hover and Focus Best Practices + +> WCAG criterion [1.4.13 Content on Hover or Focus](https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html) (Level AA) + +### Why This is Important + +If content appears and disappears on hover or focus, this can feel frustrating, unpredictable, and disruptive. Use best practices to make hover and focus more predictable and less likely to cause errors. + +This is especially accessible for: +- People with **physical disabilities** (unpredictable or specific movement) +- People with **visual disabilities** (screen reader users) +- People with **cognitive disabilities** + +### Implementation Guidelines + +#### Dismissible +There should be a way to dismiss new content **without moving hover or changing focus**. This prevents disrupting users in the middle of tasks. + +**Best practice:** Use the "Escape" key to dismiss content. + +#### Hoverable +New content should **remain visible if the user hovers over it**. + +Sometimes content appears when hovering the trigger element but disappears when hovering over the new content. This is frustrating. The content must remain visible whether hover is on the trigger OR the content itself. + +#### Persistent +Content must remain visible until: +- The user dismisses it +- The user moves the mouse off of it or the trigger +- The content no longer contains important information + +--- + +## 2. Keyboard Accessibility + +> WCAG criterion [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html) (Level A) + +### Why This is Important + +Full keyboard functionality is essential for: +- People who rely on keyboards +- Blind/visually impaired people using screen readers +- People with motor disabilities who can't use a mouse + +**All functionality available to mouse users should be available to keyboard users.** + +### Basic Keyboard Access + +Keyboard accessibility is enabled by default in browsers. **Don't remove or deactivate these defaults** - enhance them instead. + +Keyboard accessibility includes: +- ✅ Visible focus indicator +- ✅ Intuitive focus order +- ✅ Skip links to bypass repeating content +- ✅ Way to turn off character key shortcuts +- ✅ No keyboard traps + +**Common keyboard-accessible interactions:** +- Browsing navigation +- Filling out forms +- Accessing buttons and links + +### Advanced Keyboard Access + +For complex interactions (drawing programs, drag-and-drop): +- Translate gestures into keyboard commands +- Use **Tab, Enter, Space, and Arrow keys** (most common) + +**Examples from Salesforce:** +- Interact with a canvas (move/resize objects) +- Move between lists +- Sort lists + +--- + +## 3. Focus Indicator Visibility + +> WCAG criterion [2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html) (Level AA) + +### Why This is Important + +A visible focus indicator shows keyboard users what element they're currently interacting with. + +Benefits: +- Shows what element is ready for user input +- Helps people with executive dysfunction +- Reduces cognitive load by focusing attention + +### Implementation Guidelines + +#### Never Remove Default Focus Indicator +```css +/* ❌ NEVER DO THIS unless replacing with custom styling */ +outline: none; +``` + +#### Design Accessible Hover and Focus States + +**All interactive elements need hover AND focus states:** +- Buttons, links +- Text fields +- Navigation elements +- Radio buttons, checkboxes + +**Difference between hover and focus:** +| State | Purpose | +|-------|---------| +| Hover | "You could interact with this" | +| Focus | "You ARE interacting with this right now" | + +**Other important states:** +- Error state +- Loading state +- Inactive/disabled state + +#### Focus Indicator Contrast + +The focus indicator must be visible against **both**: +- The element itself +- The background behind it + +**WCAG requirement:** 3:1 contrast ratio for UI components + +--- + +## 4. Key Principles Summary + +### For TuxMate Application + +| Principle | Application | +|-----------|-------------| +| **Dismissible** | Tooltips close with Escape key | +| **Hoverable** | Tooltips stay visible when hovering content | +| **Persistent** | Content stays until user dismisses | +| **Keyboard** | All features work with Tab/Arrow/Enter/Space | +| **Focus Visible** | Clear visual indicator on focused elements | +| **Contrast** | 3:1 minimum for UI components | + +### Recommended Key Bindings + +| Key | Common Action | +|-----|---------------| +| `Tab` | Move to next focusable element | +| `Shift+Tab` | Move to previous element | +| `Enter/Space` | Activate/select | +| `Arrow keys` | Navigate within components | +| `Escape` | Dismiss/close | +| `?` | Show help | + +--- + +## Further Reading + +- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/) +- [MDN Web Docs - Keyboard](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Keyboard) +- [18F - Keyboard Access](https://accessibility.18f.gov/keyboard/) +- [Style hover, focus, and active states differently](https://zellwk.com/blog/style-hover-focus-active-states/) +- [Accessible Custom Focus Indicators](https://uxdesign.cc/accessible-custom-focus-indicators-da4768d1fb7b) diff --git a/src/app/globals.css b/src/app/globals.css index c105294..8dc10d4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,17 +3,17 @@ /* ===== WARM PAPER AESTHETIC THEMES ===== */ :root { - /* Dark theme - warm charcoal with paper undertones */ - --bg-primary: #1e1d1a; - --bg-secondary: #262520; - --bg-tertiary: #302f29; - --bg-hover: #3a3832; - --bg-focus: #454339; + /* Dark theme - lighter warm charcoal per user request */ + --bg-primary: #262522; + --bg-secondary: #2f2e2a; + --bg-tertiary: #3a3934; + --bg-hover: #45433d; + --bg-focus: #504e46; --text-primary: #f5f3ef; --text-secondary: #d8d4cc; --text-muted: #a09a8e; - --border-primary: #3a3832; - --border-secondary: #454339; + --border-primary: #45433d; + --border-secondary: #504e46; --accent: #9a958a; } @@ -35,20 +35,31 @@ @theme inline { --color-background: var(--bg-primary); --color-foreground: var(--text-primary); - --font-sans: var(--font-geist-sans); - --font-mono: var(--font-geist-mono); + --font-sans: var(--font-sans); + --font-heading: var(--font-heading); + --font-mono: var(--font-mono); } body { background: var(--bg-primary); color: var(--text-primary); - font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-family: var(--font-sans), system-ui, -apple-system, sans-serif; font-weight: 450; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; transition: background 0.3s ease, color 0.3s ease; } +h1, +h2, +h3, +h4, +h5, +h6, +.category-header { + font-family: var(--font-heading), system-ui, -apple-system, sans-serif; +} + body::before { content: ""; position: fixed; @@ -83,6 +94,7 @@ body::before { html { scroll-behavior: smooth; + font-family: var(--font-sans); } ::selection { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 85cc9de..4ff1803 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,27 +1,24 @@ import type { Metadata } from "next"; -import { Geist, Geist_Mono, JetBrains_Mono, Open_Sans } from "next/font/google"; +import { Outfit, Inter, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/hooks/useTheme"; -const geistSans = Geist({ - variable: "--font-geist-sans", +const outfit = Outfit({ + variable: "--font-heading", subsets: ["latin"], + display: "swap", }); -const geistMono = Geist_Mono({ - variable: "--font-geist-mono", +const inter = Inter({ + variable: "--font-sans", subsets: ["latin"], + display: "swap", }); const jetbrainsMono = JetBrains_Mono({ - variable: "--font-jetbrains-mono", + variable: "--font-mono", subsets: ["latin"], -}); - -const openSans = Open_Sans({ - variable: "--font-open-sans", - subsets: ["latin"], - weight: ["400", "500", "600", "700"], + display: "swap", }); export const metadata: Metadata = { @@ -100,7 +97,7 @@ export default function RootLayout({ )} {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index cf630d8..d36a26a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -19,10 +19,7 @@ import { CategorySection } from '@/components/app'; import { CommandFooter } from '@/components/command'; import { Tooltip, GlobalStyles, LoadingSkeleton } from '@/components/common'; -// The main event - export default function Home() { - // All the state we need to make this thing work const { tooltip, show: showTooltip, hide: hideTooltip, tooltipMouseEnter, tooltipMouseLeave, setTooltipRef } = useTooltip(); @@ -122,7 +119,7 @@ export default function Home() { }, []); - // Build nav items for keyboard navigation (vim keys ftw) + // Build nav items for keyboard navigation const navItems = useMemo(() => { const items: NavItem[][] = []; columns.forEach((colCategories) => { diff --git a/src/components/app/AppItem.tsx b/src/components/app/AppItem.tsx index 5a78784..4be8e00 100644 --- a/src/components/app/AppItem.tsx +++ b/src/components/app/AppItem.tsx @@ -9,6 +9,25 @@ import { AppIcon } from './AppIcon'; // Each app row in the list. Memoized because there are a LOT of these. +// Basic Tailwind-ish color palette for mapping +const COLOR_MAP: Record = { + 'orange': '#f97316', + 'blue': '#3b82f6', + 'emerald': '#10b981', + 'sky': '#0ea5e9', + 'yellow': '#eab308', + 'slate': '#64748b', + 'zinc': '#71717a', + 'rose': '#f43f5e', + 'purple': '#a855f7', + 'red': '#ef4444', + 'indigo': '#6366f1', + 'cyan': '#06b6d4', + 'green': '#22c55e', + 'teal': '#14b8a6', + 'gray': '#6b7280', +}; + interface AppItemProps { app: AppData; isSelected: boolean; @@ -19,6 +38,7 @@ interface AppItemProps { onTooltipEnter: (t: string, e: React.MouseEvent) => void; onTooltipLeave: () => void; onFocus?: () => void; + color?: string; } export const AppItem = memo(function AppItem({ @@ -31,6 +51,7 @@ export const AppItem = memo(function AppItem({ onTooltipEnter, onTooltipLeave, onFocus, + color = 'gray', }: AppItemProps) { // Why isn't this app available? Tell the user. const getUnavailableText = () => { @@ -41,6 +62,11 @@ export const AppItem = memo(function AppItem({ // Special styling for AUR packages (Arch users love their badges) const isAur = selectedDistro === 'arch' && app.targets?.arch && isAurPackage(app.targets.arch); + // Determine effective color (AUR overrides category color for the checkbox/badge, but maybe not the row border) + // Actually, let's keep the row border as the category color for consistency + const hexColor = COLOR_MAP[color] || COLOR_MAP['gray']; + const checkboxColor = isAur ? '#1793d1' : hexColor; + return (
{ e.stopPropagation(); onFocus?.(); if (isAvailable) { const willBeSelected = !isSelected; onToggle(); - // Temporarily disabled to save Umami event quota - // const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro; - // if (willBeSelected) { - // analytics.appSelected(app.name, app.category, distroName); - // } else { - // analytics.appDeselected(app.name, app.category, distroName); - // } } }} > -
- {isSelected && } +
+ {isSelected && }
@@ -116,7 +144,8 @@ export const AppItem = memo(function AppItem({ onMouseLeave={(e) => { e.stopPropagation(); onTooltipLeave(); }} > = { 'System': Cpu, }; +// Basic Tailwind-ish color palette for mapping +const COLOR_MAP: Record = { + 'orange': '#f97316', + 'blue': '#3b82f6', + 'emerald': '#10b981', + 'sky': '#0ea5e9', + 'yellow': '#eab308', + 'slate': '#64748b', + 'zinc': '#71717a', + 'rose': '#f43f5e', + 'purple': '#a855f7', + 'red': '#ef4444', + 'indigo': '#6366f1', + 'cyan': '#06b6d4', + 'green': '#22c55e', + 'teal': '#14b8a6', + 'gray': '#6b7280', +}; + // Clickable category header with chevron and selection count export function CategoryHeader({ category, @@ -32,6 +51,7 @@ export function CategoryHeader({ onToggle, selectedCount, onFocus, + color = 'gray', }: { category: string; isExpanded: boolean; @@ -39,7 +59,10 @@ export function CategoryHeader({ onToggle: () => void; selectedCount: number; onFocus?: () => void; + color?: string; }) { + const hexColor = COLOR_MAP[color] || COLOR_MAP['gray']; + return ( {category} {selectedCount > 0 && ( - [{selectedCount}] + {selectedCount} )} diff --git a/src/components/app/CategorySection.tsx b/src/components/app/CategorySection.tsx index 364dca5..e58f284 100644 --- a/src/components/app/CategorySection.tsx +++ b/src/components/app/CategorySection.tsx @@ -26,6 +26,25 @@ interface CategorySectionProps { onAppFocus?: (appId: string) => void; } +// Category color mapping +const categoryColors: Record = { + 'Web Browsers': 'orange', + 'Communication': 'blue', + 'Media': 'yellow', + 'Gaming': 'purple', + 'Office': 'indigo', + 'Creative': 'cyan', + 'System': 'red', + 'File Sharing': 'teal', + 'Security': 'green', + 'VPN & Network': 'emerald', + 'Dev: Editors': 'sky', + 'Dev: Languages': 'rose', + 'Dev: Tools': 'slate', + 'Terminal': 'zinc', + 'CLI Tools': 'gray' +}; + function CategorySectionComponent({ category, categoryApps, @@ -49,7 +68,10 @@ function CategorySectionComponent({ const hasAnimated = useRef(false); const prevAppCount = useRef(categoryApps.length); - // Initial entrance animation - GPU optimized + // Get color for this category + const color = categoryColors[category] || 'gray'; + + // Initial entrance animation useLayoutEffect(() => { if (!sectionRef.current || hasAnimated.current) return; hasAnimated.current = true; @@ -114,6 +136,7 @@ function CategorySectionComponent({ }} selectedCount={selectedInCategory} onFocus={onCategoryFocus} + color={color} />
onAppFocus?.(app.id)} + color={color} /> ))}
diff --git a/src/components/command/AurDrawerSettings.tsx b/src/components/command/AurDrawerSettings.tsx index 2047172..9d04c7c 100644 --- a/src/components/command/AurDrawerSettings.tsx +++ b/src/components/command/AurDrawerSettings.tsx @@ -8,87 +8,59 @@ interface AurDrawerSettingsProps { setSelectedHelper: (helper: 'yay' | 'paru') => void; } -// AUR settings inside the command drawer +// AUR settings configuration panel export function AurDrawerSettings({ aurAppNames, hasYayInstalled, setHasYayInstalled, selectedHelper, setSelectedHelper, -}: AurDrawerSettingsProps) { + distroColor, +}: AurDrawerSettingsProps & { distroColor: string }) { return ( -
- {/* Header with all apps listed */} -
-

- AUR packages: - {aurAppNames.join(', ')} -

+
+
+ AUR Packages: + {aurAppNames.join(', ')}
- {/* Controls with animated buttons */} -
- {/* Helper selection */} -
- AUR helper: -
+
+
+ AUR Helper +
- {/* Divider */} -
- - {/* Installation mode */} -
- Already installed? -
+
+ Install helper? +
diff --git a/src/components/command/AurFloatingCard.tsx b/src/components/command/AurFloatingCard.tsx index 90cbea2..f652c54 100644 --- a/src/components/command/AurFloatingCard.tsx +++ b/src/components/command/AurFloatingCard.tsx @@ -98,15 +98,15 @@ export function AurFloatingCard({ return (
{hasAnswered !== null && (
@@ -117,16 +117,16 @@ export function AurFloatingCard({ return (
- {/* Card 1: Do you have an AUR helper? */} + {/* Card 1: Do you have an AUR helper? - AccessGuide style */}
handleFirstAnswer(true)} className={` - flex-1 py-2 px-4 rounded-xl text-sm font-medium + flex-1 py-2 px-4 text-sm font-medium transition-[background-color,color] duration-200 ease-out ${hasAnswered === true - ? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm' + ? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]' : 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]' } `} @@ -169,10 +169,10 @@ export function AurFloatingCard({
- {/* Card 2: Which helper? (appears after first answer) */} + {/* Card 2: Which helper? (appears after first answer) - AccessGuide style */} {hasAnswered !== null && (
handleHelperSelect('yay')} className={` - flex-1 py-2.5 px-4 rounded-xl text-sm font-medium + flex-1 py-2.5 px-4 text-sm font-medium transition-[background-color,color] duration-200 ease-out ${selectedHelper === 'yay' && helperChosen - ? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm' + ? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]' : 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]' } `} @@ -243,10 +243,10 @@ export function AurFloatingCard({
- {/* Drawer Header */} -
+
-
- $ -
-
-

Terminal Command

-

- {selectedCount} app{selectedCount !== 1 ? 's' : ''} - • Press Esc to close -

+
+
+
+

Terminal Preview

+

+ {selectedCount} apps selected +

+
- {/* Command Content */} -
- {/* AUR Settings */} +
{showAur && ( )} {/* Terminal window */} -
-
-
-
-
-
- bash -
+ {/* Terminal window */} +
+
+ bash + {/* Desktop action buttons */}
-
-
- $ + +
+
+ $
- {/* Mobile Actions - side by side for better UX */} -
+ {/* Mobile Actions */} +
@@ -241,13 +259,23 @@ export function CommandFooter({ @@ -255,13 +283,23 @@ export function CommandFooter({ @@ -269,15 +307,25 @@ export function CommandFooter({
diff --git a/src/components/command/ShortcutsBar.tsx b/src/components/command/ShortcutsBar.tsx index 0d04a68..2c4465c 100644 --- a/src/components/command/ShortcutsBar.tsx +++ b/src/components/command/ShortcutsBar.tsx @@ -8,6 +8,7 @@ interface ShortcutsBarProps { searchInputRef: React.RefObject; selectedCount: number; distroName: string; + distroColor: string; showAur: boolean; selectedHelper: 'yay' | 'paru'; setSelectedHelper: (helper: 'yay' | 'paru') => void; @@ -20,6 +21,7 @@ export function ShortcutsBar({ searchInputRef, selectedCount, distroName, + distroColor, showAur, selectedHelper, setSelectedHelper, @@ -33,12 +35,16 @@ export function ShortcutsBar({ }; return ( -
+
{/* LEFT SECTION */}
{/* Mode Badge - like nvim NORMAL/INSERT (hidden on mobile) */} -
+
{distroName.toUpperCase()}
@@ -115,7 +121,10 @@ export function ShortcutsBar({
{/* End badge - like nvim line:col */} -
+
TUX
diff --git a/src/components/common/Tooltip.tsx b/src/components/common/Tooltip.tsx index 1398e15..7aa7b99 100644 --- a/src/components/common/Tooltip.tsx +++ b/src/components/common/Tooltip.tsx @@ -66,23 +66,25 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip ref={setRef} role="tooltip" className="fixed hidden md:block pointer-events-auto z-[9999]" - style={{ left: current.x, top: current.y - 10 }} + style={{ left: current.x, top: current.y - 12 }} // Moved up slightly to clear cursor onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} >
- {/* Clean tooltip bubble */} + {/* AccessGuide-style tooltip - rectangular with left border accent */}

@@ -90,14 +92,14 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip

- {/* Arrow */} -
+ {/* Arrow aligned to mouse position */} + {/* Arrow at left: 16px matches the visual design, so we shift wrapper -22px to align 16px + 6px(half arrow) approx to 0 */} +
diff --git a/src/components/distro/DistroSelector.tsx b/src/components/distro/DistroSelector.tsx index 8cbf9f0..1c55e9e 100644 --- a/src/components/distro/DistroSelector.tsx +++ b/src/components/distro/DistroSelector.tsx @@ -54,55 +54,47 @@ export function DistroSelector({ background: 'rgba(0,0,0,0.05)', }} /> - {/* Dropdown */} + {/* Dropdown - AccessGuide style: rectangular with left border */}
- {/* Header */} -
- Select Distro -
- {/* Distro List */} -
+
{distros.map((distro, i) => ( ))} @@ -119,13 +111,16 @@ export function DistroSelector({ aria-haspopup="listbox" aria-expanded={isOpen} aria-label={`Select distribution, current: ${currentDistro?.name}`} - className={`group flex items-center gap-2.5 h-10 pl-2.5 pr-3.5 rounded-2xl border border-[var(--border-primary)] bg-[var(--bg-secondary)] transition-[background-color,box-shadow,border-color] duration-300 ${isOpen ? 'ring-2 ring-[var(--accent)]/30 border-[var(--accent)]/50' : 'hover:bg-[var(--bg-hover)]'}`} + className={`group flex items-center gap-2.5 h-10 pl-3 pr-3.5 border-l-4 bg-[var(--bg-secondary)] rounded-md transition-all duration-150 ${isOpen ? 'bg-[var(--bg-tertiary)]' : 'hover:bg-[var(--bg-hover)]'}`} + style={{ + borderColor: currentDistro?.color || 'var(--accent)' + }} > -
- +
+
- {currentDistro?.name} - + {currentDistro?.name} + {mounted && typeof document !== 'undefined' && createPortal(dropdown, document.body)} diff --git a/src/components/header/HowItWorks.tsx b/src/components/header/HowItWorks.tsx index e4a2914..af9ea20 100644 --- a/src/components/header/HowItWorks.tsx +++ b/src/components/header/HowItWorks.tsx @@ -86,17 +86,18 @@ export function HowItWorks() { }} /> - {/* Modal */} + {/* Modal - AccessGuide style: rectangular with left border accent */}
{/* Header */} -
+

Help

@@ -125,9 +126,9 @@ export function HowItWorks() { {/* Content */}
- {/* Shortcuts */} + {/* Shortcuts - AccessGuide style */}
-

Keyboard Shortcuts

+

Keyboard Shortcuts

{[ ['↑↓←→', 'Navigate through apps'], @@ -144,7 +145,7 @@ export function HowItWorks() { ['1 / 2', 'Switch AUR helper (yay/paru)'], ].map(([key, desc]) => (
- + {key} {desc} @@ -153,9 +154,9 @@ export function HowItWorks() {
- {/* Getting Started */} + {/* Getting Started - AccessGuide style */}
-

Getting Started

+

Getting Started

  1. 1. Pick your distro — Select your Linux distribution from the dropdown at the top. This determines which package manager commands TuxMate generates for you. @@ -172,9 +173,9 @@ export function HowItWorks() {
- {/* Notes */} + {/* Notes - AccessGuide style */}
-

Good to Know

+

Good to Know

  • Greyed out apps aren't available in your distro's official repositories. Try switching to Flatpak or Snap in the dropdown, or hover the info icon next to the app for alternative installation methods. @@ -189,7 +190,7 @@ export function HowItWorks() { Auto-save — Your app selections are saved automatically in your browser. Come back anytime and your selections will still be there.
  • - Script Safety — Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with bash tuxmate-*.sh to safely install your selection. + Script Safety — Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with bash tuxmate-*.sh to safely install your selection.
diff --git a/src/components/ui/theme-toggle.tsx b/src/components/ui/theme-toggle.tsx index 571d107..17ebe30 100644 --- a/src/components/ui/theme-toggle.tsx +++ b/src/components/ui/theme-toggle.tsx @@ -27,7 +27,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) { return (
{ setTooltip({ content, - x: rect.left + rect.width / 2, + x: clientX, // Use mouse X instead of element center y: rect.top, }); }, 450);