feat: refined UI components and updated app data

This commit is contained in:
NIJAT
2025-12-29 12:36:28 +04:00
parent f83293afe2
commit 00d521750e
13 changed files with 1098 additions and 885 deletions

View File

@@ -64,11 +64,10 @@ export const AppItem = memo(function AppItem({
onTooltipLeave,
onFocus,
}: AppItemProps) {
// Build unavailable tooltip text
// Build unavailable tooltip text (just the reason, no description)
const getUnavailableText = () => {
if (app.unavailableReason) return app.unavailableReason;
const distroName = distros.find(d => d.id === selectedDistro)?.name || '';
return `Not available in ${distroName} repos`;
return app.unavailableReason || `Not available in ${distroName} repos`;
};
const isAur = selectedDistro === 'arch' && app.targets?.arch && isAurPackage(app.targets.arch);
@@ -98,10 +97,11 @@ export const AppItem = memo(function AppItem({
}
}}
onMouseEnter={(e) => {
if (isAvailable) onTooltipEnter(app.description, e);
// Show description tooltip for all apps (available and unavailable)
onTooltipEnter(app.description, e);
}}
onMouseLeave={() => {
if (isAvailable) onTooltipLeave();
onTooltipLeave();
}}
>
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-all duration-150

View File

@@ -58,7 +58,7 @@ function CategorySectionComponent({
const hasAnimated = useRef(false);
const prevAppCount = useRef(categoryApps.length);
// Initial entrance animation
// Initial entrance animation - GPU optimized
useLayoutEffect(() => {
if (!sectionRef.current || hasAnimated.current) return;
hasAnimated.current = true;
@@ -67,27 +67,33 @@ function CategorySectionComponent({
const header = section.querySelector('.category-header');
const items = section.querySelectorAll('.app-item');
// Initial state
gsap.set(header, { clipPath: 'inset(0 100% 0 0)' });
gsap.set(items, { y: -20, opacity: 0 });
// Use requestAnimationFrame for smoother initial setup
requestAnimationFrame(() => {
// Initial state with GPU-accelerated transforms
gsap.set(header, { clipPath: 'inset(0 100% 0 0)' });
gsap.set(items, { y: -15, opacity: 0, force3D: true });
// Animate with staggered delay based on category index
const delay = categoryIndex * 0.08;
// Staggered delay based on category index (reduced for faster feel)
const delay = categoryIndex * 0.05;
gsap.to(header, {
clipPath: 'inset(0 0% 0 0)',
duration: 0.9,
ease: 'power3.out',
delay: delay + 0.1
});
// Animate header with clip-path reveal
gsap.to(header, {
clipPath: 'inset(0 0% 0 0)',
duration: 0.6,
ease: 'power2.out',
delay: delay + 0.05
});
gsap.to(items, {
y: 0,
opacity: 1,
duration: 0.8,
stagger: 0.04,
ease: 'expo.out',
delay: delay + 0.2
// Animate items with GPU-accelerated transforms
gsap.to(items, {
y: 0,
opacity: 1,
duration: 0.5,
stagger: 0.025,
ease: 'power2.out',
delay: delay + 0.1,
force3D: true
});
});
}, [categoryIndex]);

View File

@@ -1,7 +1,5 @@
'use client';
import { Package, Download, Terminal } from 'lucide-react';
interface AurDrawerSettingsProps {
aurAppNames: string[];
hasYayInstalled: boolean;
@@ -11,7 +9,7 @@ interface AurDrawerSettingsProps {
}
/**
* AurDrawerSettings - Settings panel for AUR configuration inside the drawer
* AurDrawerSettings - Compact UI with smooth button animations
*/
export function AurDrawerSettings({
aurAppNames,
@@ -21,95 +19,81 @@ export function AurDrawerSettings({
setSelectedHelper,
}: AurDrawerSettingsProps) {
return (
<div className="mb-6 rounded-xl border border-[#1793d1]/20 bg-[#1793d1]/5 overflow-hidden">
{/* Header */}
<div className="px-5 py-4 border-b border-[#1793d1]/10 flex items-start gap-3">
<div className="p-2 rounded-lg bg-[#1793d1]/10 text-[#1793d1] shrink-0">
<Package className="w-5 h-5" />
</div>
<div>
<h4 className="text-sm font-semibold text-[var(--text-primary)]">
AUR Packages Detected
</h4>
<p className="text-xs text-[var(--text-muted)] mt-1 leading-relaxed">
These apps require an AUR helper: <span className="text-[var(--text-primary)] opacity-80">{aurAppNames.join(', ')}</span>
</p>
</div>
<div className="mb-4 rounded-xl bg-[var(--bg-tertiary)] border border-[var(--border-primary)]/40 overflow-hidden">
{/* Header with all apps listed */}
<div className="px-4 py-3 border-b border-[var(--border-primary)]/30">
<p className="text-xs text-[var(--text-muted)] leading-relaxed">
<span className="font-medium text-[var(--text-primary)]">AUR packages: </span>
{aurAppNames.join(', ')}
</p>
</div>
{/* Controls Grid */}
<div className="p-5 grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 1. Installation Mode */}
<div>
<label className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider mb-2.5 flex items-center gap-2">
<Download className="w-3.5 h-3.5" />
Installation Logic
</label>
<div className="flex bg-[var(--bg-tertiary)] p-1 rounded-lg border border-[var(--border-primary)]/50">
<button
onClick={() => setHasYayInstalled(false)}
className={`flex-1 py-2 px-3 text-xs font-medium rounded-md transition-all ${!hasYayInstalled
? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm border border-[var(--border-primary)]/50'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
}`}
>
Install New
</button>
<button
onClick={() => setHasYayInstalled(true)}
className={`flex-1 py-2 px-3 text-xs font-medium rounded-md transition-all ${hasYayInstalled
? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm border border-[var(--border-primary)]/50'
: 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'
}`}
>
I Have One
</button>
</div>
<p className="text-[10px] text-[var(--text-muted)] mt-2 opacity-70 px-1">
{hasYayInstalled
? "Script will use your existing helper"
: "Script will install the helper first"}
</p>
</div>
{/* 2. Helper Selection */}
<div>
<label className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider mb-2.5 flex items-center gap-2">
<Terminal className="w-3.5 h-3.5" />
Preferred Helper
</label>
<div className="grid grid-cols-2 gap-2">
{/* Controls with animated buttons */}
<div className="px-4 py-3 flex flex-col sm:flex-row sm:items-center gap-4 text-xs">
{/* Helper selection */}
<div className="flex items-center gap-3">
<span className="text-[var(--text-secondary)] font-medium">AUR helper:</span>
<div className="flex bg-[var(--bg-secondary)] rounded-lg p-1 border border-[var(--border-primary)]/30">
<button
onClick={() => setSelectedHelper('yay')}
className={`relative px-3 py-2 rounded-lg text-left border transition-all ${selectedHelper === 'yay'
? 'bg-[#1793d1]/10 border-[#1793d1]/30 text-[#1793d1]'
: 'bg-[var(--bg-tertiary)] border-[var(--border-primary)]/50 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]'
className={`relative px-3 py-1.5 rounded-md font-medium transition-all duration-200 ease-out ${selectedHelper === 'yay'
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'
}`}
style={{
transform: selectedHelper === 'yay' ? 'scale(1)' : 'scale(0.98)',
}}
>
<span className="block text-xs font-bold">yay</span>
<span className="block text-[10px] opacity-70 mt-0.5">Go-based</span>
{selectedHelper === 'yay' && (
<div className="absolute top-2 right-2 w-1.5 h-1.5 rounded-full bg-[#1793d1]" />
)}
yay <span className="opacity-60 font-normal">(Go)</span>
</button>
<button
onClick={() => setSelectedHelper('paru')}
className={`relative px-3 py-2 rounded-lg text-left border transition-all ${selectedHelper === 'paru'
? 'bg-[#1793d1]/10 border-[#1793d1]/30 text-[#1793d1]'
: 'bg-[var(--bg-tertiary)] border-[var(--border-primary)]/50 text-[var(--text-muted)] hover:bg-[var(--bg-hover)]'
className={`relative px-3 py-1.5 rounded-md font-medium transition-all duration-200 ease-out ${selectedHelper === 'paru'
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'
}`}
style={{
transform: selectedHelper === 'paru' ? 'scale(1)' : 'scale(0.98)',
}}
>
<span className="block text-xs font-bold">paru</span>
<span className="block text-[10px] opacity-70 mt-0.5">Rust-based</span>
{selectedHelper === 'paru' && (
<div className="absolute top-2 right-2 w-1.5 h-1.5 rounded-full bg-[#1793d1]" />
)}
paru <span className="opacity-60 font-normal">(Rust)</span>
</button>
</div>
</div>
{/* Divider */}
<div className="w-px h-5 bg-[var(--border-primary)]/40 hidden sm:block" />
{/* Installation mode */}
<div className="flex items-center gap-3">
<span className="text-[var(--text-secondary)] font-medium">Already installed?</span>
<div className="flex bg-[var(--bg-secondary)] rounded-lg p-1 border border-[var(--border-primary)]/30">
<button
onClick={() => setHasYayInstalled(true)}
className={`relative px-3 py-1.5 rounded-md font-medium transition-all duration-200 ease-out ${hasYayInstalled
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'
}`}
style={{
transform: hasYayInstalled ? 'scale(1)' : 'scale(0.98)',
}}
>
Yes, use it
</button>
<button
onClick={() => setHasYayInstalled(false)}
className={`relative px-3 py-1.5 rounded-md font-medium transition-all duration-200 ease-out ${!hasYayInstalled
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
: 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'
}`}
style={{
transform: !hasYayInstalled ? 'scale(1)' : 'scale(0.98)',
}}
>
No, install it
</button>
</div>
</div>
</div>
</div>
);

View File

@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { X } from 'lucide-react';
interface AurFloatingCardProps {
@@ -31,19 +31,19 @@ export function AurFloatingCard({
const [hasAnswered, setHasAnswered] = useState<boolean | null>(null);
// Track if user has selected a helper (completed flow)
const [helperChosen, setHelperChosen] = useState(false);
// Track if user has interacted (dismissed or selected) to prevent nagging
const [userInteracted, setUserInteracted] = useState(false);
// Track if user has interacted (dismissed or selected) to prevent nagging - use ref to persist
const userInteractedRef = useRef(false);
// Reset when new AUR packages appear, BUT ONLY if user hasn't interacted yet
useEffect(() => {
if (show && aurAppNames.length > 0 && !userInteracted) {
if (show && aurAppNames.length > 0 && !userInteractedRef.current) {
setDismissed(false);
setIsExiting(false);
setShowConfirmation(false);
setHelperChosen(false);
setHasAnswered(null);
}
}, [aurAppNames.length, show, userInteracted]);
}, [aurAppNames.length, show]);
if (!show || dismissed) return null;
@@ -55,7 +55,7 @@ export function AurFloatingCard({
const handleHelperSelect = (helper: 'yay' | 'paru') => {
setSelectedHelper(helper);
setHelperChosen(true);
setUserInteracted(true); // Don't ask again
userInteractedRef.current = true; // Don't ask again
// Start exit animation after a brief moment
setTimeout(() => {
@@ -67,7 +67,7 @@ export function AurFloatingCard({
};
const handleDismiss = () => {
setUserInteracted(true); // Don't ask again
userInteractedRef.current = true; // Don't ask again
setIsExiting(true);
setTimeout(() => {
setDismissed(true);
@@ -87,11 +87,11 @@ export function AurFloatingCard({
}, 3000);
return (
<div className="fixed bottom-24 right-4 z-30">
<div className="fixed top-4 right-4 z-30">
<p
className="text-[13px] text-[var(--text-muted)]"
style={{
animation: 'cardSlideIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
animation: 'slideInFromRight 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
}}
>
You can change this later in preview tab
@@ -103,17 +103,17 @@ export function AurFloatingCard({
// Hide cards while exiting
if (isExiting && helperChosen) {
return (
<div className="fixed bottom-24 right-4 z-30 flex flex-col gap-3 items-end">
<div className="fixed top-4 right-4 z-30 flex flex-col gap-3 items-end">
<div
className="w-72 bg-[var(--bg-secondary)] backdrop-blur-xl border border-[var(--border-primary)]/60 rounded-2xl shadow-xl shadow-black/10 overflow-hidden"
style={{ animation: 'cardSlideOut 0.25s ease-out forwards' }}
style={{ animation: 'slideOutToRight 0.25s ease-out forwards' }}
>
<div className="p-4" />
</div>
{hasAnswered !== null && (
<div
className="w-72 bg-[var(--bg-secondary)] backdrop-blur-xl border border-[var(--border-primary)]/60 rounded-2xl shadow-xl shadow-black/10 overflow-hidden"
style={{ animation: 'cardSlideOut 0.2s ease-out forwards' }}
style={{ animation: 'slideOutToRight 0.2s ease-out forwards' }}
>
<div className="p-4" />
</div>
@@ -123,7 +123,7 @@ export function AurFloatingCard({
}
return (
<div className="fixed bottom-24 right-4 z-30 flex flex-col gap-3 items-end">
<div className="fixed top-4 right-4 z-30 flex flex-col gap-3 items-end">
{/* Card 1: Do you have an AUR helper? */}
<div
className={`
@@ -135,8 +135,8 @@ export function AurFloatingCard({
`}
style={{
animation: isExiting
? 'cardSlideOut 0.2s ease-out forwards'
: 'cardSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
? 'slideOutToRight 0.2s ease-out forwards'
: 'slideInFromRight 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
}}
>
{/* Header */}
@@ -207,8 +207,8 @@ export function AurFloatingCard({
`}
style={{
animation: isExiting
? 'cardSlideOut 0.15s ease-out forwards'
: 'cardSlideInSecond 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s forwards',
? 'slideOutToRight 0.15s ease-out forwards'
: 'slideInFromRightSecond 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s forwards',
opacity: 0
}}
>

View File

@@ -166,8 +166,8 @@ export function CommandFooter({
};
return (
<div className="fixed bottom-0 left-0 right-0 p-3" style={{ zIndex: 10 }}>
{/* AUR Floating Card - appears when AUR packages selected */}
<>
{/* AUR Floating Card - appears when AUR packages selected (outside animated container) */}
<AurFloatingCard
show={showAur}
aurAppNames={aurAppNames}
@@ -177,241 +177,233 @@ export function CommandFooter({
setSelectedHelper={setSelectedHelper}
/>
{/* Footer container with strong outer glow */}
<div className="relative w-[85%] mx-auto">
{/* Outer glow - large spread */}
<div
className="absolute -inset-12 rounded-3xl pointer-events-none"
style={{
background: 'var(--bg-primary)',
filter: 'blur(40px)',
zIndex: -1
}}
/>
{/* Middle glow layer */}
<div
className="absolute -inset-8 rounded-3xl pointer-events-none"
style={{
background: 'var(--bg-primary)',
filter: 'blur(30px)',
zIndex: -1
}}
/>
{/* Inner glow - sharp */}
<div
className="absolute -inset-4 rounded-2xl pointer-events-none"
style={{
background: 'var(--bg-primary)',
filter: 'blur(20px)',
zIndex: -1
}}
/>
{/* Bars container */}
<div className="relative flex flex-col gap-1.5">
{/* Shortcuts Bar with Search (nvim-style) */}
<ShortcutsBar
searchQuery={searchQuery}
onSearchChange={onSearchChange}
searchInputRef={searchInputRef}
selectedCount={selectedCount}
distroName={distroDisplayName}
showAur={showAur}
selectedHelper={selectedHelper}
setSelectedHelper={setSelectedHelper}
{/* Slide-up Drawer - outside animated container for proper positioning */}
{drawerOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
onClick={closeDrawer}
aria-hidden="true"
style={{ animation: drawerClosing ? 'fadeOut 0.3s ease-out forwards' : 'fadeIn 0.3s ease-out' }}
/>
{/* Command Bar - Bufferline style tabs */}
<div className="bg-[var(--bg-tertiary)] font-mono text-xs rounded-lg overflow-hidden border border-[var(--border-primary)]/40 shadow-2xl">
<div className="flex items-stretch">
{/* Tab: Expand/Preview (opens drawer) */}
{/* Drawer */}
<div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title"
className="fixed z-50 bg-[var(--bg-secondary)] border border-[var(--border-primary)] shadow-2xl
bottom-0 left-0 right-0 rounded-t-2xl
md:bottom-auto md:top-1/2 md:left-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-2xl md:max-w-2xl md:w-[90vw]"
style={{
animation: drawerClosing
? 'slideDown 0.3s cubic-bezier(0.32, 0, 0.67, 0) forwards'
: 'slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
maxHeight: '80vh'
}}
>
{/* Drawer Handle - mobile only */}
<div className="flex justify-center pt-3 pb-2 md:hidden">
<button
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
disabled={selectedCount === 0}
className={`flex items-center gap-2 px-4 py-3 border-r border-[var(--border-primary)]/30 transition-all shrink-0 bg-indigo-500/10 text-indigo-400 hover:bg-indigo-500/20 hover:text-indigo-300 ${selectedCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
title="Toggle Preview (Tab)"
>
<ChevronUp className="w-3.5 h-3.5 shrink-0" />
<span className="font-bold whitespace-nowrap">PREVIEW</span>
{selectedCount > 0 && (
<span className="text-[10px] opacity-60 ml-0.5 whitespace-nowrap">[{selectedCount}]</span>
)}
</button>
className="w-12 h-1.5 bg-[var(--text-muted)]/40 rounded-full cursor-pointer hover:bg-[var(--text-muted)] transition-colors"
onClick={closeDrawer}
aria-label="Close drawer"
/>
</div>
{/* Command text - fills available space, centered both ways */}
<div
className="flex-1 min-w-0 flex items-center justify-center px-4 py-4 overflow-hidden bg-[var(--bg-secondary)] cursor-pointer hover:bg-[var(--bg-hover)] transition-colors group"
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
>
<code className={`whitespace-nowrap overflow-x-auto command-scroll leading-none ${selectedCount > 0 ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'}`}>
{command}
</code>
{/* Drawer Header */}
<div className="flex items-center justify-between px-4 sm:px-6 pb-3 md:pt-4 border-b border-[var(--border-primary)]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center">
<span className="text-emerald-500 font-bold text-sm">$</span>
</div>
<div>
<h3 id="drawer-title" className="text-sm font-semibold text-[var(--text-primary)]">Terminal Command</h3>
<p className="text-xs text-[var(--text-muted)]">{selectedCount} app{selectedCount !== 1 ? 's' : ''} Press Esc to close</p>
</div>
</div>
<button
onClick={closeDrawer}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
aria-label="Close drawer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Tab: Download */}
{/* Command Content */}
<div className="p-4 sm:p-6 overflow-y-auto" style={{ maxHeight: 'calc(80vh - 200px)' }}>
{/* AUR Settings (if AUR packages selected) */}
{showAur && (
<AurDrawerSettings
aurAppNames={aurAppNames}
hasYayInstalled={hasYayInstalled}
setHasYayInstalled={setHasYayInstalled}
selectedHelper={selectedHelper}
setSelectedHelper={setSelectedHelper}
/>
)}
<div className="bg-[#1a1a1a] rounded-xl overflow-hidden border border-[var(--border-primary)]">
<div className="flex items-center justify-between px-4 py-2 bg-[#252525] border-b border-[var(--border-primary)]">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/80" />
<div className="w-3 h-3 rounded-full bg-yellow-500/80" />
<div className="w-3 h-3 rounded-full bg-green-500/80" />
<span className="ml-2 text-xs text-[var(--text-muted)]">bash</span>
</div>
<div className="hidden md:flex items-center gap-2">
<button
onClick={handleDownload}
className="h-7 px-3 flex items-center gap-1.5 rounded-md bg-[var(--bg-tertiary)]/50 text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] transition-colors text-xs font-medium"
>
<Download className="w-3.5 h-3.5" />
Download
</button>
<button
onClick={() => { handleCopy(); setTimeout(closeDrawer, 3000); }}
className={`h-7 px-3 flex items-center gap-1.5 rounded-md text-xs font-medium transition-all ${copied
? 'bg-emerald-600 text-white'
: 'bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600 hover:text-white'
}`}
>
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
<div className="p-4 font-mono text-sm overflow-x-auto">
<div className="flex gap-2">
<span className="text-emerald-400 select-none shrink-0">$</span>
<code className="text-gray-300 break-all whitespace-pre-wrap" style={{ lineHeight: '1.6' }}>
{command}
</code>
</div>
</div>
</div>
</div>
{/* Mobile Actions */}
<div className="md:hidden flex flex-col items-stretch gap-3 px-4 py-4 border-t border-[var(--border-primary)]">
<button
onClick={handleDownload}
disabled={selectedCount === 0}
onMouseEnter={() => selectedCount > 0 && setShowDownloadTooltip(true)}
onMouseLeave={() => setShowDownloadTooltip(false)}
className={`flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-colors ${selectedCount > 0
? 'text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)] hover:text-[var(--text-primary)]'
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
}`}
title="Download Script (d)"
className="flex-1 h-14 flex items-center justify-center gap-2 rounded-xl bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors font-medium text-base"
>
<Download className="w-3 h-3 shrink-0" />
<span className="hidden sm:inline whitespace-nowrap">Download</span>
<Download className="w-5 h-5" />
Download Script
</button>
{/* Tab: Copy (highlighted) */}
<button
onClick={handleCopy}
disabled={selectedCount === 0}
className={`flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-colors ${selectedCount > 0
? (copied
? 'bg-emerald-600 text-white'
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90')
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
onClick={() => { handleCopy(); setTimeout(closeDrawer, 3000); }}
className={`flex-1 h-14 flex items-center justify-center gap-2 rounded-xl font-medium text-base transition-all ${copied
? 'bg-emerald-600 text-white'
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90'
}`}
title="Copy Command (y)"
>
{copied ? <Check className="w-3 h-3 shrink-0" /> : <Copy className="w-3 h-3 shrink-0" />}
<span className="hidden sm:inline whitespace-nowrap">{copied ? 'Copied!' : 'Copy'}</span>
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
{copied ? 'Copied!' : 'Copy Command'}
</button>
</div>
</div>
</>
)}
{/* Slide-up Drawer */}
{drawerOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-40"
onClick={closeDrawer}
aria-hidden="true"
style={{ animation: drawerClosing ? 'fadeOut 0.3s ease-out forwards' : 'fadeIn 0.3s ease-out' }}
/>
{/* Drawer */}
<div
role="dialog"
aria-modal="true"
aria-labelledby="drawer-title"
className="fixed z-50 bg-[var(--bg-secondary)] border border-[var(--border-primary)] shadow-2xl
bottom-0 left-0 right-0 rounded-t-2xl
md:bottom-auto md:top-1/2 md:left-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:rounded-2xl md:max-w-2xl md:w-[90vw]"
style={{
animation: drawerClosing
? 'slideDown 0.3s cubic-bezier(0.32, 0, 0.67, 0) forwards'
: 'slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
maxHeight: '80vh'
}}
>
{/* Drawer Handle - mobile only */}
<div className="flex justify-center pt-3 pb-2 md:hidden">
<button
className="w-12 h-1.5 bg-[var(--text-muted)]/40 rounded-full cursor-pointer hover:bg-[var(--text-muted)] transition-colors"
onClick={closeDrawer}
aria-label="Close drawer"
/>
</div>
{/* Animated footer container */}
<div
className="fixed bottom-0 left-0 right-0 p-3"
style={{
zIndex: 10,
animation: 'footerSlideUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.3s both'
}}
>
{/* Footer container with glow */}
<div className="relative w-[85%] mx-auto">
{/* Soft glow behind bars */}
<div
className="absolute -inset-12 pointer-events-none"
style={{
background: 'var(--bg-primary)',
filter: 'blur(40px)',
opacity: 1,
zIndex: -1
}}
/>
{/* Drawer Header */}
<div className="flex items-center justify-between px-4 sm:px-6 pb-3 md:pt-4 border-b border-[var(--border-primary)]">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-emerald-500/20 flex items-center justify-center">
<span className="text-emerald-500 font-bold text-sm">$</span>
</div>
<div>
<h3 id="drawer-title" className="text-sm font-semibold text-[var(--text-primary)]">Terminal Command</h3>
<p className="text-xs text-[var(--text-muted)]">{selectedCount} app{selectedCount !== 1 ? 's' : ''} Press Esc to close</p>
</div>
</div>
<button
onClick={closeDrawer}
className="w-8 h-8 flex items-center justify-center rounded-full hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
aria-label="Close drawer"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Bars container */}
<div className="relative flex flex-col gap-1.5">
{/* Shortcuts Bar with Search (nvim-style) */}
<ShortcutsBar
searchQuery={searchQuery}
onSearchChange={onSearchChange}
searchInputRef={searchInputRef}
selectedCount={selectedCount}
distroName={distroDisplayName}
showAur={showAur}
selectedHelper={selectedHelper}
setSelectedHelper={setSelectedHelper}
/>
{/* Command Content */}
<div className="p-4 sm:p-6 overflow-y-auto" style={{ maxHeight: 'calc(80vh - 200px)' }}>
{/* AUR Settings (if AUR packages selected) */}
{showAur && (
<AurDrawerSettings
aurAppNames={aurAppNames}
hasYayInstalled={hasYayInstalled}
setHasYayInstalled={setHasYayInstalled}
selectedHelper={selectedHelper}
setSelectedHelper={setSelectedHelper}
/>
{/* Command Bar - Bufferline style tabs */}
<div className="bg-[var(--bg-tertiary)] font-mono text-xs rounded-lg overflow-hidden border border-[var(--border-primary)]/40 shadow-2xl">
<div className="flex items-stretch">
{/* Tab: Expand/Preview (opens drawer) */}
<button
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
disabled={selectedCount === 0}
className={`flex items-center gap-2 px-4 py-3 border-r border-[var(--border-primary)]/30 transition-all shrink-0 bg-indigo-500/10 text-indigo-400 hover:bg-indigo-500/20 hover:text-indigo-300 ${selectedCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
title="Toggle Preview (Tab)"
>
<ChevronUp className="w-3.5 h-3.5 shrink-0" />
<span className="font-bold whitespace-nowrap">PREVIEW</span>
{selectedCount > 0 && (
<span className="text-[10px] opacity-60 ml-0.5 whitespace-nowrap">[{selectedCount}]</span>
)}
<div className="bg-[#1a1a1a] rounded-xl overflow-hidden border border-[var(--border-primary)]">
<div className="flex items-center justify-between px-4 py-2 bg-[#252525] border-b border-[var(--border-primary)]">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500/80" />
<div className="w-3 h-3 rounded-full bg-yellow-500/80" />
<div className="w-3 h-3 rounded-full bg-green-500/80" />
<span className="ml-2 text-xs text-[var(--text-muted)]">bash</span>
</div>
<div className="hidden md:flex items-center gap-2">
<button
onClick={handleDownload}
className="h-7 px-3 flex items-center gap-1.5 rounded-md bg-[var(--bg-tertiary)]/50 text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] transition-colors text-xs font-medium"
>
<Download className="w-3.5 h-3.5" />
Download
</button>
<button
onClick={() => { handleCopy(); setTimeout(closeDrawer, 3000); }}
className={`h-7 px-3 flex items-center gap-1.5 rounded-md text-xs font-medium transition-all ${copied
? 'bg-emerald-600 text-white'
: 'bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600 hover:text-white'
}`}
>
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>
<div className="p-4 font-mono text-sm overflow-x-auto">
<div className="flex gap-2">
<span className="text-emerald-400 select-none shrink-0">$</span>
<code className="text-gray-300 break-all whitespace-pre-wrap" style={{ lineHeight: '1.6' }}>
{command}
</code>
</div>
</div>
</div>
</button>
{/* Command text - fills available space, centered both ways */}
<div
className="flex-1 min-w-0 flex items-center justify-center px-4 py-4 overflow-hidden bg-[var(--bg-secondary)] cursor-pointer hover:bg-[var(--bg-hover)] transition-colors group"
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
>
<code className={`whitespace-nowrap overflow-x-auto command-scroll leading-none ${selectedCount > 0 ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'}`}>
{command}
</code>
</div>
{/* Mobile Actions */}
<div className="md:hidden flex flex-col items-stretch gap-3 px-4 py-4 border-t border-[var(--border-primary)]">
<button
onClick={handleDownload}
className="flex-1 h-14 flex items-center justify-center gap-2 rounded-xl bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] transition-colors font-medium text-base"
>
<Download className="w-5 h-5" />
Download Script
</button>
<button
onClick={() => { handleCopy(); setTimeout(closeDrawer, 3000); }}
className={`flex-1 h-14 flex items-center justify-center gap-2 rounded-xl font-medium text-base transition-all ${copied
{/* Tab: Download */}
<button
onClick={handleDownload}
disabled={selectedCount === 0}
onMouseEnter={() => selectedCount > 0 && setShowDownloadTooltip(true)}
onMouseLeave={() => setShowDownloadTooltip(false)}
className={`flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-colors ${selectedCount > 0
? 'text-[var(--text-secondary)] hover:bg-[var(--bg-secondary)] hover:text-[var(--text-primary)]'
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
}`}
title="Download Script (d)"
>
<Download className="w-3 h-3 shrink-0" />
<span className="hidden sm:inline whitespace-nowrap">Download</span>
</button>
{/* Tab: Copy (highlighted) */}
<button
onClick={handleCopy}
disabled={selectedCount === 0}
className={`flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-colors ${selectedCount > 0
? (copied
? 'bg-emerald-600 text-white'
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90'
}`}
>
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
{copied ? 'Copied!' : 'Copy Command'}
</button>
</div>
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90')
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
}`}
title="Copy Command (y)"
>
{copied ? <Check className="w-3 h-3 shrink-0" /> : <Copy className="w-3 h-3 shrink-0" />}
<span className="hidden sm:inline whitespace-nowrap">{copied ? 'Copied!' : 'Copy'}</span>
</button>
</div>
</>
)}
</div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -17,6 +17,11 @@ interface ShortcutsBarProps {
/**
* ShortcutsBar - Neovim-style statusline with search on left, shortcuts on right
* Uses theme-aware colors for dark/light mode compatibility
*
* Enhancements:
* - Cleaner grouping of shortcuts
* - Better visual hierarchy
* - Softer separators
*/
export const ShortcutsBar = forwardRef<HTMLInputElement, ShortcutsBarProps>(
function ShortcutsBar({
@@ -37,13 +42,6 @@ export const ShortcutsBar = forwardRef<HTMLInputElement, ShortcutsBarProps>(
}
};
const helperShortcuts = showAur ? [
{ key: '1', label: 'yay' },
{ key: '2', label: 'paru' },
] : [];
return (
<div className="bg-[var(--bg-tertiary)] border border-[var(--border-primary)] font-mono text-xs rounded-lg overflow-hidden">
<div className="flex items-stretch justify-between">
@@ -94,67 +92,46 @@ export const ShortcutsBar = forwardRef<HTMLInputElement, ShortcutsBarProps>(
<div className="flex items-stretch border-r border-[var(--border-primary)]/30">
<button
onClick={() => setSelectedHelper('yay')}
className={`px-3 flex items-center gap-2 text-[10px] font-medium transition-colors border-r border-[var(--border-primary)]/30 whitespace-nowrap ${selectedHelper === 'yay' ? 'bg-[var(--text-primary)] text-[var(--bg-primary)] font-bold' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'}`}
className={`px-3 flex items-center gap-1.5 text-[10px] font-medium transition-colors border-r border-[var(--border-primary)]/30 whitespace-nowrap ${selectedHelper === 'yay' ? 'bg-[var(--text-primary)] text-[var(--bg-primary)] font-bold' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'}`}
>
<span className="font-mono opacity-70">1</span>
<span className="font-mono opacity-50">1</span>
yay
</button>
<button
onClick={() => setSelectedHelper('paru')}
className={`px-3 flex items-center gap-2 text-[10px] font-medium transition-colors whitespace-nowrap ${selectedHelper === 'paru' ? 'bg-[var(--text-primary)] text-[var(--bg-primary)] font-bold' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'}`}
className={`px-3 flex items-center gap-1.5 text-[10px] font-medium transition-colors whitespace-nowrap ${selectedHelper === 'paru' ? 'bg-[var(--text-primary)] text-[var(--bg-primary)] font-bold' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]'}`}
>
<span className="font-mono opacity-70">2</span>
<span className="font-mono opacity-50">2</span>
paru
</button>
</div>
)}
</div>
{/* RIGHT SECTION - Shortcuts */}
{/* RIGHT SECTION - Compact Shortcuts */}
<div className="flex items-stretch">
<div className="hidden sm:flex items-center gap-4 px-4 py-1 text-[var(--text-muted)] text-[11px] font-medium border-l border-[var(--border-primary)]/30">
{/* Navigation Group */}
<div className="hidden lg:flex items-center gap-1.5 transition-opacity hover:opacity-100">
<span className="font-mono text-[10px] tracking-widest text-[var(--text-muted)]">NAV</span>
<div className="flex items-center gap-1 font-mono text-[var(--text-primary)]">
<span></span>
<span className="opacity-50">/</span>
<span>hjkl</span>
</div>
</div>
{/* Separator */}
<div className="w-px h-3 bg-[var(--border-primary)]/40 hidden lg:block"></div>
{/* Actions Group */}
<div className="flex items-center gap-4">
{[...helperShortcuts,
{ key: '/', label: 'Search' },
{ key: 'Space', label: 'Toggle' },
{ key: 'y', label: 'Copy' },
{ key: 'd', label: 'Download' },
{ key: 'c', label: 'Clear' },
{ key: 't', label: 'Theme' }
].map(({ key, label }) => (
<div key={key} className="flex items-center gap-1.5 group cursor-help transition-colors hover:text-[var(--text-primary)]">
<span className={`font-mono font-bold transition-colors ${showAur && (key === '1' || key === '2') ? 'text-[#1793d1]' : 'text-[var(--text-primary)] group-hover:text-[#1793d1]'}`}>
{key}
</span>
<span className="opacity-60 group-hover:opacity-100 transition-opacity">{label}</span>
</div>
))}
<div className="flex items-center gap-4 border-l border-[var(--border-primary)]/40 pl-4">
<div className="flex items-center gap-1.5 transition-colors hover:text-[var(--text-primary)]">
<span className="font-mono font-bold text-[var(--text-primary)]">Esc</span>
<span className="opacity-60">Back</span>
</div>
<div className="flex items-center gap-1.5 transition-colors hover:text-[var(--text-primary)]">
<span className="font-mono font-bold text-[var(--text-primary)]">Tab</span>
<span className="opacity-60">Preview</span>
</div>
</div>
</div>
<div className="hidden sm:flex items-center gap-3 px-3 py-1 text-[var(--text-muted)] text-[10px] border-l border-[var(--border-primary)]/30">
{/* Navigation */}
<span className="hidden lg:inline"><b className="text-[var(--text-secondary)]"> </b>/<b className="text-[var(--text-secondary)]"> hjkl</b> Navigation</span>
<span className="hidden lg:inline opacity-30">·</span>
{/* Actions */}
<span><b className="text-[var(--text-secondary)]">/</b> search</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">Space</b> toggle</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">y</b> copy</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">d</b> download</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">c</b> clear</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">t</b> theme</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">Tab</b> preview</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">Esc</b> back</span>
<span className="opacity-30">·</span>
<span><b className="text-[var(--text-secondary)]">?</b> help</span>
</div>
{/* End badge - like nvim line:col */}

View File

@@ -3,79 +3,166 @@
/**
* LoadingSkeleton - Placeholder UI while localStorage hydrates
*
* Shows animated skeleton blocks mimicking the app grid layout
* to prevent layout shift and provide visual feedback during loading.
* Shows animated skeleton blocks mimicking the app grid layout.
* Uses INLINE keyframe animation to ensure buttery smooth animation
* from the very first render (before CSS is fully loaded/parsed).
*/
export function LoadingSkeleton() {
return (
<div className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)]">
{/* Header Skeleton */}
<header className="pt-8 sm:pt-12 pb-8 sm:pb-10 px-4 sm:px-6">
<div className="max-w-6xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-start gap-4">
{/* Logo placeholder */}
<div className="w-16 h-16 sm:w-[72px] sm:h-[72px] rounded-xl bg-[var(--bg-tertiary)] animate-pulse" />
<div className="flex flex-col gap-2">
<div className="h-6 w-32 bg-[var(--bg-tertiary)] rounded animate-pulse" />
<div className="h-3 w-48 bg-[var(--bg-tertiary)] rounded animate-pulse" />
<div className="h-3 w-24 bg-[var(--bg-tertiary)] rounded animate-pulse" />
<>
{/* Inline keyframes for immediate animation - no external CSS needed */}
<style jsx global>{`
@keyframes skeletonShimmer {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.7; }
}
.sk-pulse {
animation: skeletonShimmer 1.5s ease-in-out infinite;
will-change: opacity;
}
`}</style>
<div className="min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)]">
{/* Header Skeleton */}
<header className="pt-8 sm:pt-12 pb-8 sm:pb-10 px-4 sm:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-start gap-4">
{/* Logo placeholder */}
<div
className="w-16 h-16 sm:w-[72px] sm:h-[72px] rounded-xl bg-[var(--bg-tertiary)] sk-pulse"
/>
<div className="flex flex-col gap-2">
<div className="h-6 w-32 bg-[var(--bg-tertiary)] rounded sk-pulse" />
<div
className="h-3 w-48 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: '0.1s' }}
/>
<div
className="h-3 w-36 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: '0.2s' }}
/>
</div>
</div>
<div className="flex items-center gap-3">
<div
className="h-6 w-16 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: '0.1s' }}
/>
<div
className="h-6 w-16 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: '0.15s' }}
/>
<div className="w-px h-6 bg-[var(--border-primary)]" />
<div
className="h-6 w-12 bg-[var(--bg-tertiary)] rounded-full sk-pulse"
style={{ animationDelay: '0.2s' }}
/>
<div
className="h-10 w-28 bg-[var(--bg-tertiary)] rounded-2xl sk-pulse"
style={{ animationDelay: '0.25s' }}
/>
</div>
</div>
<div className="flex items-center gap-3">
<div className="h-8 w-20 bg-[var(--bg-tertiary)] rounded-lg animate-pulse" />
<div className="h-10 w-28 bg-[var(--bg-tertiary)] rounded-2xl animate-pulse" />
</div>
</div>
</div>
</header>
</header>
{/* Grid Skeleton */}
<main className="px-4 sm:px-6 pb-24">
<div className="max-w-6xl mx-auto">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 sm:gap-x-8">
{[...Array(5)].map((_, colIdx) => (
<div key={colIdx} className="space-y-5">
{[...Array(3)].map((_, catIdx) => (
<div key={catIdx} className="mb-5">
{/* Category header skeleton */}
<div className="flex items-center gap-2 mb-3 pb-1.5 border-b border-[var(--border-primary)]">
<div className="w-3 h-3 bg-[var(--bg-tertiary)] rounded animate-pulse" />
<div className="h-3 w-20 bg-[var(--bg-tertiary)] rounded animate-pulse" />
</div>
{/* App items skeleton */}
{[...Array(4 + catIdx)].map((_, appIdx) => (
<div
key={appIdx}
className="flex items-center gap-2.5 py-1.5 px-2"
style={{ animationDelay: `${(colIdx * 3 + catIdx) * 50 + appIdx * 20}ms` }}
>
<div className="w-4 h-4 rounded border-2 border-[var(--bg-tertiary)] animate-pulse" />
<div className="w-5 h-5 rounded bg-[var(--bg-tertiary)] animate-pulse" />
{/* Grid Skeleton */}
<main className="px-4 sm:px-6 pb-40">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-x-4 sm:gap-x-8">
{[...Array(5)].map((_, colIdx) => (
<div key={colIdx} className="space-y-5">
{[...Array(3)].map((_, catIdx) => (
<div key={catIdx} className="mb-5">
{/* Category header skeleton */}
<div className="flex items-center gap-2 mb-3 pb-1.5 border-b border-[var(--border-primary)]">
<div
className="h-4 bg-[var(--bg-tertiary)] rounded animate-pulse"
style={{ width: `${70 + (appIdx % 3) * 10}%` }}
className="w-3 h-3 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: `${colIdx * 0.08}s` }}
/>
<div
className="h-3 w-20 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: `${colIdx * 0.08 + 0.03}s` }}
/>
</div>
))}
</div>
))}
</div>
))}
{/* App items skeleton */}
{[...Array(3 + catIdx)].map((_, appIdx) => (
<div
key={appIdx}
className="flex items-center gap-2.5 py-1.5 px-2"
>
<div
className="w-4 h-4 rounded border-2 border-[var(--bg-tertiary)] sk-pulse"
style={{ animationDelay: `${colIdx * 0.08 + appIdx * 0.02}s` }}
/>
<div
className="w-5 h-5 rounded bg-[var(--bg-tertiary)] sk-pulse"
style={{ animationDelay: `${colIdx * 0.08 + appIdx * 0.02 + 0.01}s` }}
/>
<div
className="h-4 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{
width: `${60 + (appIdx % 4) * 10}%`,
animationDelay: `${colIdx * 0.08 + appIdx * 0.02 + 0.02}s`
}}
/>
</div>
))}
</div>
))}
</div>
))}
</div>
</div>
</div>
</main>
</main>
{/* Footer Skeleton */}
<div className="fixed bottom-0 left-0 right-0 h-16 bg-[var(--bg-secondary)] border-t border-[var(--border-primary)]">
<div className="max-w-6xl mx-auto h-full flex items-center justify-between px-4 sm:px-6">
<div className="h-10 flex-1 mr-4 bg-[var(--bg-tertiary)] rounded-lg animate-pulse" />
<div className="flex gap-2">
<div className="w-10 h-10 bg-[var(--bg-tertiary)] rounded-lg animate-pulse" />
<div className="w-10 h-10 bg-[var(--bg-tertiary)] rounded-lg animate-pulse" />
{/* Footer Skeleton - Matches new nvim-style footer */}
<div className="fixed bottom-0 left-0 right-0 p-3">
<div className="relative w-[85%] mx-auto flex flex-col gap-1.5">
{/* ShortcutsBar skeleton */}
<div className="bg-[var(--bg-tertiary)] border border-[var(--border-primary)] rounded-lg overflow-hidden">
<div className="flex items-center justify-between h-8">
<div className="flex items-center">
<div className="w-20 h-full bg-[var(--text-primary)]/20 sk-pulse" />
<div className="flex items-center gap-2 px-3">
<div className="w-3 h-3 bg-[var(--bg-secondary)] rounded sk-pulse" />
<div className="w-20 h-3 bg-[var(--bg-secondary)] rounded sk-pulse" />
</div>
</div>
<div className="flex items-center gap-4 px-4">
<div className="w-32 h-3 bg-[var(--bg-secondary)] rounded sk-pulse hidden sm:block" />
<div className="w-12 h-full bg-[var(--text-primary)]/20 sk-pulse" />
</div>
</div>
</div>
{/* Command Bar skeleton */}
<div className="bg-[var(--bg-tertiary)] border border-[var(--border-primary)]/40 rounded-lg overflow-hidden">
<div className="flex items-center h-11">
<div
className="w-24 h-full bg-indigo-500/10 sk-pulse flex-shrink-0"
style={{ animationDelay: '0.1s' }}
/>
<div className="flex-1 flex items-center justify-center px-4 bg-[var(--bg-secondary)]">
<div
className="w-48 h-4 bg-[var(--bg-tertiary)] rounded sk-pulse"
style={{ animationDelay: '0.15s' }}
/>
</div>
<div
className="w-20 h-full bg-[var(--bg-tertiary)] sk-pulse flex-shrink-0"
style={{ animationDelay: '0.2s' }}
/>
<div
className="w-16 h-full bg-[var(--text-primary)]/20 sk-pulse flex-shrink-0"
style={{ animationDelay: '0.25s' }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</>
);
}

View File

@@ -1,114 +1,104 @@
'use client';
/**
* Tooltip - Positioned tooltip with markdown-like formatting
*
* Features:
* - Supports inline code, bold text, and links
* - Slide-up animation
* - Arrow pointer
* - Hover persistence (tooltip stays visible when hovered)
*
* @param tooltip - Tooltip data (text, position, key)
* @param onEnter - Callback when mouse enters tooltip
* @param onLeave - Callback when mouse leaves tooltip
*
* @example
* <Tooltip
* tooltip={{ text: "Hello **world**", x: 100, y: 200, key: 1 }}
* onEnter={() => {}}
* onLeave={() => {}}
* />
*/
/**
* Renders a single line with inline formatting
*/
function renderLine(text: string) {
// Split by code, links, and bold
const parts = text.split(/(`[^`]+`|\[.*?\]\(.*?\)|\*\*.*?\*\*)/);
return parts.map((part, i) => {
// Check for inline code
const codeMatch = part.match(/^`([^`]+)`$/);
if (codeMatch) {
return (
<code key={i} className="bg-[var(--bg-primary)] px-1.5 py-0.5 rounded text-[var(--accent)] font-mono text-[10px] select-all break-all">
{codeMatch[1]}
</code>
);
}
// Check for bold
const boldMatch = part.match(/^\*\*(.*?)\*\*$/);
if (boldMatch) {
return <strong key={i} className="font-semibold text-[var(--text-primary)]">{boldMatch[1]}</strong>;
}
// Check for links
const linkMatch = part.match(/\[(.*?)\]\((.*?)\)/);
if (linkMatch) {
return (
<a key={i} href={linkMatch[2]} target="_blank" rel="noopener noreferrer"
className="text-[var(--accent)] underline hover:opacity-80">
{linkMatch[1]}
</a>
);
}
return <span key={i}>{part}</span>;
});
}
/**
* Renders tooltip content with newline support
*/
function renderTooltipContent(text: string) {
// First handle escaped newlines
const lines = text.split(/\\n/);
return lines.map((line, lineIdx) => (
<span key={lineIdx}>
{lineIdx > 0 && <br />}
{renderLine(line)}
</span>
));
}
import React from 'react';
export interface TooltipData {
text: string;
x: number;
y: number;
width?: number;
key?: number;
width: number;
key: number;
}
export function Tooltip({
tooltip,
onEnter,
onLeave
}: {
interface TooltipProps {
tooltip: TooltipData | null;
onEnter: () => void;
onLeave: () => void;
}) {
onEnter?: () => void;
onLeave?: () => void;
}
/**
* Tooltip - Global tooltip component with refined styling and animation
*
* Features:
* - Fixed positioning based on element coordinates
* - "Warm paper" aesthetic styling
* - Smooth entry animation
* - Markdown text rendering (bold, code, links)
* - Max width with wrapping
*/
export function Tooltip({ tooltip, onEnter, onLeave }: TooltipProps) {
if (!tooltip) return null;
// Center horizontally relative to the element
const left = tooltip.x;
const top = tooltip.y;
// Helper to render markdown content
const renderContent = (text: string) => {
// Split by **bold**, `code`, or [link](url)
return text.split(/(\*\*.*?\*\*|`.*?`|\[.*?\]\(.*?\))/g).map((part, i) => {
// Bold
if (part.startsWith('**') && part.endsWith('**')) {
return (
<strong key={i} className="font-bold text-[var(--accent)]">
{part.slice(2, -2)}
</strong>
);
}
// Code
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code key={i} className="bg-[var(--bg-secondary)] px-1 rounded font-mono text-[var(--accent)] text-[10px]">
{part.slice(1, -1)}
</code>
);
}
// Link
if (part.startsWith('[') && part.includes('](') && part.endsWith(')')) {
const match = part.match(/\[(.*?)\]\((.*?)\)/);
if (match) {
return (
<a
key={i}
href={match[2]}
target="_blank"
rel="noopener noreferrer"
className="text-[var(--accent)] underline decoration-[var(--accent)]/50 hover:decoration-[var(--accent)] font-semibold transition-all hover:text-emerald-500"
onClick={(e) => e.stopPropagation()} // Prevent triggering parent clicks
>
{match[1]}
</a>
);
}
}
return <span key={i}>{part}</span>;
});
};
return (
<div
key={tooltip.key}
role="tooltip"
className="fixed z-50 pointer-events-auto"
style={{
left: left,
top: top,
transform: 'translate(-50%, -100%)',
// Using the specific key ensures fresh animation on new tooltip
animation: 'tooltipSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards',
maxWidth: '400px', // Limit width
width: 'max-content'
}}
onMouseEnter={onEnter}
onMouseLeave={onLeave}
className="fixed px-3 py-2.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] text-xs rounded-lg shadow-xl border border-[var(--border-secondary)] max-w-[320px] leading-relaxed"
style={{
left: tooltip.x,
top: tooltip.y,
transform: 'translate(-50%, -100%)',
zIndex: 99999,
animation: 'tooltipSlideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) forwards',
}}>
{renderTooltipContent(tooltip.text)}
{/* Arrow pointer */}
<div
className="absolute left-1/2 -translate-x-1/2 w-3 h-3 bg-[var(--bg-tertiary)] border-r border-b border-[var(--border-secondary)] rotate-45"
style={{ bottom: '-7px' }}
/>
>
<div className="relative mb-2 px-3 py-2 rounded-lg bg-[var(--bg-tertiary)] text-[var(--text-primary)] text-xs font-medium shadow-xl border border-[var(--border-primary)]/40 backdrop-blur-sm whitespace-normal break-words leading-relaxed">
{renderContent(tooltip.text)}
{/* Arrow pointer */}
<div
className="absolute -bottom-1 left-1/2 -translate-x-1/2 w-2 h-2 bg-[var(--bg-tertiary)] border-b border-r border-[var(--border-primary)]/40 rotate-45"
/>
</div>
</div>
);
}

View File

@@ -6,234 +6,286 @@ import { HelpCircle, X } from 'lucide-react';
import { analytics } from '@/lib/analytics';
/**
* HowItWorks - Interactive help popup with quick start guide
* HowItWorks - Interactive help modal with quick start guide
*
* Displays a popup with:
* Displays a centered modal with backdrop blur, containing:
* - Quick start steps for using TuxMate
* - Info about unavailable apps
* - Arch/AUR specific info
* - Keyboard shortcuts
* - Pro tips
*
* @example
* <HowItWorks />
*/
export function HowItWorks() {
const [isOpen, setIsOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [mounted, setMounted] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const popupRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setMounted(true);
}, []);
// Close on click outside
// Lock body scroll when modal is open
useEffect(() => {
if (!isOpen) return;
const handleClickOutside = (e: MouseEvent) => {
if (
triggerRef.current && !triggerRef.current.contains(e.target as Node) &&
popupRef.current && !popupRef.current.contains(e.target as Node)
) {
setIsOpen(false);
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => {
document.body.style.overflow = '';
};
}, [isOpen]);
// Global keyboard shortcut: ? to toggle modal
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Ignore if typing in input
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
if (e.key === '?' || (e.shiftKey && e.key === '/')) {
e.preventDefault();
if (isOpen) {
handleClose();
} else {
handleOpen();
}
}
// Close on Escape
if (e.key === 'Escape' && isOpen) {
handleClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen]);
// Close on Escape
useEffect(() => {
if (!isOpen) return;
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') setIsOpen(false);
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [isOpen]);
const getPopupPosition = () => {
if (!triggerRef.current) return { top: 0, left: 0 };
const rect = triggerRef.current.getBoundingClientRect();
return {
top: rect.bottom + 12,
left: Math.max(8, Math.min(rect.left, window.innerWidth - 420)),
};
const handleOpen = () => {
setIsClosing(false);
setIsOpen(true);
analytics.helpOpened();
};
const pos = isOpen ? getPopupPosition() : { top: 0, left: 0 };
const handleClose = () => {
setIsClosing(true);
analytics.helpClosed();
// Wait for exit animation to finish
setTimeout(() => {
setIsOpen(false);
setIsClosing(false);
}, 200);
};
const popup = isOpen && mounted ? (
<div
ref={popupRef}
className="how-it-works-popup bg-[var(--bg-secondary)] backdrop-blur-xl border border-[var(--border-primary)] shadow-2xl"
style={{
position: 'fixed',
top: pos.top,
left: pos.left,
zIndex: 99999,
borderRadius: '16px',
width: '400px',
maxWidth: 'calc(100vw - 16px)',
maxHeight: 'min(70vh, 600px)',
display: 'flex',
flexDirection: 'column',
animation: 'popupSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
overflow: 'hidden',
}}
>
{/* Header - fixed */}
<div className="flex items-center justify-between gap-2 p-4 pb-3 border-b border-[var(--border-primary)] shrink-0">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-full bg-[var(--accent)]/20 flex items-center justify-center">
<HelpCircle className="w-4 h-4 text-[var(--accent)]" />
const modal = (
<>
{/* Backdrop with blur */}
<div
className="fixed inset-0 bg-black/30 backdrop-blur-sm z-[99998]"
onClick={handleClose}
style={{
animation: isClosing
? 'fadeOut 0.2s ease-out forwards'
: 'fadeIn 0.25s ease-out'
}}
/>
{/* Modal */}
<div
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]"
style={{
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '20px',
width: '440px',
maxWidth: 'calc(100vw - 32px)',
maxHeight: 'min(80vh, 650px)',
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',
}}
>
{/* Header */}
<div className="flex items-center justify-between gap-3 px-5 py-4 border-b border-[var(--border-primary)] shrink-0">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-[var(--accent)]/15 flex items-center justify-center">
<HelpCircle className="w-5 h-5 text-[var(--accent)]" />
</div>
<div>
<h3 id="how-it-works-title" className="text-lg font-semibold text-[var(--text-primary)]">How TuxMate Works</h3>
<p className="text-xs text-[var(--text-muted)]">Quick guide &amp; keyboard shortcuts</p>
</div>
</div>
<button
onClick={handleClose}
className="w-8 h-8 flex items-center justify-center rounded-lg hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Scrollable content */}
<div className="flex-1 overflow-y-auto p-5 space-y-5" style={{ scrollbarGutter: 'stable' }}>
{/* Quick Start Steps */}
<div>
<h3 className="text-base font-semibold text-[var(--text-primary)]">How TuxMate Works</h3>
<p className="text-xs text-[var(--text-muted)]">Quick guide &amp; tips</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="w-7 h-7 flex items-center justify-center rounded-full hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
>
<X className="w-4 h-4" />
</button>
</div>
{/* Scrollable content */}
<div className="flex-1 overflow-y-auto p-4 space-y-5" style={{ scrollbarGutter: 'stable' }}>
{/* Quick Start Steps */}
<div>
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Quick Start</h4>
<div className="space-y-3">
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">1</div>
<p className="text-sm text-[var(--text-secondary)]">Select your distro from the dropdown</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">2</div>
<p className="text-sm text-[var(--text-secondary)]">Check the apps you want to install</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">3</div>
<p className="text-sm text-[var(--text-secondary)]">Copy the command or download the script</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">4</div>
<p className="text-sm text-[var(--text-secondary)]">Paste in terminal (<code className="text-xs bg-[var(--bg-tertiary)] px-1 py-0.5 rounded">Ctrl+Shift+V</code>) and run</p>
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Quick Start</h4>
<div className="space-y-3">
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">1</div>
<p className="text-sm text-[var(--text-secondary)]">Select your distro from the dropdown</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">2</div>
<p className="text-sm text-[var(--text-secondary)]">Check the apps you want to install</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">3</div>
<p className="text-sm text-[var(--text-secondary)]">Copy the command or download the script</p>
</div>
<div className="flex gap-3">
<div className="w-5 h-5 rounded-full bg-[var(--accent)]/20 flex items-center justify-center text-[10px] font-bold text-[var(--accent)] shrink-0">4</div>
<p className="text-sm text-[var(--text-secondary)]">Paste in terminal (<code className="text-xs bg-[var(--bg-tertiary)] px-1 py-0.5 rounded">Ctrl+Shift+V</code>) and run</p>
</div>
</div>
</div>
</div>
{/* Unavailable Apps */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">App Not Available?</h4>
<div className="space-y-2.5 text-xs text-[var(--text-muted)] leading-relaxed">
<p>Greyed-out apps aren&apos;t in your distro&apos;s repos. Here&apos;s what you can do:</p>
<ul className="space-y-2 ml-2">
{/* Unavailable Apps */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">App Not Available?</h4>
<div className="space-y-2.5 text-xs text-[var(--text-muted)] leading-relaxed">
<p>Greyed-out apps aren&apos;t in your distro&apos;s repos. Here&apos;s what you can do:</p>
<ul className="space-y-2 ml-2">
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Use Flatpak/Snap:</strong> Switch to Flatpak or Snap in the distro selector for universal packages</span>
</li>
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Download from website:</strong> Visit the app&apos;s official site and grab the <code className="bg-[var(--bg-tertiary)] px-1 rounded">.deb</code>, <code className="bg-[var(--bg-tertiary)] px-1 rounded">.rpm</code>, or <code className="bg-[var(--bg-tertiary)] px-1 rounded">.AppImage</code></span>
</li>
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Hover the icon:</strong> Some unavailable apps show links to alternative download methods</span>
</li>
</ul>
</div>
</div>
{/* Arch & AUR */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Arch Linux &amp; AUR</h4>
<p className="text-xs text-[var(--text-muted)] leading-relaxed">
Some Arch packages are in the <strong className="text-[var(--text-secondary)]">AUR</strong> (Arch User Repository).
TuxMate uses <code className="bg-[var(--bg-tertiary)] px-1 rounded">yay</code> or <code className="bg-[var(--bg-tertiary)] px-1 rounded">paru</code> to install these.
When selecting AUR packages, a popup will ask which helper you have. You can switch between helpers anytime using <kbd className="px-1 py-0.5 bg-[var(--bg-tertiary)] rounded text-[10px]">1</kbd> (yay) or <kbd className="px-1 py-0.5 bg-[var(--bg-tertiary)] rounded text-[10px]">2</kbd> (paru).
</p>
</div>
{/* Keyboard Shortcuts */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Keyboard Shortcuts</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono"></kbd>
<span className="text-[var(--text-muted)]">Navigate</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">hjkl</kbd>
<span className="text-[var(--text-muted)]">Vim navigation</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">Space</kbd>
<span className="text-[var(--text-muted)]">Toggle selection</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">/</kbd>
<span className="text-[var(--text-muted)]">Search apps</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">y</kbd>
<span className="text-[var(--text-muted)]">Copy command</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">d</kbd>
<span className="text-[var(--text-muted)]">Download script</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">c</kbd>
<span className="text-[var(--text-muted)]">Clear selection</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">t</kbd>
<span className="text-[var(--text-muted)]">Toggle theme</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">Tab</kbd>
<span className="text-[var(--text-muted)]">Open preview</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">Esc</kbd>
<span className="text-[var(--text-muted)]">Close popups</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">?</kbd>
<span className="text-[var(--text-muted)]">This help</span>
</div>
</div>
</div>
{/* Pro Tips */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Pro Tips</h4>
<ul className="space-y-2 text-xs text-[var(--text-muted)] leading-relaxed">
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Use Flatpak/Snap:</strong> Switch to Flatpak or Snap in the distro selector for universal packages</span>
<span className="text-emerald-500">💡</span>
<span>The <strong className="text-[var(--text-secondary)]">download button</strong> gives you a full shell script with progress tracking, error handling, and a summary</span>
</li>
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Download from website:</strong> Visit the app&apos;s official site and grab the <code className="bg-[var(--bg-tertiary)] px-1 rounded">.deb</code>, <code className="bg-[var(--bg-tertiary)] px-1 rounded">.rpm</code>, or <code className="bg-[var(--bg-tertiary)] px-1 rounded">.AppImage</code></span>
<span className="text-emerald-500">💡</span>
<span>
<strong className="text-[var(--text-secondary)]">Running the script:</strong>{' '}
<code className="bg-[var(--bg-tertiary)] px-1 rounded">chmod +x tuxmate-*.sh && ./tuxmate-*.sh</code> or{' '}
<code className="bg-[var(--bg-tertiary)] px-1 rounded">bash tuxmate-*.sh</code>
</span>
</li>
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Check GitHub Releases:</strong> Many apps publish packages on their GitHub releases page</span>
<span className="text-emerald-500">💡</span>
<span>Your selections are <strong className="text-[var(--text-secondary)]">saved automatically</strong> come back anytime to modify your setup</span>
</li>
<li className="flex gap-2">
<span className="text-[var(--accent)]"></span>
<span><strong className="text-[var(--text-secondary)]">Hover the icon:</strong> Some unavailable apps show links to alternative download methods</span>
<span className="text-emerald-500">💡</span>
<span>Running <code className="bg-[var(--bg-tertiary)] px-1 rounded">.deb</code> files: <code className="bg-[var(--bg-tertiary)] px-1 rounded">sudo dpkg -i file.deb</code></span>
</li>
<li className="flex gap-2">
<span className="text-emerald-500">💡</span>
<span>Running <code className="bg-[var(--bg-tertiary)] px-1 rounded">.rpm</code> files: <code className="bg-[var(--bg-tertiary)] px-1 rounded">sudo dnf install ./file.rpm</code></span>
</li>
</ul>
</div>
</div>
{/* Arch & AUR */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Arch Linux &amp; AUR</h4>
<p className="text-xs text-[var(--text-muted)] leading-relaxed">
Some Arch packages are in the <strong className="text-[var(--text-secondary)]">AUR</strong> (Arch User Repository).
TuxMate uses <code className="bg-[var(--bg-tertiary)] px-1 rounded">yay</code> to install these.
If you don&apos;t have yay, check &quot;I have yay installed&quot; to skip auto-installation, or leave it unchecked to install yay first.
</p>
</div>
{/* Keyboard Shortcuts */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Keyboard Shortcuts</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono"></kbd>
<span className="text-[var(--text-muted)]">Navigate apps</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">Space</kbd>
<span className="text-[var(--text-muted)]">Toggle selection</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">Enter</kbd>
<span className="text-[var(--text-muted)]">Expand/collapse</span>
</div>
<div className="flex items-center gap-2">
<kbd className="px-1.5 py-0.5 bg-[var(--bg-tertiary)] text-[var(--text-secondary)] rounded text-[10px] font-mono">Esc</kbd>
<span className="text-[var(--text-muted)]">Close popups</span>
</div>
</div>
</div>
{/* Pro Tips */}
<div className="pt-3 border-t border-[var(--border-primary)]">
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-3">Pro Tips</h4>
<ul className="space-y-2 text-xs text-[var(--text-muted)] leading-relaxed">
<li className="flex gap-2">
<span className="text-emerald-500">💡</span>
<span>The <strong className="text-[var(--text-secondary)]">download button</strong> gives you a full shell script with progress tracking, error handling, and a summary</span>
</li>
<li className="flex gap-2">
<span className="text-emerald-500">💡</span>
<span>Your selections are <strong className="text-[var(--text-secondary)]">saved automatically</strong> come back anytime to modify your setup</span>
</li>
<li className="flex gap-2">
<span className="text-emerald-500">💡</span>
<span>Running <code className="bg-[var(--bg-tertiary)] px-1 rounded">.deb</code> files: <code className="bg-[var(--bg-tertiary)] px-1 rounded">sudo dpkg -i file.deb</code> or double-click in your file manager</span>
</li>
<li className="flex gap-2">
<span className="text-emerald-500">💡</span>
<span>Running <code className="bg-[var(--bg-tertiary)] px-1 rounded">.rpm</code> files: <code className="bg-[var(--bg-tertiary)] px-1 rounded">sudo dnf install ./file.rpm</code> or <code className="bg-[var(--bg-tertiary)] px-1 rounded">sudo zypper install ./file.rpm</code></span>
</li>
</ul>
</div>
</div>
{/* Arrow pointer */}
<div
className="absolute w-3 h-3 bg-[var(--bg-secondary)] border-l border-t border-[var(--border-primary)] rotate-45"
style={{ top: '-7px', left: '24px' }}
/>
</div>
) : null;
</>
);
return (
<>
<button
ref={triggerRef}
onClick={() => {
const wasOpen = isOpen;
setIsOpen(!isOpen);
if (!wasOpen) analytics.helpOpened();
else analytics.helpClosed();
}}
onClick={handleOpen}
className={`flex items-center gap-1.5 text-sm transition-all duration-200 hover:scale-105 ${isOpen ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'}`}
>
<HelpCircle className="w-4 h-4" />
<span className="hidden sm:inline whitespace-nowrap">How it works?</span>
</button>
{mounted && typeof document !== 'undefined' && createPortal(popup, document.body)}
{isOpen && mounted && typeof document !== 'undefined' && createPortal(modal, document.body)}
</>
);
}