diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 887bc67..6f2eabb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -34,7 +34,13 @@ const themeScript = ` (function() { try { var theme = localStorage.getItem('theme'); - if (theme === 'light' || !theme) { + var isDark = false; + if (theme) { + isDark = theme === 'dark'; + } else { + isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + } + if (!isDark) { document.documentElement.classList.add('light'); } } catch (e) {} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7819f1e..c57ec2b 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState, useMemo, useCallback, useRef, useLayoutEffect, useEffect } from 'react'; -import { X } from 'lucide-react'; import gsap from 'gsap'; // Hooks @@ -206,7 +205,7 @@ export default function Home() {
{/* Logo & Title */}
-
+
TuxMate Logo TuxMate -

- The Linux Bulk App Installer. -

-
-

- Select apps • Arrow keys + Space +

+

+ The Linux Bulk App Installer.

- | - + +
+ +
@@ -236,21 +234,6 @@ export default function Home() {
- {selectedCount > 0 && ( - <> - · - - - )}
{/* Control buttons */} diff --git a/src/components/app/AppIcon.tsx b/src/components/app/AppIcon.tsx index 9e536b1..ec2e30c 100644 --- a/src/components/app/AppIcon.tsx +++ b/src/components/app/AppIcon.tsx @@ -15,6 +15,7 @@ export function AppIcon({ url, name }: { url: string; name: string }) { } return ( + // eslint-disable-next-line @next/next/no-img-element {isAur && ( + // eslint-disable-next-line @next/next/no-img-element = { + 'Web Browsers': Globe, + 'Communication': MessageCircle, + 'Dev: Languages': Code2, + 'Dev: Editors': FileCode, + 'Dev: Tools': Wrench, + 'Terminal': Terminal, + 'CLI Tools': Command, + 'Media': Play, + 'Creative': Palette, + 'Gaming': Gamepad2, + 'Office': Briefcase, + 'VPN & Network': Network, + 'Security': Lock, + 'File Sharing': Share2, + 'System': Cpu, +}; // Clickable category header with chevron and selection count export function CategoryHeader({ @@ -25,17 +47,22 @@ export function CategoryHeader({ tabIndex={-1} aria-expanded={isExpanded} aria-label={`${category} category, ${selectedCount} apps selected`} - className={`category-header w-full flex items-center gap-2 text-[11px] font-semibold text-[var(--text-muted)] - hover:text-[var(--text-secondary)] uppercase tracking-widest mb-2 pb-1.5 - border-b border-[var(--border-primary)] transition-colors duration-150 px-0.5 outline-none - ${isFocused ? 'bg-[var(--bg-focus)] text-[var(--text-secondary)]' : ''}`} - style={{ transition: 'color 0.5s, border-color 0.5s' }} + // Soft Pill design: rounded tags with accent color + className={`category-header group w-full flex items-center gap-2 text-xs font-bold text-[var(--accent)] + bg-[var(--accent)]/10 hover:bg-[var(--accent)]/20 + uppercase tracking-wider py-1 px-3 rounded-lg mb-3 + transition-all duration-200 outline-none + ${isFocused ? 'bg-[var(--accent)]/25 shadow-sm' : ''}`} > - + + {(() => { + const Icon = CATEGORY_ICONS[category] || Terminal; + return ; + })()} {category} {selectedCount > 0 && ( {selectedCount} diff --git a/src/components/app/CategorySection.tsx b/src/components/app/CategorySection.tsx index 81f6308..cfde8dd 100644 --- a/src/components/app/CategorySection.tsx +++ b/src/components/app/CategorySection.tsx @@ -80,7 +80,6 @@ function CategorySectionComponent({ y: 0, opacity: 1, duration: 0.5, - stagger: 0.025, ease: 'power2.out', delay: delay + 0.1, force3D: true diff --git a/src/components/command/AurFloatingCard.tsx b/src/components/command/AurFloatingCard.tsx index b7ecd96..fd5c755 100644 --- a/src/components/command/AurFloatingCard.tsx +++ b/src/components/command/AurFloatingCard.tsx @@ -16,7 +16,6 @@ interface AurFloatingCardProps { export function AurFloatingCard({ show, aurAppNames, - hasYayInstalled, setHasYayInstalled, selectedHelper, setSelectedHelper, @@ -34,6 +33,7 @@ export function AurFloatingCard({ // Reset when new AUR packages appear, BUT ONLY if user hasn't interacted yet useEffect(() => { if (show && aurAppNames.length > 0 && !userInteractedRef.current) { + // eslint-disable-next-line react-hooks/set-state-in-effect setDismissed(false); setIsExiting(false); setShowConfirmation(false); @@ -72,10 +72,6 @@ export function AurFloatingCard({ }, 200); }; - const handleConfirmationDismiss = () => { - setDismissed(true); - }; - // Show confirmation message after selecting helper, auto-dismiss after 3s if (showConfirmation) { // Auto dismiss after 3 seconds diff --git a/src/components/command/CommandFooter.tsx b/src/components/command/CommandFooter.tsx index 1b4f714..3aa002b 100644 --- a/src/components/command/CommandFooter.tsx +++ b/src/components/command/CommandFooter.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { Check, Copy, ChevronUp, Download } from 'lucide-react'; +import { Check, Copy, ChevronUp, Download, X } from 'lucide-react'; import { distros, type DistroId } from '@/lib/data'; import { generateInstallScript } from '@/lib/generateInstallScript'; import { analytics } from '@/lib/analytics'; @@ -227,6 +227,20 @@ export function CommandFooter({
+ {/* Clear button */} + + {/* Download button */} - )} -
- - {/* App count */} - {selectedCount > 0 && ( -
- [{selectedCount} app{selectedCount !== 1 ? 's' : ''}] -
- )} - - {/* AUR Helper Switch */} - {showAur && ( -
- - -
+ /> + {searchQuery && ( + )}
- {/* RIGHT SECTION - Compact Shortcuts */} -
-
- {/* Navigation */} - ←↓↑→ / hjkl Navigation - · - {/* Actions */} - / search - · - Space toggle - · - y copy - · - d download - · - c clear - · - t theme - · - Tab preview - · - Esc back - · - ? help + {/* App count */} + {selectedCount > 0 && ( +
+ [{selectedCount} app{selectedCount !== 1 ? 's' : ''}]
+ )} - {/* End badge - like nvim line:col */} -
- TUX + {/* AUR Helper Switch */} + {showAur && ( +
+ +
+ )} +
+ + {/* RIGHT SECTION - Compact Shortcuts */} +
+
+ {/* Navigation */} + ←↓↑→ / hjkl Navigation + · + {/* Actions */} + / search + · + Space toggle + · + Tab preview + · + ? help +
+ + {/* End badge - like nvim line:col */} +
+ TUX
- ); - } -); +
+ ); +} diff --git a/src/components/distro/DistroIcon.tsx b/src/components/distro/DistroIcon.tsx index a16036f..e86feb7 100644 --- a/src/components/distro/DistroIcon.tsx +++ b/src/components/distro/DistroIcon.tsx @@ -18,6 +18,7 @@ export function DistroIcon({ url, name, size = 20 }: { url: string; name: string } return ( + // eslint-disable-next-line @next/next/no-img-element d.id === selectedDistro); useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); }, []); diff --git a/src/components/header/HowItWorks.tsx b/src/components/header/HowItWorks.tsx index af67b6c..2061a8f 100644 --- a/src/components/header/HowItWorks.tsx +++ b/src/components/header/HowItWorks.tsx @@ -12,7 +12,24 @@ export function HowItWorks() { const [mounted, setMounted] = useState(false); const triggerRef = useRef(null); + const handleOpen = () => { + setIsClosing(false); + setIsOpen(true); + analytics.helpOpened(); + }; + + const handleClose = () => { + setIsClosing(true); + analytics.helpClosed(); + // Wait for exit animation to finish + setTimeout(() => { + setIsOpen(false); + setIsClosing(false); + }, 200); + }; + useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true); }, []); @@ -56,22 +73,6 @@ export function HowItWorks() { return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen]); - const handleOpen = () => { - setIsClosing(false); - setIsOpen(true); - analytics.helpOpened(); - }; - - const handleClose = () => { - setIsClosing(true); - analytics.helpClosed(); - // Wait for exit animation to finish - setTimeout(() => { - setIsOpen(false); - setIsClosing(false); - }, 200); - }; - const modal = ( <> {/* Backdrop with blur */} @@ -90,180 +91,105 @@ export function HowItWorks() { role="dialog" aria-modal="true" aria-labelledby="how-it-works-title" - className="fixed bg-[var(--bg-secondary)] border border-[var(--border-primary)] shadow-2xl z-[99999]" + className="fixed bg-[var(--bg-secondary)] border border-[var(--border-primary)] z-[99999]" style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)', - borderRadius: '20px', - width: '440px', + borderRadius: '16px', + width: '620px', maxWidth: 'calc(100vw - 32px)', - maxHeight: 'min(80vh, 650px)', + maxHeight: 'min(85vh, 720px)', display: 'flex', flexDirection: 'column', animation: isClosing ? 'modalSlideOut 0.2s ease-out forwards' : 'modalSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1)', overflow: 'hidden', + boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.2)', }} > {/* Header */} -
-
-
- -
-
-

How TuxMate Works

-

Quick guide & keyboard shortcuts

-
-
+
+

+ Help +

- {/* Scrollable content */} -
- {/* Quick Start Steps */} -
-

Quick Start

-
-
-
1
-

Select your distro from the dropdown

-
-
-
2
-

Check the apps you want to install

-
-
-
3
-

Copy the command or download the script

-
-
-
4
-

Paste in terminal (Ctrl+Shift+V) and run

-
+ {/* Content */} +
+ + {/* Shortcuts */} +
+

Keyboard Shortcuts

+
+ {[ + ['↑↓←→', 'Navigate through apps'], + ['hjkl', 'Vim-style navigation'], + ['Space', 'Select or deselect app'], + ['/', 'Focus search box'], + ['y', 'Copy install command'], + ['d', 'Download install script'], + ['c', 'Clear all selections'], + ['t', 'Toggle light/dark theme'], + ['Tab', 'Preview current selection'], + ['Esc', 'Close this modal'], + ['?', 'Show this help'], + ['1 / 2', 'Switch AUR helper (yay/paru)'], + ].map(([key, desc]) => ( +
+ + {key} + + {desc} +
+ ))}
-
+ - {/* Unavailable Apps */} -
-

App Not Available?

-
-

Greyed-out apps aren't in your distro's repos. Here's what you can do:

-
    -
  • - - Use Flatpak/Snap: Switch to Flatpak or Snap in the distro selector for universal packages -
  • -
  • - - Download from website: Visit the app's official site and grab the .deb, .rpm, or .AppImage -
  • -
  • - - Hover the ⓘ icon: Some unavailable apps show links to alternative download methods -
  • -
-
-
+ {/* 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. +
  2. +
  3. + 2. Select apps — Browse the categories and click on apps to add them to your selection. Selected apps are highlighted. Use keyboard shortcuts to navigate faster. +
  4. +
  5. + 3. Copy or download — Copy the generated install command to your clipboard, or download a complete shell script. Downloaded scripts include error handling and can install multiple apps at once. +
  6. +
  7. + 4. Run in terminal — Open your terminal, paste the command (Ctrl+Shift+V), and press Enter. The script will handle the rest. +
  8. +
+
- {/* Arch & AUR */} -
-

Arch Linux & AUR

-

- Some Arch packages are in the AUR (Arch User Repository). - TuxMate uses yay or paru to install these. - When selecting AUR packages, a popup will ask which helper you have. You can switch between helpers anytime using 1 (yay) or 2 (paru). -

-
- - {/* Keyboard Shortcuts */} -
-

Keyboard Shortcuts

-
-
- ↑↓←→ - Navigate -
-
- hjkl - Vim navigation -
-
- Space - Toggle selection -
-
- / - Search apps -
-
- y - Copy command -
-
- d - Download script -
-
- c - Clear selection -
-
- t - Toggle theme -
-
- Tab - Open preview -
-
- Esc - Close popups -
-
- ? - This help -
-
-
- - {/* Pro Tips */} -
-

Pro Tips

-
    -
  • - 💡 - The download button gives you a full shell script with progress tracking, error handling, and a summary + {/* Notes */} +
    +

    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.
    • -
    • - 💡 - - Running the script:{' '} - chmod +x tuxmate-*.sh && ./tuxmate-*.sh or{' '} - bash tuxmate-*.sh - +
    • + Arch Linux users — Some packages come from the AUR. TuxMate uses yay or paru as the AUR helper. Press 1 or 2 anytime to switch between them.
    • -
    • - 💡 - Your selections are saved automatically — come back anytime to modify your setup +
    • + Auto-save — Your app selections are saved automatically in your browser. Come back anytime and your selections will still be there.
    • -
    • - 💡 - Running .deb files: sudo dpkg -i file.deb -
    • -
    • - 💡 - Running .rpm files: sudo dnf install ./file.rpm +
    • + Running scripts — Downloaded scripts are saved as tuxmate-*.sh. Run them with bash tuxmate-*.sh
    -
+
diff --git a/src/components/ui/theme-toggle.tsx b/src/components/ui/theme-toggle.tsx index a70e7f1..571d107 100644 --- a/src/components/ui/theme-toggle.tsx +++ b/src/components/ui/theme-toggle.tsx @@ -16,6 +16,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) { // Prevent hydration mismatch by only rendering after mount useEffect(() => { + // eslint-disable-next-line react-hooks/set-state-in-effect setMounted(true) }, []) diff --git a/src/hooks/useLinuxInit.ts b/src/hooks/useLinuxInit.ts index 77a5d82..c549382 100644 --- a/src/hooks/useLinuxInit.ts +++ b/src/hooks/useLinuxInit.ts @@ -54,6 +54,7 @@ export function useLinuxInit(): UseLinuxInitReturn { const savedHelper = localStorage.getItem(STORAGE_KEY_HELPER) as 'yay' | 'paru' | null; if (savedDistro && distros.some(d => d.id === savedDistro)) { + // eslint-disable-next-line react-hooks/set-state-in-effect setSelectedDistroState(savedDistro); } @@ -76,7 +77,7 @@ export function useLinuxInit(): UseLinuxInitReturn { if (savedHelper === 'paru') { setSelectedHelper('paru'); } - } catch (e) { + } catch { // Ignore localStorage errors } setHydrated(true); @@ -90,7 +91,7 @@ export function useLinuxInit(): UseLinuxInitReturn { localStorage.setItem(STORAGE_KEY_APPS, JSON.stringify([...selectedApps])); localStorage.setItem(STORAGE_KEY_YAY, hasYayInstalled.toString()); localStorage.setItem(STORAGE_KEY_HELPER, selectedHelper); - } catch (e) { + } catch { // Ignore localStorage errors } }, [selectedDistro, selectedApps, hasYayInstalled, selectedHelper, hydrated]); diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx index f62c61e..c8a0a69 100644 --- a/src/hooks/useTheme.tsx +++ b/src/hooks/useTheme.tsx @@ -22,12 +22,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) { const [hydrated, setHydrated] = useState(false) useEffect(() => { - // On mount, sync with localStorage and mark as hydrated - const saved = localStorage.getItem('theme') as Theme | null - if (saved && saved !== theme) { - setTheme(saved) - document.documentElement.classList.toggle('light', saved === 'light') - } + // eslint-disable-next-line react-hooks/set-state-in-effect setHydrated(true) }, []) diff --git a/src/lib/data.ts b/src/lib/data.ts index 1df6101..6d9928d 100644 --- a/src/lib/data.ts +++ b/src/lib/data.ts @@ -153,7 +153,7 @@ export const apps: AppData[] = [ { id: 'fd', name: 'fd', description: 'Simple, fast alternative to find command', category: 'CLI Tools', iconUrl: mdi('file-search-outline', '#56BE89'), targets: { ubuntu: 'fd-find', debian: 'fd-find', arch: 'fd', fedora: 'fd-find', opensuse: 'fd', nix: 'fd' }, unavailableReason: 'fd is a CLI tool and not available via Flatpak or Snap.' }, { id: 'tmux', name: 'tmux', description: 'Terminal session manager and multiplexer', category: 'CLI Tools', iconUrl: si('tmux', '#1BB91F'), targets: { ubuntu: 'tmux', debian: 'tmux', arch: 'tmux', fedora: 'tmux', opensuse: 'tmux', nix: 'tmux' }, unavailableReason: 'tmux is a CLI tool and not available via Flatpak or Snap.' }, - { id: 'zellij', name: 'Zellij', description: 'Modern terminal multiplexer with layout system', category: 'Terminal', iconUrl: mdi('view-split-vertical', '#A48CF4'), targets: { ubuntu: 'zellij', arch: 'zellij', fedora: 'zellij', opensuse: 'zellij', nix: 'zellij' }, unavailableReason: 'Not in Debian repos. Install via `cargo install zellij` or see [zellij.dev](https://zellij.dev/documentation/installation.html).' }, + { id: 'zellij', name: 'Zellij', description: 'Modern terminal multiplexer with layout system', category: 'CLI Tools', iconUrl: mdi('view-split-vertical', '#A48CF4'), targets: { ubuntu: 'zellij', arch: 'zellij', fedora: 'zellij', opensuse: 'zellij', nix: 'zellij' }, unavailableReason: 'Not in Debian repos. Install via `cargo install zellij` or see [zellij.dev](https://zellij.dev/documentation/installation.html).' }, { id: 'superfile', name: 'Superfile', description: 'Modern terminal file manager with TUI', category: 'CLI Tools', iconUrl: mdi('folder-multiple', '#FFD93D'), targets: { arch: 'superfile', nix: 'superfile' }, unavailableReason: 'Install via `go install` or see [superfile.dev](https://superfile.dev/getting-started/installation/).' }, { id: 'rsync', name: 'rsync', description: 'Fast incremental file transfer and sync tool', category: 'CLI Tools', iconUrl: mdi('sync', '#2ECC71'), targets: { ubuntu: 'rsync', debian: 'rsync', arch: 'rsync', fedora: 'rsync', opensuse: 'rsync', nix: 'rsync' }, unavailableReason: 'rsync is a CLI tool and not available via Flatpak or Snap.' }, { id: 'uv', name: 'uv', description: 'Fast Python package manager', category: 'Dev: Languages', iconUrl: si('astral', '#5C4EE5'), targets: { arch: 'uv', nix: 'uv' }, unavailableReason: 'Install via `curl -LsSf https://astral.sh/uv/install.sh | sh`. See [installation guide](https://docs.astral.sh/uv/getting-started/installation/).' },