mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-28 00:35:26 +02:00
feat: refined UI components and updated app data
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 & 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 & 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't in your distro's repos. Here'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't in your distro's repos. Here'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'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 & 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'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 & 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't have yay, check "I have yay installed" 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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user