diff --git a/.gitignore b/.gitignore index 5ef6a52..c18a7a7 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# local notes +.notes.md diff --git a/src/app/error.tsx b/src/app/error.tsx index 017d8ce..a7f0671 100644 --- a/src/app/error.tsx +++ b/src/app/error.tsx @@ -2,10 +2,7 @@ import { useEffect } from 'react'; -/** - * Error Boundary for the application - * Catches runtime errors and provides recovery option - */ +// Next.js error boundary - shows when something explodes export default function Error({ error, reset, diff --git a/src/app/page.tsx b/src/app/page.tsx index 65d42a3..7819f1e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -20,26 +20,10 @@ import { CategorySection } from '@/components/app'; import { CommandFooter } from '@/components/command'; import { Tooltip, GlobalStyles, LoadingSkeleton } from '@/components/common'; +// The main event -// ============================================================================ -// Main Page Component -// ============================================================================ - -/** - * Home - Main TuxMate application page - * - * This is the root component that composes all the UI elements: - * - Header with logo, links, and controls - * - App grid organized by categories - * - Command footer with copy/download functionality - * - * State management is handled by the useLinuxInit hook. - * Keyboard navigation is handled by the useKeyboardNavigation hook. - */ export default function Home() { - // ======================================================================== - // State & Hooks - // ======================================================================== + // All the state we need to make this thing work const { tooltip, show: showTooltip, hide: hideTooltip, onTooltipEnter, onTooltipLeave } = useDelayedTooltip(600); @@ -84,11 +68,8 @@ export default function Home() { return () => window.removeEventListener('keydown', handleKeyDown); }, []); - // ======================================================================== - // Category & Column Layout - // ======================================================================== - /** All categories with their apps (filtered by search) */ + // Distribute apps into a nice grid const allCategoriesWithApps = useMemo(() => { const query = searchQuery.toLowerCase().trim(); return categories @@ -106,10 +87,10 @@ export default function Home() { .filter(c => c.apps.length > 0); }, [searchQuery]); - /** Number of columns for the app grid layout */ + // 5 columns looks good on most screens const COLUMN_COUNT = 5; - /** Distribute categories across columns for balanced layout */ + // Tetris-style packing: shortest column gets the next category const columns = useMemo(() => { const cols: Array = Array.from({ length: COLUMN_COUNT }, () => []); const heights = Array(COLUMN_COUNT).fill(0); @@ -135,11 +116,8 @@ export default function Home() { }); }, []); - // ======================================================================== - // Keyboard Navigation - // ======================================================================== - /** Build navigation items from columns and expanded categories */ + // Build nav items for keyboard navigation (vim keys ftw) const navItems = useMemo(() => { const items: NavItem[][] = []; columns.forEach((colCategories) => { @@ -174,7 +152,7 @@ export default function Home() { const title = header.querySelector('.header-animate'); const controls = header.querySelector('.header-controls'); - // Animate title with clip-path reveal + // Fancy clip-path reveal for the logo gsap.fromTo(title, { clipPath: 'inset(0 100% 0 0)' }, { @@ -201,9 +179,8 @@ export default function Home() { ); }, [isHydrated]); - // ======================================================================== - // Loading State (must be AFTER all hooks) - // ======================================================================== + + // Don't render until we've loaded from localStorage (avoids flash) // Show loading skeleton until localStorage is hydrated if (!isHydrated) { @@ -264,12 +241,12 @@ export default function Home() { · diff --git a/src/components/app/AppIcon.tsx b/src/components/app/AppIcon.tsx index eea0251..9e536b1 100644 --- a/src/components/app/AppIcon.tsx +++ b/src/components/app/AppIcon.tsx @@ -2,18 +2,7 @@ import { useState } from 'react'; -/** - * AppIcon - Application icon with lazy loading and fallback - * - * Displays the app's icon from URL, with graceful fallback - * to the first letter of the app name in a colored square. - * - * @param url - URL to the app icon image - * @param name - Name of the app (used for fallback) - * - * @example - * - */ +// App icon with lazy loading, falls back to first letter if it fails export function AppIcon({ url, name }: { url: string; name: string }) { const [error, setError] = useState(false); diff --git a/src/components/app/AppItem.tsx b/src/components/app/AppItem.tsx index 994807f..364d2d7 100644 --- a/src/components/app/AppItem.tsx +++ b/src/components/app/AppItem.tsx @@ -7,39 +7,7 @@ import { analytics } from '@/lib/analytics'; import { isAurPackage } from '@/lib/aur'; import { AppIcon } from './AppIcon'; -/** - * AppItem - Individual app checkbox item (memoized for performance) - * - * Features: - * - Checkbox with selection state - * - Unavailable state with info icon - * - Tooltip on hover - * - Focus state for keyboard navigation - * - Analytics tracking - * - Memoized to prevent unnecessary re-renders - * - * @param app - App data object - * @param isSelected - Whether the app is selected - * @param isAvailable - Whether the app is available for the selected distro - * @param isFocused - Whether the item has keyboard focus - * @param selectedDistro - Currently selected distro ID - * @param onToggle - Callback when app is toggled - * @param onTooltipEnter - Callback for tooltip show - * @param onTooltipLeave - Callback for tooltip hide - * @param onFocus - Optional callback when item receives focus - * - * @example - * toggleApp(appData.id)} - * onTooltipEnter={showTooltip} - * onTooltipLeave={hideTooltip} - * /> - */ +// Each app row in the list. Memoized because there are a LOT of these. interface AppItemProps { app: AppData; @@ -64,12 +32,13 @@ export const AppItem = memo(function AppItem({ onTooltipLeave, onFocus, }: AppItemProps) { - // Build unavailable tooltip text (just the reason, no description) + // Why isn't this app available? Tell the user. const getUnavailableText = () => { const distroName = distros.find(d => d.id === selectedDistro)?.name || ''; return app.unavailableReason || `Not available in ${distroName} repos`; }; + // Special styling for AUR packages (Arch users love their badges) const isAur = selectedDistro === 'arch' && app.targets?.arch && isAurPackage(app.targets.arch); return ( @@ -79,7 +48,7 @@ export const AppItem = memo(function AppItem({ aria-checked={isSelected} aria-label={`${app.name}${!isAvailable ? ' (unavailable)' : ''}`} aria-disabled={!isAvailable} - className={`app-item w-full flex items-center gap-2.5 py-1.5 px-2 rounded-md outline-none transition-all duration-150 + className={`app-item w-full flex items-center gap-2.5 py-1.5 px-2 rounded-md outline-none transition-colors duration-150 ${isFocused ? 'bg-[var(--bg-focus)]' : ''} ${!isAvailable ? 'opacity-40 grayscale-[30%]' : 'hover:bg-[var(--bg-hover)] cursor-pointer'}`} style={{ transition: 'background-color 0.15s, color 0.5s' }} @@ -105,7 +74,7 @@ export const AppItem = memo(function AppItem({ onTooltipLeave(); }} > -
{ e.stopPropagation(); onTooltipLeave(); }} > toggleCategory("Browsers")} - * selectedCount={3} - * /> - */ +// Clickable category header with chevron and selection count export function CategoryHeader({ category, isExpanded, diff --git a/src/components/app/CategorySection.tsx b/src/components/app/CategorySection.tsx index c2abadd..81f6308 100644 --- a/src/components/app/CategorySection.tsx +++ b/src/components/app/CategorySection.tsx @@ -7,16 +7,7 @@ import { analytics } from '@/lib/analytics'; import { CategoryHeader } from './CategoryHeader'; import { AppItem } from './AppItem'; -/** - * CategorySection - Full category section with apps grid - * - * Features: - * - GSAP entrance animation with staggered reveals - * - Expandable/collapsible content - * - Keyboard navigation support - * - Analytics tracking for expand/collapse - */ - +// A category with its apps - handles animations and expand/collapse interface CategorySectionProps { category: Category; categoryApps: AppData[]; @@ -126,7 +117,7 @@ function CategorySectionComponent({ onFocus={onCategoryFocus} />
{categoryApps.map((app) => ( diff --git a/src/components/app/index.ts b/src/components/app/index.ts index 793e215..dbd696a 100644 --- a/src/components/app/index.ts +++ b/src/components/app/index.ts @@ -1,12 +1,4 @@ -/** - * App Components - * - * Components related to app display and selection: - * - AppIcon: App icon with lazy loading and fallback - * - AppItem: Individual app checkbox item - * - CategoryHeader: Expandable category header - * - CategorySection: Full category with apps grid - */ +// App components - icons, items, categories export { AppIcon } from './AppIcon'; export { AppItem } from './AppItem'; diff --git a/src/components/command/AurBar.tsx b/src/components/command/AurBar.tsx index e8dcd01..b13985c 100644 --- a/src/components/command/AurBar.tsx +++ b/src/components/command/AurBar.tsx @@ -2,24 +2,7 @@ import { Check } from 'lucide-react'; -/** - * AurBar - Arch User Repository packages info bar - * - * Displays information about AUR packages that will be installed, - * with a checkbox to indicate if yay is already installed. - * Only visible when Arch is selected and AUR packages are chosen. - * - * @param aurAppNames - Array of app names that require AUR - * @param hasYayInstalled - Whether the user has yay installed - * @param setHasYayInstalled - Callback to update yay installation status - * - * @example - * setHasYay(value)} - * /> - */ +// Shows AUR packages in a bar with "I have yay" checkbox export function AurBar({ aurAppNames, hasYayInstalled, @@ -67,7 +50,7 @@ export function AurBar({ className="sr-only" />
void; } -/** - * AurDrawerSettings - Compact UI with smooth button animations - */ +// AUR settings inside the command drawer export function AurDrawerSettings({ aurAppNames, hasYayInstalled, @@ -36,9 +34,9 @@ export function AurDrawerSettings({
+ + {/* Drawer Header */} +
+
+
+ $ +
+
+

Terminal Command

+

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

+
+
+ +
+ + {/* Command Content */} +
+ {/* AUR Settings */} + {showAur && ( + + )} + + {/* Terminal window */} +
+
+
+
+
+
+ bash +
+ {/* Desktop action buttons */} +
+ + +
+
+
+
+ $ + + {command} + +
+
+
+
+ + {/* Mobile Actions */} +
+ + +
+
+ + ); +} diff --git a/src/components/command/CommandFooter.tsx b/src/components/command/CommandFooter.tsx index fa00182..1b4f714 100644 --- a/src/components/command/CommandFooter.tsx +++ b/src/components/command/CommandFooter.tsx @@ -1,14 +1,14 @@ 'use client'; import { useState, useEffect, useCallback } from 'react'; -import { Check, Copy, ChevronUp, X, Download } from 'lucide-react'; +import { Check, Copy, ChevronUp, Download } from 'lucide-react'; import { distros, type DistroId } from '@/lib/data'; import { generateInstallScript } from '@/lib/generateInstallScript'; import { analytics } from '@/lib/analytics'; import { useTheme } from '@/hooks/useTheme'; import { ShortcutsBar } from './ShortcutsBar'; import { AurFloatingCard } from './AurFloatingCard'; -import { AurDrawerSettings } from './AurDrawerSettings'; +import { CommandDrawer } from './CommandDrawer'; interface CommandFooterProps { command: string; @@ -19,25 +19,15 @@ interface CommandFooterProps { aurAppNames: string[]; hasYayInstalled: boolean; setHasYayInstalled: (value: boolean) => void; - // Search props searchQuery: string; onSearchChange: (query: string) => void; searchInputRef: React.RefObject; - // Clear selections clearAll: () => void; - // AUR Helper selectedHelper: 'yay' | 'paru'; setSelectedHelper: (helper: 'yay' | 'paru') => void; } -/** - * CommandFooter - Fixed bottom bar with command output - * - * Features shortcuts bar at top, command preview, AUR badge, Download/Copy buttons. - * Search is now a separate floating popup component. - * - * Update: Added distinct drawer expansion button and global hotkeys. - */ +// The sticky footer with command preview and copy/download buttons export function CommandFooter({ command, selectedCount, @@ -55,8 +45,6 @@ export function CommandFooter({ setSelectedHelper, }: CommandFooterProps) { const [copied, setCopied] = useState(false); - const [showCopyTooltip, setShowCopyTooltip] = useState(false); - const [showDownloadTooltip, setShowDownloadTooltip] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false); const [drawerClosing, setDrawerClosing] = useState(false); @@ -70,7 +58,7 @@ export function CommandFooter({ }, 250); }, []); - // Close drawer on Escape key + // Close drawer on Escape useEffect(() => { if (!drawerOpen) return; const handleEscape = (e: KeyboardEvent) => { @@ -83,74 +71,16 @@ export function CommandFooter({ const showAur = selectedDistro === 'arch' && hasAurPackages; const distroDisplayName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro; - // Global keyboard shortcuts (vim-like) - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Ignore if in naturally interactive elements - if ( - e.target instanceof HTMLInputElement || - e.target instanceof HTMLTextAreaElement || - e.target instanceof HTMLSelectElement - ) { - return; - } - - // Ignore if modifier keys are pressed (prevents conflicts with browser shortcuts like Ctrl+D) - if (e.ctrlKey || e.altKey || e.metaKey) return; - - // These shortcuts always work - const alwaysEnabled = ['t', 'c']; - if (selectedCount === 0 && !alwaysEnabled.includes(e.key)) return; - - switch (e.key) { - case 'y': - handleCopy(); - break; - case 'd': - handleDownload(); - break; - case 't': - // Flash effect for theme toggle - document.body.classList.add('theme-flash'); - setTimeout(() => document.body.classList.remove('theme-flash'), 150); - toggleTheme(); - break; - case 'c': - clearAll(); - break; - case '1': - if (showAur) setSelectedHelper('yay'); - break; - case '2': - if (showAur) setSelectedHelper('paru'); - break; - case 'Tab': - e.preventDefault(); - if (selectedCount > 0) { - setDrawerOpen(prev => !prev); - } - break; - } - }; - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedCount, toggleTheme, clearAll, showAur, setHasYayInstalled]); - - const handleCopy = async () => { + const handleCopy = useCallback(async () => { if (selectedCount === 0) return; await navigator.clipboard.writeText(command); setCopied(true); - setShowCopyTooltip(true); const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro; analytics.commandCopied(distroName, selectedCount); - setTimeout(() => { - setCopied(false); - setShowCopyTooltip(false); - }, 3000); - }; + setTimeout(() => setCopied(false), 3000); + }, [command, selectedCount, selectedDistro]); - const handleDownload = () => { + const handleDownload = useCallback(() => { if (selectedCount === 0) return; const script = generateInstallScript({ distroId: selectedDistro, @@ -166,11 +96,50 @@ export function CommandFooter({ setTimeout(() => URL.revokeObjectURL(url), 1000); const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro; analytics.scriptDownloaded(distroName, selectedCount); - }; + }, [selectedCount, selectedDistro, selectedApps, selectedHelper]); + + // Global keyboard shortcuts (vim-like) + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement || + e.target instanceof HTMLSelectElement + ) { + return; + } + + // Ignore modifier keys (prevents conflicts with browser shortcuts) + if (e.ctrlKey || e.altKey || e.metaKey) return; + + const alwaysEnabled = ['t', 'c']; + if (selectedCount === 0 && !alwaysEnabled.includes(e.key)) return; + + switch (e.key) { + case 'y': handleCopy(); break; + case 'd': handleDownload(); break; + case 't': + document.body.classList.add('theme-flash'); + setTimeout(() => document.body.classList.remove('theme-flash'), 150); + toggleTheme(); + break; + case 'c': clearAll(); break; + case '1': if (showAur) setSelectedHelper('yay'); break; + case '2': if (showAur) setSelectedHelper('paru'); break; + case 'Tab': + e.preventDefault(); + if (selectedCount > 0) setDrawerOpen(prev => !prev); + break; + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedCount, toggleTheme, clearAll, showAur, setSelectedHelper, handleCopy, handleDownload]); return ( <> - {/* AUR Floating Card - appears when AUR packages selected (outside animated container) */} + {/* AUR Floating Card */} - {/* Slide-up Drawer - outside animated container for proper positioning */} - {drawerOpen && ( - <> - {/* Backdrop */} -