mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-17 19:53:11 +02:00
Refine UI: Aesthetic overhaul, mobile improvements
This commit is contained in:
164
docs/accessibility-guidelines.md
Normal file
164
docs/accessibility-guidelines.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# AccessGuide.io - Accessibility Guidelines Reference
|
||||
|
||||
*Compiled from [Access Guide](https://www.accessguide.io/) - a friendly introduction to digital accessibility based on WCAG 2.1*
|
||||
|
||||
---
|
||||
|
||||
## 1. Hover and Focus Best Practices
|
||||
|
||||
> WCAG criterion [1.4.13 Content on Hover or Focus](https://www.w3.org/WAI/WCAG21/Understanding/content-on-hover-or-focus.html) (Level AA)
|
||||
|
||||
### Why This is Important
|
||||
|
||||
If content appears and disappears on hover or focus, this can feel frustrating, unpredictable, and disruptive. Use best practices to make hover and focus more predictable and less likely to cause errors.
|
||||
|
||||
This is especially accessible for:
|
||||
- People with **physical disabilities** (unpredictable or specific movement)
|
||||
- People with **visual disabilities** (screen reader users)
|
||||
- People with **cognitive disabilities**
|
||||
|
||||
### Implementation Guidelines
|
||||
|
||||
#### Dismissible
|
||||
There should be a way to dismiss new content **without moving hover or changing focus**. This prevents disrupting users in the middle of tasks.
|
||||
|
||||
**Best practice:** Use the "Escape" key to dismiss content.
|
||||
|
||||
#### Hoverable
|
||||
New content should **remain visible if the user hovers over it**.
|
||||
|
||||
Sometimes content appears when hovering the trigger element but disappears when hovering over the new content. This is frustrating. The content must remain visible whether hover is on the trigger OR the content itself.
|
||||
|
||||
#### Persistent
|
||||
Content must remain visible until:
|
||||
- The user dismisses it
|
||||
- The user moves the mouse off of it or the trigger
|
||||
- The content no longer contains important information
|
||||
|
||||
---
|
||||
|
||||
## 2. Keyboard Accessibility
|
||||
|
||||
> WCAG criterion [2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html) (Level A)
|
||||
|
||||
### Why This is Important
|
||||
|
||||
Full keyboard functionality is essential for:
|
||||
- People who rely on keyboards
|
||||
- Blind/visually impaired people using screen readers
|
||||
- People with motor disabilities who can't use a mouse
|
||||
|
||||
**All functionality available to mouse users should be available to keyboard users.**
|
||||
|
||||
### Basic Keyboard Access
|
||||
|
||||
Keyboard accessibility is enabled by default in browsers. **Don't remove or deactivate these defaults** - enhance them instead.
|
||||
|
||||
Keyboard accessibility includes:
|
||||
- ✅ Visible focus indicator
|
||||
- ✅ Intuitive focus order
|
||||
- ✅ Skip links to bypass repeating content
|
||||
- ✅ Way to turn off character key shortcuts
|
||||
- ✅ No keyboard traps
|
||||
|
||||
**Common keyboard-accessible interactions:**
|
||||
- Browsing navigation
|
||||
- Filling out forms
|
||||
- Accessing buttons and links
|
||||
|
||||
### Advanced Keyboard Access
|
||||
|
||||
For complex interactions (drawing programs, drag-and-drop):
|
||||
- Translate gestures into keyboard commands
|
||||
- Use **Tab, Enter, Space, and Arrow keys** (most common)
|
||||
|
||||
**Examples from Salesforce:**
|
||||
- Interact with a canvas (move/resize objects)
|
||||
- Move between lists
|
||||
- Sort lists
|
||||
|
||||
---
|
||||
|
||||
## 3. Focus Indicator Visibility
|
||||
|
||||
> WCAG criterion [2.4.7 Focus Visible](https://www.w3.org/WAI/WCAG21/Understanding/focus-visible.html) (Level AA)
|
||||
|
||||
### Why This is Important
|
||||
|
||||
A visible focus indicator shows keyboard users what element they're currently interacting with.
|
||||
|
||||
Benefits:
|
||||
- Shows what element is ready for user input
|
||||
- Helps people with executive dysfunction
|
||||
- Reduces cognitive load by focusing attention
|
||||
|
||||
### Implementation Guidelines
|
||||
|
||||
#### Never Remove Default Focus Indicator
|
||||
```css
|
||||
/* ❌ NEVER DO THIS unless replacing with custom styling */
|
||||
outline: none;
|
||||
```
|
||||
|
||||
#### Design Accessible Hover and Focus States
|
||||
|
||||
**All interactive elements need hover AND focus states:**
|
||||
- Buttons, links
|
||||
- Text fields
|
||||
- Navigation elements
|
||||
- Radio buttons, checkboxes
|
||||
|
||||
**Difference between hover and focus:**
|
||||
| State | Purpose |
|
||||
|-------|---------|
|
||||
| Hover | "You could interact with this" |
|
||||
| Focus | "You ARE interacting with this right now" |
|
||||
|
||||
**Other important states:**
|
||||
- Error state
|
||||
- Loading state
|
||||
- Inactive/disabled state
|
||||
|
||||
#### Focus Indicator Contrast
|
||||
|
||||
The focus indicator must be visible against **both**:
|
||||
- The element itself
|
||||
- The background behind it
|
||||
|
||||
**WCAG requirement:** 3:1 contrast ratio for UI components
|
||||
|
||||
---
|
||||
|
||||
## 4. Key Principles Summary
|
||||
|
||||
### For TuxMate Application
|
||||
|
||||
| Principle | Application |
|
||||
|-----------|-------------|
|
||||
| **Dismissible** | Tooltips close with Escape key |
|
||||
| **Hoverable** | Tooltips stay visible when hovering content |
|
||||
| **Persistent** | Content stays until user dismisses |
|
||||
| **Keyboard** | All features work with Tab/Arrow/Enter/Space |
|
||||
| **Focus Visible** | Clear visual indicator on focused elements |
|
||||
| **Contrast** | 3:1 minimum for UI components |
|
||||
|
||||
### Recommended Key Bindings
|
||||
|
||||
| Key | Common Action |
|
||||
|-----|---------------|
|
||||
| `Tab` | Move to next focusable element |
|
||||
| `Shift+Tab` | Move to previous element |
|
||||
| `Enter/Space` | Activate/select |
|
||||
| `Arrow keys` | Navigate within components |
|
||||
| `Escape` | Dismiss/close |
|
||||
| `?` | Show help |
|
||||
|
||||
---
|
||||
|
||||
## Further Reading
|
||||
|
||||
- [WCAG 2.1 Guidelines](https://www.w3.org/WAI/WCAG21/quickref/)
|
||||
- [MDN Web Docs - Keyboard](https://developer.mozilla.org/en-US/docs/Web/Accessibility/Understanding_WCAG/Keyboard)
|
||||
- [18F - Keyboard Access](https://accessibility.18f.gov/keyboard/)
|
||||
- [Style hover, focus, and active states differently](https://zellwk.com/blog/style-hover-focus-active-states/)
|
||||
- [Accessible Custom Focus Indicators](https://uxdesign.cc/accessible-custom-focus-indicators-da4768d1fb7b)
|
||||
@@ -3,17 +3,17 @@
|
||||
/* ===== WARM PAPER AESTHETIC THEMES ===== */
|
||||
|
||||
:root {
|
||||
/* Dark theme - warm charcoal with paper undertones */
|
||||
--bg-primary: #1e1d1a;
|
||||
--bg-secondary: #262520;
|
||||
--bg-tertiary: #302f29;
|
||||
--bg-hover: #3a3832;
|
||||
--bg-focus: #454339;
|
||||
/* Dark theme - lighter warm charcoal per user request */
|
||||
--bg-primary: #262522;
|
||||
--bg-secondary: #2f2e2a;
|
||||
--bg-tertiary: #3a3934;
|
||||
--bg-hover: #45433d;
|
||||
--bg-focus: #504e46;
|
||||
--text-primary: #f5f3ef;
|
||||
--text-secondary: #d8d4cc;
|
||||
--text-muted: #a09a8e;
|
||||
--border-primary: #3a3832;
|
||||
--border-secondary: #454339;
|
||||
--border-primary: #45433d;
|
||||
--border-secondary: #504e46;
|
||||
--accent: #9a958a;
|
||||
}
|
||||
|
||||
@@ -35,20 +35,31 @@
|
||||
@theme inline {
|
||||
--color-background: var(--bg-primary);
|
||||
--color-foreground: var(--text-primary);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
--font-sans: var(--font-sans);
|
||||
--font-heading: var(--font-heading);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-family: var(--font-sans), system-ui, -apple-system, sans-serif;
|
||||
font-weight: 450;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
.category-header {
|
||||
font-family: var(--font-heading), system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
@@ -83,6 +94,7 @@ body::before {
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
::selection {
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, JetBrains_Mono, Open_Sans } from "next/font/google";
|
||||
import { Outfit, Inter, JetBrains_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { ThemeProvider } from "@/hooks/useTheme";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
const outfit = Outfit({
|
||||
variable: "--font-heading",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
const inter = Inter({
|
||||
variable: "--font-sans",
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const jetbrainsMono = JetBrains_Mono({
|
||||
variable: "--font-jetbrains-mono",
|
||||
variable: "--font-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const openSans = Open_Sans({
|
||||
variable: "--font-open-sans",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "600", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -100,7 +97,7 @@ export default function RootLayout({
|
||||
)}
|
||||
</head>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${jetbrainsMono.variable} ${openSans.variable} antialiased`}
|
||||
className={`${outfit.variable} ${inter.variable} ${jetbrainsMono.variable} antialiased`}
|
||||
>
|
||||
<ThemeProvider>
|
||||
{children}
|
||||
|
||||
@@ -19,10 +19,7 @@ import { CategorySection } from '@/components/app';
|
||||
import { CommandFooter } from '@/components/command';
|
||||
import { Tooltip, GlobalStyles, LoadingSkeleton } from '@/components/common';
|
||||
|
||||
// The main event
|
||||
|
||||
export default function Home() {
|
||||
// All the state we need to make this thing work
|
||||
|
||||
const { tooltip, show: showTooltip, hide: hideTooltip, tooltipMouseEnter, tooltipMouseLeave, setTooltipRef } = useTooltip();
|
||||
|
||||
@@ -122,7 +119,7 @@ export default function Home() {
|
||||
}, []);
|
||||
|
||||
|
||||
// Build nav items for keyboard navigation (vim keys ftw)
|
||||
// Build nav items for keyboard navigation
|
||||
const navItems = useMemo(() => {
|
||||
const items: NavItem[][] = [];
|
||||
columns.forEach((colCategories) => {
|
||||
|
||||
@@ -9,6 +9,25 @@ import { AppIcon } from './AppIcon';
|
||||
|
||||
// Each app row in the list. Memoized because there are a LOT of these.
|
||||
|
||||
// Basic Tailwind-ish color palette for mapping
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
'orange': '#f97316',
|
||||
'blue': '#3b82f6',
|
||||
'emerald': '#10b981',
|
||||
'sky': '#0ea5e9',
|
||||
'yellow': '#eab308',
|
||||
'slate': '#64748b',
|
||||
'zinc': '#71717a',
|
||||
'rose': '#f43f5e',
|
||||
'purple': '#a855f7',
|
||||
'red': '#ef4444',
|
||||
'indigo': '#6366f1',
|
||||
'cyan': '#06b6d4',
|
||||
'green': '#22c55e',
|
||||
'teal': '#14b8a6',
|
||||
'gray': '#6b7280',
|
||||
};
|
||||
|
||||
interface AppItemProps {
|
||||
app: AppData;
|
||||
isSelected: boolean;
|
||||
@@ -19,6 +38,7 @@ interface AppItemProps {
|
||||
onTooltipEnter: (t: string, e: React.MouseEvent) => void;
|
||||
onTooltipLeave: () => void;
|
||||
onFocus?: () => void;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const AppItem = memo(function AppItem({
|
||||
@@ -31,6 +51,7 @@ export const AppItem = memo(function AppItem({
|
||||
onTooltipEnter,
|
||||
onTooltipLeave,
|
||||
onFocus,
|
||||
color = 'gray',
|
||||
}: AppItemProps) {
|
||||
// Why isn't this app available? Tell the user.
|
||||
const getUnavailableText = () => {
|
||||
@@ -41,6 +62,11 @@ export const AppItem = memo(function AppItem({
|
||||
// Special styling for AUR packages (Arch users love their badges)
|
||||
const isAur = selectedDistro === 'arch' && app.targets?.arch && isAurPackage(app.targets.arch);
|
||||
|
||||
// Determine effective color (AUR overrides category color for the checkbox/badge, but maybe not the row border)
|
||||
// Actually, let's keep the row border as the category color for consistency
|
||||
const hexColor = COLOR_MAP[color] || COLOR_MAP['gray'];
|
||||
const checkboxColor = isAur ? '#1793d1' : hexColor;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-nav-id={`app:${app.id}`}
|
||||
@@ -48,33 +74,35 @@ export const AppItem = memo(function AppItem({
|
||||
aria-checked={isSelected}
|
||||
aria-label={`${app.name}${!isAvailable ? ' (unavailable)' : ''}`}
|
||||
aria-disabled={!isAvailable}
|
||||
className={`app-item w-full flex items-center gap-2.5 py-1.5 px-2 rounded-md outline-none transition-colors duration-150
|
||||
${isFocused ? 'bg-[var(--bg-focus)]' : ''}
|
||||
${!isAvailable ? 'opacity-40 grayscale-[30%]' : 'hover:bg-[var(--bg-hover)] cursor-pointer'}`}
|
||||
style={{ transition: 'background-color 0.15s, color 0.5s' }}
|
||||
className={`app-item group w-full flex items-center gap-2.5 py-1.5 px-2 outline-none transition-all duration-150
|
||||
${isFocused ? 'bg-[var(--bg-secondary)] border-l-2 shadow-sm' : 'border-l-2 border-transparent'}
|
||||
${!isAvailable
|
||||
? 'opacity-40 grayscale-[30%]'
|
||||
: 'hover:bg-[color-mix(in_srgb,var(--item-color),transparent_90%)] cursor-pointer'
|
||||
}`}
|
||||
style={{
|
||||
transition: 'background-color 0.15s, color 0.5s',
|
||||
borderColor: isFocused ? hexColor : 'transparent',
|
||||
backgroundColor: isFocused ? `color-mix(in srgb, ${hexColor}, transparent 85%)` : undefined, // Stronger tint on focus (15% opacity)
|
||||
'--item-color': hexColor,
|
||||
} as React.CSSProperties}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onFocus?.();
|
||||
if (isAvailable) {
|
||||
const willBeSelected = !isSelected;
|
||||
onToggle();
|
||||
// Temporarily disabled to save Umami event quota
|
||||
// const distroName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro;
|
||||
// if (willBeSelected) {
|
||||
// analytics.appSelected(app.name, app.category, distroName);
|
||||
// } else {
|
||||
// analytics.appDeselected(app.name, app.category, distroName);
|
||||
// }
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors duration-150
|
||||
${isAur
|
||||
? (isSelected ? 'bg-[#1793d1] border-[#1793d1]' : 'border-[#1793d1]/50')
|
||||
: (isSelected ? 'bg-[var(--text-secondary)] border-[var(--text-secondary)]' : 'border-[var(--border-secondary)]')
|
||||
}
|
||||
${!isAvailable ? 'border-dashed' : ''}`}>
|
||||
{isSelected && <Check className="w-2.5 h-2.5 text-[var(--bg-primary)]" strokeWidth={3} />}
|
||||
<div
|
||||
className={`w-4 h-4 rounded border-2 flex items-center justify-center flex-shrink-0 transition-colors duration-150 ${!isAvailable ? 'border-dashed' : ''}`}
|
||||
style={{
|
||||
borderColor: isSelected || isAur ? checkboxColor : 'var(--border-secondary)',
|
||||
backgroundColor: isSelected ? checkboxColor : 'transparent',
|
||||
}}
|
||||
>
|
||||
{isSelected && <Check className="w-2.5 h-2.5 text-white" strokeWidth={3} />}
|
||||
</div>
|
||||
<AppIcon url={app.iconUrl} name={app.name} />
|
||||
<div className="flex-1 flex items-baseline gap-1.5 min-w-0 overflow-hidden">
|
||||
@@ -116,7 +144,8 @@ export const AppItem = memo(function AppItem({
|
||||
onMouseLeave={(e) => { e.stopPropagation(); onTooltipLeave(); }}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-[var(--text-muted)] hover:text-[var(--accent)] transition-[color,transform] duration-300 hover:rotate-[360deg] hover:scale-110"
|
||||
className="w-4 h-4 text-[var(--text-muted)] transition-[color,transform] duration-300 hover:rotate-[360deg] hover:scale-110"
|
||||
style={{ color: isFocused ? hexColor : undefined }} // Use category color on hover/focus
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -24,6 +24,25 @@ const CATEGORY_ICONS: Record<string, LucideIcon> = {
|
||||
'System': Cpu,
|
||||
};
|
||||
|
||||
// Basic Tailwind-ish color palette for mapping
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
'orange': '#f97316',
|
||||
'blue': '#3b82f6',
|
||||
'emerald': '#10b981',
|
||||
'sky': '#0ea5e9',
|
||||
'yellow': '#eab308',
|
||||
'slate': '#64748b',
|
||||
'zinc': '#71717a',
|
||||
'rose': '#f43f5e',
|
||||
'purple': '#a855f7',
|
||||
'red': '#ef4444',
|
||||
'indigo': '#6366f1',
|
||||
'cyan': '#06b6d4',
|
||||
'green': '#22c55e',
|
||||
'teal': '#14b8a6',
|
||||
'gray': '#6b7280',
|
||||
};
|
||||
|
||||
// Clickable category header with chevron and selection count
|
||||
export function CategoryHeader({
|
||||
category,
|
||||
@@ -32,6 +51,7 @@ export function CategoryHeader({
|
||||
onToggle,
|
||||
selectedCount,
|
||||
onFocus,
|
||||
color = 'gray',
|
||||
}: {
|
||||
category: string;
|
||||
isExpanded: boolean;
|
||||
@@ -39,7 +59,10 @@ export function CategoryHeader({
|
||||
onToggle: () => void;
|
||||
selectedCount: number;
|
||||
onFocus?: () => void;
|
||||
color?: string;
|
||||
}) {
|
||||
const hexColor = COLOR_MAP[color] || COLOR_MAP['gray'];
|
||||
|
||||
return (
|
||||
<button
|
||||
data-nav-id={`category:${category}`}
|
||||
@@ -47,25 +70,40 @@ export function CategoryHeader({
|
||||
tabIndex={-1}
|
||||
aria-expanded={isExpanded}
|
||||
aria-label={`${category} category, ${selectedCount} apps selected`}
|
||||
// Soft Pill design: rounded tags with accent color
|
||||
className={`category-header group w-full h-7 flex items-center gap-2 text-xs font-bold text-[var(--accent)]
|
||||
bg-[var(--accent)]/10 hover:bg-[var(--accent)]/20
|
||||
uppercase tracking-wider px-3 rounded-lg mb-3
|
||||
// AccessGuide-style: subtle bg with colored left border accent
|
||||
className={`category-header group w-full h-8 flex items-center gap-2 text-sm font-semibold
|
||||
border-l-4
|
||||
px-3 mb-3
|
||||
transition-all duration-200 outline-none
|
||||
${isFocused ? 'bg-[var(--accent)]/25 shadow-sm' : ''}`}
|
||||
hover:bg-[color-mix(in_srgb,var(--header-color),transparent_80%)]`}
|
||||
style={{
|
||||
color: 'var(--text-primary)',
|
||||
borderColor: hexColor,
|
||||
backgroundColor: isFocused
|
||||
? `color-mix(in srgb, ${hexColor}, transparent 75%)` // 25% opacity for focus
|
||||
: `color-mix(in srgb, ${hexColor}, transparent 90%)`, // 10% opacity for default
|
||||
'--header-color': hexColor,
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
<ChevronRight className={`w-4 h-4 text-[var(--accent)]/70 group-hover:text-[var(--accent)] transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`} />
|
||||
<ChevronRight
|
||||
className={`w-4 h-4 transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
||||
style={{ color: hexColor }}
|
||||
/>
|
||||
{(() => {
|
||||
const Icon = CATEGORY_ICONS[category] || Terminal;
|
||||
return <Icon className="w-4 h-4 text-[var(--accent)] opacity-80 group-hover:opacity-100 transition-opacity duration-200" />;
|
||||
return <Icon className="w-4 h-4" style={{ color: hexColor }} />;
|
||||
})()}
|
||||
<span className="flex-1 text-left">{category}</span>
|
||||
{selectedCount > 0 && (
|
||||
<span
|
||||
className="text-xs text-[var(--accent)] font-bold ml-1.5 opacity-100"
|
||||
style={{ transition: 'color 0.5s' }}
|
||||
className="text-xs font-bold ml-1.5 px-1.5 py-0.5 rounded"
|
||||
style={{
|
||||
transition: 'color 0.5s',
|
||||
color: hexColor,
|
||||
backgroundColor: `color-mix(in srgb, ${hexColor}, transparent 85%)`
|
||||
}}
|
||||
>
|
||||
[{selectedCount}]
|
||||
{selectedCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -26,6 +26,25 @@ interface CategorySectionProps {
|
||||
onAppFocus?: (appId: string) => void;
|
||||
}
|
||||
|
||||
// Category color mapping
|
||||
const categoryColors: Record<Category, string> = {
|
||||
'Web Browsers': 'orange',
|
||||
'Communication': 'blue',
|
||||
'Media': 'yellow',
|
||||
'Gaming': 'purple',
|
||||
'Office': 'indigo',
|
||||
'Creative': 'cyan',
|
||||
'System': 'red',
|
||||
'File Sharing': 'teal',
|
||||
'Security': 'green',
|
||||
'VPN & Network': 'emerald',
|
||||
'Dev: Editors': 'sky',
|
||||
'Dev: Languages': 'rose',
|
||||
'Dev: Tools': 'slate',
|
||||
'Terminal': 'zinc',
|
||||
'CLI Tools': 'gray'
|
||||
};
|
||||
|
||||
function CategorySectionComponent({
|
||||
category,
|
||||
categoryApps,
|
||||
@@ -49,7 +68,10 @@ function CategorySectionComponent({
|
||||
const hasAnimated = useRef(false);
|
||||
const prevAppCount = useRef(categoryApps.length);
|
||||
|
||||
// Initial entrance animation - GPU optimized
|
||||
// Get color for this category
|
||||
const color = categoryColors[category] || 'gray';
|
||||
|
||||
// Initial entrance animation
|
||||
useLayoutEffect(() => {
|
||||
if (!sectionRef.current || hasAnimated.current) return;
|
||||
hasAnimated.current = true;
|
||||
@@ -114,6 +136,7 @@ function CategorySectionComponent({
|
||||
}}
|
||||
selectedCount={selectedInCategory}
|
||||
onFocus={onCategoryFocus}
|
||||
color={color}
|
||||
/>
|
||||
<div
|
||||
className={`overflow-hidden transition-[max-height,opacity] duration-500 ${isExpanded ? 'max-h-[1000px] opacity-100' : 'max-h-0 opacity-0'}`}
|
||||
@@ -131,6 +154,7 @@ function CategorySectionComponent({
|
||||
onTooltipEnter={onTooltipEnter}
|
||||
onTooltipLeave={onTooltipLeave}
|
||||
onFocus={() => onAppFocus?.(app.id)}
|
||||
color={color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -8,87 +8,59 @@ interface AurDrawerSettingsProps {
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
}
|
||||
|
||||
// AUR settings inside the command drawer
|
||||
// AUR settings configuration panel
|
||||
export function AurDrawerSettings({
|
||||
aurAppNames,
|
||||
hasYayInstalled,
|
||||
setHasYayInstalled,
|
||||
selectedHelper,
|
||||
setSelectedHelper,
|
||||
}: AurDrawerSettingsProps) {
|
||||
distroColor,
|
||||
}: AurDrawerSettingsProps & { distroColor: string }) {
|
||||
return (
|
||||
<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 className="mb-4 rounded-lg border border-[var(--border-primary)] bg-[var(--bg-secondary)] overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-[var(--border-primary)]/50 flex">
|
||||
<span className="text-xs font-medium text-[var(--text-secondary)] whitespace-nowrap mr-2">AUR Packages:</span>
|
||||
<span className="text-xs text-[var(--text-muted)] truncate">{aurAppNames.join(', ')}</span>
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<div className="p-4 grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[var(--text-secondary)] font-medium">AUR Helper</span>
|
||||
<div className="flex w-full bg-[var(--bg-primary)] rounded-md border border-[var(--border-primary)] p-1 h-10">
|
||||
<button
|
||||
onClick={() => setSelectedHelper('yay')}
|
||||
className={`relative px-3 py-1.5 rounded-md font-medium transition-[background-color,color,transform] 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)',
|
||||
}}
|
||||
className={`flex-1 rounded-sm font-medium transition-all ${selectedHelper === 'yay' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
style={{ backgroundColor: selectedHelper === 'yay' ? distroColor : 'transparent', color: selectedHelper === 'yay' ? '#000' : undefined }}
|
||||
>
|
||||
yay <span className="opacity-60 font-normal">(Go)</span>
|
||||
yay
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedHelper('paru')}
|
||||
className={`relative px-3 py-1.5 rounded-md font-medium transition-[background-color,color,transform] 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)',
|
||||
}}
|
||||
className={`flex-1 rounded-sm font-medium transition-all ${selectedHelper === 'paru' ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
style={{ backgroundColor: selectedHelper === 'paru' ? distroColor : 'transparent', color: selectedHelper === 'paru' ? '#000' : undefined }}
|
||||
>
|
||||
paru <span className="opacity-60 font-normal">(Rust)</span>
|
||||
paru
|
||||
</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">
|
||||
<div className="flex flex-col gap-2">
|
||||
<span className="text-[var(--text-secondary)] font-medium">Install helper?</span>
|
||||
<div className="flex w-full bg-[var(--bg-primary)] rounded-md border border-[var(--border-primary)] p-1 h-10">
|
||||
<button
|
||||
onClick={() => setHasYayInstalled(true)}
|
||||
className={`relative px-3 py-1.5 rounded-md font-medium transition-[background-color,color,transform] 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)',
|
||||
}}
|
||||
className={`flex-1 rounded-sm font-medium transition-all ${hasYayInstalled ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
style={{ backgroundColor: hasYayInstalled ? distroColor : 'transparent', color: hasYayInstalled ? '#000' : undefined }}
|
||||
>
|
||||
Yes, use it
|
||||
No
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setHasYayInstalled(false)}
|
||||
className={`relative px-3 py-1.5 rounded-md font-medium transition-[background-color,color,transform] 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)',
|
||||
}}
|
||||
className={`flex-1 rounded-sm font-medium transition-all ${!hasYayInstalled ? 'bg-[var(--bg-secondary)] text-[var(--text-primary)] shadow-sm' : 'text-[var(--text-muted)] hover:text-[var(--text-primary)]'}`}
|
||||
style={{ backgroundColor: !hasYayInstalled ? distroColor : 'transparent', color: !hasYayInstalled ? '#000' : undefined }}
|
||||
>
|
||||
No, install it
|
||||
Yes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -98,15 +98,15 @@ export function AurFloatingCard({
|
||||
return (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 md:left-auto md:right-4 md:translate-x-0 z-30 flex flex-col gap-3 items-center md: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: 'slideOutToRight 0.25s ease-out forwards' }}
|
||||
className="w-72 bg-[var(--bg-secondary)] backdrop-blur-xl border-l-4 shadow-lg overflow-hidden"
|
||||
style={{ borderLeftColor: 'var(--accent)', 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: 'slideOutToRight 0.2s ease-out forwards' }}
|
||||
className="w-72 bg-[var(--bg-secondary)] backdrop-blur-xl border-l-4 shadow-lg overflow-hidden"
|
||||
style={{ borderLeftColor: 'var(--accent)', animation: 'slideOutToRight 0.2s ease-out forwards' }}
|
||||
>
|
||||
<div className="p-4" />
|
||||
</div>
|
||||
@@ -117,16 +117,16 @@ export function AurFloatingCard({
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 left-1/2 -translate-x-1/2 md:left-auto md:right-4 md:translate-x-0 z-30 flex flex-col gap-3 items-center md:items-end">
|
||||
{/* Card 1: Do you have an AUR helper? */}
|
||||
{/* Card 1: Do you have an AUR helper? - AccessGuide style */}
|
||||
<div
|
||||
className={`
|
||||
w-72 bg-[var(--bg-secondary)] backdrop-blur-xl
|
||||
border border-[var(--border-primary)]/60
|
||||
rounded-2xl shadow-xl shadow-black/10
|
||||
border-l-4 shadow-lg
|
||||
overflow-hidden
|
||||
transition-[box-shadow,border-color] duration-200
|
||||
transition-[box-shadow] duration-200
|
||||
`}
|
||||
style={{
|
||||
borderLeftColor: 'var(--accent)',
|
||||
animation: isExiting
|
||||
? 'slideOutToRight 0.2s ease-out forwards'
|
||||
: 'slideInFromRight 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
|
||||
@@ -156,10 +156,10 @@ export function AurFloatingCard({
|
||||
<button
|
||||
onClick={() => handleFirstAnswer(true)}
|
||||
className={`
|
||||
flex-1 py-2 px-4 rounded-xl text-sm font-medium
|
||||
flex-1 py-2 px-4 text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${hasAnswered === true
|
||||
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
@@ -169,10 +169,10 @@ export function AurFloatingCard({
|
||||
<button
|
||||
onClick={() => handleFirstAnswer(false)}
|
||||
className={`
|
||||
flex-1 py-2 px-4 rounded-xl text-sm font-medium
|
||||
flex-1 py-2 px-4 text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${hasAnswered === false
|
||||
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
@@ -189,16 +189,16 @@ export function AurFloatingCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Which helper? (appears after first answer) */}
|
||||
{/* Card 2: Which helper? (appears after first answer) - AccessGuide style */}
|
||||
{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
|
||||
border-l-4 shadow-lg
|
||||
overflow-hidden
|
||||
`}
|
||||
style={{
|
||||
borderLeftColor: 'var(--accent)',
|
||||
animation: isExiting
|
||||
? 'slideOutToRight 0.15s ease-out forwards'
|
||||
: 'slideInFromRightSecond 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) 0.05s forwards',
|
||||
@@ -227,10 +227,10 @@ export function AurFloatingCard({
|
||||
<button
|
||||
onClick={() => handleHelperSelect('yay')}
|
||||
className={`
|
||||
flex-1 py-2.5 px-4 rounded-xl text-sm font-medium
|
||||
flex-1 py-2.5 px-4 text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${selectedHelper === 'yay' && helperChosen
|
||||
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
@@ -243,10 +243,10 @@ export function AurFloatingCard({
|
||||
<button
|
||||
onClick={() => handleHelperSelect('paru')}
|
||||
className={`
|
||||
flex-1 py-2.5 px-4 rounded-xl text-sm font-medium
|
||||
flex-1 py-2.5 px-4 text-sm font-medium
|
||||
transition-[background-color,color] duration-200 ease-out
|
||||
${selectedHelper === 'paru' && helperChosen
|
||||
? 'bg-[var(--text-primary)] text-[var(--bg-primary)] shadow-sm'
|
||||
? 'bg-[var(--accent)]/20 text-[var(--text-primary)] border-l-2 border-l-[var(--accent)]'
|
||||
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-[var(--text-primary)]'
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -20,9 +20,10 @@ interface CommandDrawerProps {
|
||||
setHasYayInstalled: (value: boolean) => void;
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
distroColor: string;
|
||||
}
|
||||
|
||||
// Slide-up drawer for command preview (mobile: bottom sheet, desktop: centered modal)
|
||||
// Bottom sheet for mobile, centered modal for desktop
|
||||
export function CommandDrawer({
|
||||
isOpen,
|
||||
isClosing,
|
||||
@@ -38,6 +39,7 @@ export function CommandDrawer({
|
||||
setHasYayInstalled,
|
||||
selectedHelper,
|
||||
setSelectedHelper,
|
||||
distroColor,
|
||||
}: CommandDrawerProps) {
|
||||
// Swipe-to-dismiss state
|
||||
const [dragOffset, setDragOffset] = useState(0);
|
||||
@@ -87,16 +89,18 @@ export function CommandDrawer({
|
||||
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]"
|
||||
className="fixed z-50 bg-[var(--bg-secondary)] rounded-t-xl md:rounded-lg shadow-2xl
|
||||
bottom-0 left-0 right-0
|
||||
md:bottom-auto md:top-1/2 md:left-1/2 md:-translate-x-1/2 md:-translate-y-1/2 md:max-w-2xl md:w-[90vw]"
|
||||
style={{
|
||||
boxShadow: '0 0 0 1px var(--border-primary), 0 20px 60px -10px rgba(0, 0, 0, 0.5)',
|
||||
animation: isClosing
|
||||
? 'slideDown 0.3s cubic-bezier(0.32, 0, 0.67, 0) forwards'
|
||||
: dragOffset > 0 ? 'none' : 'slideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
maxHeight: '80vh',
|
||||
transform: dragOffset > 0 ? `translateY(${dragOffset}px)` : undefined,
|
||||
transition: isDragging ? 'none' : 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1)'
|
||||
transition: isDragging ? 'none' : 'transform 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
border: '1px solid var(--border-primary)',
|
||||
}}
|
||||
>
|
||||
{/* Drawer Handle - mobile only, draggable */}
|
||||
@@ -112,32 +116,28 @@ export function CommandDrawer({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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 justify-between px-4 py-3 border-b border-[var(--border-primary)] bg-[var(--bg-secondary)]">
|
||||
<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' : ''}
|
||||
<span className="hidden md:inline"> • Press Esc to close</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-5 rounded-full" style={{ backgroundColor: distroColor }}></div>
|
||||
<div>
|
||||
<h3 id="drawer-title" className="text-sm font-bold uppercase tracking-wider text-[var(--text-secondary)]">Terminal Preview</h3>
|
||||
<p className="text-xs text-[var(--text-muted)] mt-0.5">
|
||||
{selectedCount} apps selected
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
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"
|
||||
className="p-2 -mr-2 rounded-md 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>
|
||||
|
||||
{/* Command Content */}
|
||||
<div className="p-4 sm:p-6 overflow-y-auto" style={{ maxHeight: 'calc(80vh - 200px)' }}>
|
||||
{/* AUR Settings */}
|
||||
<div className="p-4 overflow-y-auto bg-[var(--bg-primary)]/50" style={{ maxHeight: 'calc(80vh - 140px)' }}>
|
||||
{showAur && (
|
||||
<AurDrawerSettings
|
||||
aurAppNames={aurAppNames}
|
||||
@@ -145,44 +145,47 @@ export function CommandDrawer({
|
||||
setHasYayInstalled={setHasYayInstalled}
|
||||
selectedHelper={selectedHelper}
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
distroColor={distroColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Terminal window */}
|
||||
<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>
|
||||
{/* Terminal window */}
|
||||
<div className="bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-primary)] overflow-hidden shadow-sm">
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-[var(--bg-tertiary)] border-b border-[var(--border-primary)]">
|
||||
<span className="text-xs font-mono text-[var(--text-muted)]">bash</span>
|
||||
|
||||
{/* Desktop action buttons */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<button
|
||||
onClick={onDownload}
|
||||
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"
|
||||
className="h-8 px-4 flex items-center gap-2 rounded-md hover:bg-[var(--bg-primary)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-all text-xs font-medium"
|
||||
>
|
||||
<Download className="w-3.5 h-3.5" />
|
||||
Download
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Script</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyAndClose}
|
||||
className={`h-7 px-3 flex items-center gap-1.5 rounded-md text-xs font-medium transition-colors ${copied
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600 hover:text-white'
|
||||
className={`h-8 px-4 flex items-center gap-2 rounded-md text-xs font-medium transition-all ${copied
|
||||
? 'shadow-sm'
|
||||
: 'hover:bg-[var(--bg-primary)] text-[var(--text-muted)] hover:text-[var(--text-primary)]'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: copied ? distroColor : 'transparent',
|
||||
color: copied ? '#000' : undefined,
|
||||
}}
|
||||
>
|
||||
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
<span>{copied ? 'Copied' : 'Copy'}</span>
|
||||
</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>
|
||||
|
||||
<div className="p-4 font-mono text-sm overflow-x-auto bg-[var(--bg-secondary)]">
|
||||
<div className="flex gap-3">
|
||||
<span className="select-none shrink-0 font-bold" style={{ color: distroColor }}>$</span>
|
||||
<code
|
||||
className="text-gray-300 break-all whitespace-pre-wrap"
|
||||
className="text-[var(--text-primary)] break-all whitespace-pre-wrap select-text"
|
||||
style={{
|
||||
lineHeight: '1.6',
|
||||
fontFamily: 'var(--font-jetbrains-mono), monospace'
|
||||
@@ -195,21 +198,24 @@ export function CommandDrawer({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Actions - side by side for better UX */}
|
||||
<div className="md:hidden flex items-stretch gap-3 px-4 py-4 border-t border-[var(--border-primary)]">
|
||||
{/* Mobile Actions */}
|
||||
<div className="md:hidden flex items-stretch gap-3 px-4 py-3 border-t border-[var(--border-primary)] bg-[var(--bg-secondary)]">
|
||||
<button
|
||||
onClick={onDownload}
|
||||
className="flex-1 h-12 flex items-center justify-center gap-2 rounded-xl bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] active:scale-[0.98] transition-all font-medium text-sm"
|
||||
className="flex-1 h-11 flex items-center justify-center gap-2 rounded-md bg-[var(--bg-tertiary)] border border-[var(--border-primary)] text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] active:scale-[0.98] transition-all font-medium text-sm"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCopyAndClose}
|
||||
className={`flex-1 h-12 flex items-center justify-center gap-2 rounded-xl font-medium text-sm active:scale-[0.98] transition-all ${copied
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-[var(--text-primary)] text-[var(--bg-primary)] hover:opacity-90'
|
||||
className={`flex-1 h-11 flex items-center justify-center gap-2 rounded-md font-medium text-sm active:scale-[0.98] transition-all shadow-sm ${copied
|
||||
? 'text-black'
|
||||
: 'text-[var(--text-primary)] bg-[var(--bg-tertiary)] border border-[var(--border-primary)]'
|
||||
}`}
|
||||
style={{
|
||||
backgroundColor: copied ? distroColor : undefined,
|
||||
}}
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
|
||||
@@ -79,6 +79,7 @@ export function CommandFooter({
|
||||
|
||||
const showAur = selectedDistro === 'arch' && hasAurPackages;
|
||||
const distroDisplayName = distros.find(d => d.id === selectedDistro)?.name || selectedDistro;
|
||||
const distroColor = distros.find(d => d.id === selectedDistro)?.color || 'var(--accent)';
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
if (selectedCount === 0) return;
|
||||
@@ -174,6 +175,7 @@ export function CommandFooter({
|
||||
setHasYayInstalled={setHasYayInstalled}
|
||||
selectedHelper={selectedHelper}
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
distroColor={distroColor}
|
||||
/>
|
||||
|
||||
{/* Animated footer container - only shows after first selection */}
|
||||
@@ -205,23 +207,39 @@ export function CommandFooter({
|
||||
searchInputRef={searchInputRef}
|
||||
selectedCount={selectedCount}
|
||||
distroName={distroDisplayName}
|
||||
distroColor={distroColor}
|
||||
showAur={showAur}
|
||||
selectedHelper={selectedHelper}
|
||||
setSelectedHelper={setSelectedHelper}
|
||||
/>
|
||||
|
||||
{/* Command Bar */}
|
||||
<div className="bg-[var(--bg-tertiary)] font-mono text-xs rounded-lg overflow-hidden border border-[var(--border-primary)]/40 shadow-2xl">
|
||||
{/* Command Bar - AccessGuide style */}
|
||||
<div className="bg-[var(--bg-tertiary)] font-mono text-xs overflow-hidden border-l-4 shadow-2xl"
|
||||
style={{ borderLeftColor: distroColor }}>
|
||||
<div className="flex items-stretch">
|
||||
{/* Preview button (hidden on mobile) */}
|
||||
<button
|
||||
onClick={() => selectedCount > 0 && setDrawerOpen(true)}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-r border-[var(--border-primary)]/30 transition-colors 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' : ''}`}
|
||||
className={`hidden md:flex items-center gap-2 px-5 py-3 border-r border-[var(--border-primary)]/20 transition-all shrink-0 font-medium ${selectedCount === 0 ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
title="Toggle Preview (Tab)"
|
||||
style={{
|
||||
backgroundColor: selectedCount > 0 ? `color-mix(in srgb, ${distroColor}, transparent 90%)` : undefined,
|
||||
color: selectedCount > 0 ? distroColor : undefined,
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedCount > 0) {
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${distroColor}, transparent 80%)`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedCount > 0) {
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${distroColor}, transparent 90%)`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ChevronUp className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="font-bold whitespace-nowrap">PREVIEW</span>
|
||||
<span className="whitespace-nowrap text-xs uppercase tracking-wider">Preview</span>
|
||||
{selectedCount > 0 && (
|
||||
<span className="text-[10px] opacity-60 ml-0.5 whitespace-nowrap">[{selectedCount}]</span>
|
||||
)}
|
||||
@@ -232,7 +250,7 @@ export function CommandFooter({
|
||||
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)]'}`}>
|
||||
<code className={`whitespace-nowrap overflow-x-auto command-scroll leading-none text-sm font-semibold ${selectedCount > 0 ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)]'}`}>
|
||||
{command}
|
||||
</code>
|
||||
</div>
|
||||
@@ -241,13 +259,23 @@ export function CommandFooter({
|
||||
<button
|
||||
onClick={clearAll}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-all duration-150 font-sans text-sm font-medium ${selectedCount > 0
|
||||
? 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] active:scale-[0.97]'
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-l border-[var(--border-primary)]/20 transition-all duration-150 font-sans text-sm ${selectedCount > 0
|
||||
? 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]'
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title="Clear All (c)"
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedCount > 0) {
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${distroColor}, transparent 95%)`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedCount > 0) {
|
||||
e.currentTarget.style.backgroundColor = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X className="w-3.5 h-3.5 shrink-0" />
|
||||
<X className="w-4 h-4 shrink-0 opacity-70" />
|
||||
<span className="hidden sm:inline whitespace-nowrap">Clear</span>
|
||||
</button>
|
||||
|
||||
@@ -255,13 +283,23 @@ export function CommandFooter({
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-all duration-150 font-sans text-sm font-medium ${selectedCount > 0
|
||||
? 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] active:scale-[0.97]'
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-l border-[var(--border-primary)]/20 transition-all duration-150 font-sans text-sm ${selectedCount > 0
|
||||
? 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]'
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title="Download Script (d)"
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedCount > 0) {
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${distroColor}, transparent 95%)`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedCount > 0) {
|
||||
e.currentTarget.style.backgroundColor = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Download className="w-3.5 h-3.5 shrink-0" />
|
||||
<Download className="w-4 h-4 shrink-0 opacity-70" />
|
||||
<span className="hidden sm:inline whitespace-nowrap">Download</span>
|
||||
</button>
|
||||
|
||||
@@ -269,15 +307,25 @@ export function CommandFooter({
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
disabled={selectedCount === 0}
|
||||
className={`hidden md:flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-all duration-150 font-sans text-sm font-medium ${selectedCount > 0
|
||||
className={`hidden md:flex items-center gap-2 px-4 py-3 border-l border-[var(--border-primary)]/20 transition-all duration-150 font-sans text-sm ${selectedCount > 0
|
||||
? (copied
|
||||
? 'text-emerald-400'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-hover)] active:scale-[0.97]')
|
||||
? 'text-emerald-400 font-medium'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] active:scale-[0.97]')
|
||||
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
|
||||
}`}
|
||||
title="Copy Command (y)"
|
||||
onMouseEnter={(e) => {
|
||||
if (selectedCount > 0 && !copied) {
|
||||
e.currentTarget.style.backgroundColor = `color-mix(in srgb, ${distroColor}, transparent 95%)`;
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (selectedCount > 0 && !copied) {
|
||||
e.currentTarget.style.backgroundColor = '';
|
||||
}
|
||||
}}
|
||||
>
|
||||
{copied ? <Check className="w-3.5 h-3.5 shrink-0" /> : <Copy className="w-3.5 h-3.5 shrink-0" />}
|
||||
{copied ? <Check className="w-4 h-4 shrink-0" /> : <Copy className="w-4 h-4 shrink-0 opacity-70" />}
|
||||
<span className="hidden sm:inline whitespace-nowrap">{copied ? 'Copied!' : 'Copy'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ interface ShortcutsBarProps {
|
||||
searchInputRef: React.RefObject<HTMLInputElement | null>;
|
||||
selectedCount: number;
|
||||
distroName: string;
|
||||
distroColor: string;
|
||||
showAur: boolean;
|
||||
selectedHelper: 'yay' | 'paru';
|
||||
setSelectedHelper: (helper: 'yay' | 'paru') => void;
|
||||
@@ -20,6 +21,7 @@ export function ShortcutsBar({
|
||||
searchInputRef,
|
||||
selectedCount,
|
||||
distroName,
|
||||
distroColor,
|
||||
showAur,
|
||||
selectedHelper,
|
||||
setSelectedHelper,
|
||||
@@ -33,12 +35,16 @@ export function ShortcutsBar({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[var(--bg-tertiary)] border border-[var(--border-primary)] font-mono text-xs rounded-lg overflow-hidden">
|
||||
<div className="bg-[var(--bg-tertiary)] border-l-4 font-mono text-xs overflow-hidden"
|
||||
style={{ borderLeftColor: distroColor }}>
|
||||
<div className="flex items-stretch justify-between">
|
||||
{/* LEFT SECTION */}
|
||||
<div className="flex items-stretch">
|
||||
{/* Mode Badge - like nvim NORMAL/INSERT (hidden on mobile) */}
|
||||
<div className="hidden md:flex bg-[var(--text-primary)] text-[var(--bg-primary)] px-3 py-1 font-bold items-center whitespace-nowrap">
|
||||
<div
|
||||
className="hidden md:flex text-white px-3 py-1 font-bold items-center whitespace-nowrap"
|
||||
style={{ backgroundColor: distroColor }}
|
||||
>
|
||||
{distroName.toUpperCase()}
|
||||
</div>
|
||||
|
||||
@@ -115,7 +121,10 @@ export function ShortcutsBar({
|
||||
</div>
|
||||
|
||||
{/* End badge - like nvim line:col */}
|
||||
<div className="bg-[var(--text-primary)] text-[var(--bg-primary)] px-3 py-1 flex items-center font-bold text-xs tracking-wider">
|
||||
<div
|
||||
className="text-white px-3 py-1 flex items-center font-bold text-xs tracking-wider"
|
||||
style={{ backgroundColor: distroColor }}
|
||||
>
|
||||
TUX
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,23 +66,25 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip
|
||||
ref={setRef}
|
||||
role="tooltip"
|
||||
className="fixed hidden md:block pointer-events-auto z-[9999]"
|
||||
style={{ left: current.x, top: current.y - 10 }}
|
||||
style={{ left: current.x, top: current.y - 12 }} // Moved up slightly to clear cursor
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div className={`
|
||||
absolute left-1/2 bottom-0 -translate-x-1/2
|
||||
absolute left-0 bottom-0
|
||||
transition-opacity duration-75
|
||||
${visible ? 'opacity-100' : 'opacity-0 pointer-events-none'}
|
||||
`}>
|
||||
{/* Clean tooltip bubble */}
|
||||
{/* AccessGuide-style tooltip - rectangular with left border accent */}
|
||||
<div
|
||||
className="px-3.5 py-2.5 rounded-lg shadow-lg overflow-hidden"
|
||||
className="px-3.5 py-2.5 shadow-lg overflow-hidden border-l-4 relative"
|
||||
style={{
|
||||
minWidth: '300px',
|
||||
maxWidth: '300px',
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
border: '1px solid var(--border-primary)',
|
||||
borderLeftColor: 'var(--accent)',
|
||||
boxShadow: '0 4px 20px rgba(0,0,0,0.2)',
|
||||
transform: 'translateX(-22px)', // Shift tooltip so arrow aligns with mouse (arrow is at left: 16px + half width)
|
||||
}}
|
||||
>
|
||||
<p className="text-[13px] leading-[1.55] text-[var(--text-secondary)] break-words" style={{ wordBreak: 'break-word' }}>
|
||||
@@ -90,14 +92,14 @@ export function Tooltip({ tooltip, onMouseEnter, onMouseLeave, setRef }: Tooltip
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<div className="absolute left-1/2 -translate-x-1/2 -bottom-[5px]">
|
||||
{/* Arrow aligned to mouse position */}
|
||||
{/* Arrow at left: 16px matches the visual design, so we shift wrapper -22px to align 16px + 6px(half arrow) approx to 0 */}
|
||||
<div className="absolute left-0 -bottom-[6px]" style={{ transform: 'translateX(-6px)' }}>
|
||||
<div
|
||||
className="w-2.5 h-2.5 rotate-45"
|
||||
className="w-3 h-3 rotate-45"
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-secondary)',
|
||||
borderRight: '1px solid var(--border-primary)',
|
||||
borderBottom: '1px solid var(--border-primary)',
|
||||
boxShadow: '2px 2px 4px rgba(0,0,0,0.15)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -54,55 +54,47 @@ export function DistroSelector({
|
||||
background: 'rgba(0,0,0,0.05)',
|
||||
}}
|
||||
/>
|
||||
{/* Dropdown */}
|
||||
{/* Dropdown - AccessGuide style: rectangular with left border */}
|
||||
<div
|
||||
className="distro-dropdown bg-[var(--bg-secondary)] border border-[var(--border-primary)]"
|
||||
className="distro-dropdown bg-[var(--bg-secondary)] border-l-4 rounded-md"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: dropdownPos.top,
|
||||
right: dropdownPos.right,
|
||||
zIndex: 99999,
|
||||
borderRadius: '20px',
|
||||
padding: '10px',
|
||||
minWidth: '200px',
|
||||
boxShadow: '0 20px 60px -10px rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.05)',
|
||||
borderLeftColor: currentDistro?.color || 'var(--accent)',
|
||||
padding: '8px 0',
|
||||
minWidth: '220px',
|
||||
boxShadow: '0 8px 32px rgba(0,0,0,0.2)',
|
||||
transformOrigin: 'top right',
|
||||
animation: 'distroDropdownOpen 0.35s cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
animation: 'distroDropdownOpen 0.25s ease-out',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="px-3 py-2 mb-1">
|
||||
<span className="text-[10px] font-semibold text-[var(--text-muted)] uppercase tracking-widest">Select Distro</span>
|
||||
</div>
|
||||
|
||||
{/* Distro List */}
|
||||
<div className="space-y-0.5">
|
||||
<div>
|
||||
{distros.map((distro, i) => (
|
||||
<button
|
||||
key={distro.id}
|
||||
onClick={() => { onSelect(distro.id); setIsOpen(false); analytics.distroSelected(distro.name); }}
|
||||
className={`group w-full flex items-center gap-3 py-2.5 px-3 rounded-xl border-none cursor-pointer text-left transition-[background-color,transform] duration-200 ${selectedDistro === distro.id
|
||||
? 'bg-[var(--accent)]/10'
|
||||
: 'bg-transparent hover:bg-[var(--bg-hover)] hover:scale-[1.02]'
|
||||
className={`group w-full flex items-center gap-3 py-3 px-4 cursor-pointer text-left transition-colors duration-100 ${selectedDistro === distro.id
|
||||
? 'border-l-2 -ml-[2px] pl-[18px]'
|
||||
: 'hover:bg-[var(--bg-hover)]'
|
||||
}`}
|
||||
style={{
|
||||
animation: `distroItemSlide 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) ${i * 0.04}s both`,
|
||||
animation: `distroItemSlide 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) ${i * 0.03}s both`,
|
||||
backgroundColor: selectedDistro === distro.id ? `color-mix(in srgb, ${distro.color}, transparent 85%)` : undefined,
|
||||
borderColor: selectedDistro === distro.id ? distro.color : undefined
|
||||
}}
|
||||
>
|
||||
<div className={`w-7 h-7 rounded-lg flex items-center justify-center transition-transform duration-200 ${selectedDistro === distro.id
|
||||
? 'bg-[var(--accent)]/20 scale-110'
|
||||
: 'bg-[var(--bg-tertiary)] group-hover:scale-105'
|
||||
}`}>
|
||||
<DistroIcon url={distro.iconUrl} name={distro.name} size={18} />
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
<DistroIcon url={distro.iconUrl} name={distro.name} size={22} />
|
||||
</div>
|
||||
<span className={`flex-1 text-sm transition-colors ${selectedDistro === distro.id
|
||||
<span className={`flex-1 text-[15px] ${selectedDistro === distro.id
|
||||
? 'text-[var(--text-primary)] font-medium'
|
||||
: 'text-[var(--text-secondary)]'
|
||||
}`}>{distro.name}</span>
|
||||
{selectedDistro === distro.id && (
|
||||
<div className="w-5 h-5 rounded-full bg-[var(--accent)] flex items-center justify-center">
|
||||
<Check className="w-3 h-3 text-white" />
|
||||
</div>
|
||||
<Check className="w-5 h-5" style={{ color: distro.color }} strokeWidth={2.5} />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
@@ -119,13 +111,16 @@ export function DistroSelector({
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isOpen}
|
||||
aria-label={`Select distribution, current: ${currentDistro?.name}`}
|
||||
className={`group flex items-center gap-2.5 h-10 pl-2.5 pr-3.5 rounded-2xl border border-[var(--border-primary)] bg-[var(--bg-secondary)] transition-[background-color,box-shadow,border-color] duration-300 ${isOpen ? 'ring-2 ring-[var(--accent)]/30 border-[var(--accent)]/50' : 'hover:bg-[var(--bg-hover)]'}`}
|
||||
className={`group flex items-center gap-2.5 h-10 pl-3 pr-3.5 border-l-4 bg-[var(--bg-secondary)] rounded-md transition-all duration-150 ${isOpen ? 'bg-[var(--bg-tertiary)]' : 'hover:bg-[var(--bg-hover)]'}`}
|
||||
style={{
|
||||
borderColor: currentDistro?.color || 'var(--accent)'
|
||||
}}
|
||||
>
|
||||
<div className="w-6 h-6 rounded-lg bg-[var(--bg-tertiary)] flex items-center justify-center overflow-hidden transition-transform duration-300 group-hover:scale-110">
|
||||
<DistroIcon url={currentDistro?.iconUrl || ''} name={currentDistro?.name || ''} size={16} />
|
||||
<div className="w-6 h-6 flex items-center justify-center">
|
||||
<DistroIcon url={currentDistro?.iconUrl || ''} name={currentDistro?.name || ''} size={20} />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">{currentDistro?.name}</span>
|
||||
<ChevronDown className={`w-3.5 h-3.5 text-[var(--text-muted)] transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`} />
|
||||
<span className="text-[15px] font-medium text-[var(--text-primary)]">{currentDistro?.name}</span>
|
||||
<ChevronDown className={`w-4 h-4 text-[var(--text-muted)] transition-transform duration-150 ${isOpen ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
{mounted && typeof document !== 'undefined' && createPortal(dropdown, document.body)}
|
||||
</>
|
||||
|
||||
@@ -86,17 +86,18 @@ export function HowItWorks() {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
{/* Modal - AccessGuide style: rectangular with left border accent */}
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="how-it-works-title"
|
||||
className="fixed bg-[var(--bg-secondary)] border border-[var(--border-primary)] z-[99999]"
|
||||
className="fixed bg-[var(--bg-primary)] border-l-4 z-[99999]"
|
||||
style={{
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
borderRadius: '16px',
|
||||
borderRadius: '0 4px 4px 0',
|
||||
borderLeftColor: 'var(--accent)',
|
||||
width: '620px',
|
||||
maxWidth: 'calc(100vw - 32px)',
|
||||
maxHeight: 'min(85vh, 720px)',
|
||||
@@ -106,17 +107,17 @@ export function HowItWorks() {
|
||||
? 'modalSlideOut 0.2s ease-out forwards'
|
||||
: 'modalSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1)',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.2)',
|
||||
boxShadow: '0 16px 48px -8px rgba(0, 0, 0, 0.25)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-primary)]">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border-secondary)]">
|
||||
<h3 id="how-it-works-title" className="text-base font-semibold text-[var(--text-primary)]">
|
||||
Help
|
||||
</h3>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-1.5 -mr-1 rounded-lg hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
||||
className="p-1.5 -mr-1 hover:bg-[var(--bg-hover)] text-[var(--text-muted)] hover:text-[var(--text-primary)] transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
@@ -125,9 +126,9 @@ export function HowItWorks() {
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto px-6 py-6 space-y-6" style={{ scrollbarGutter: 'stable' }}>
|
||||
|
||||
{/* Shortcuts */}
|
||||
{/* Shortcuts - AccessGuide style */}
|
||||
<section>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-4">Keyboard Shortcuts</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-4 pl-3 border-l-2 border-[var(--accent)]">Keyboard Shortcuts</h4>
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-2">
|
||||
{[
|
||||
['↑↓←→', 'Navigate through apps'],
|
||||
@@ -144,7 +145,7 @@ export function HowItWorks() {
|
||||
['1 / 2', 'Switch AUR helper (yay/paru)'],
|
||||
].map(([key, desc]) => (
|
||||
<div key={key} className="flex items-center gap-3 text-sm">
|
||||
<kbd className="inline-flex items-center justify-center min-w-[52px] px-2 py-1 rounded-md bg-[var(--bg-tertiary)] border border-[var(--border-primary)] text-xs font-mono text-[var(--text-secondary)]">
|
||||
<kbd className="inline-flex items-center justify-center min-w-[52px] px-2 py-1 bg-[var(--bg-secondary)] border-l-2 border-[var(--accent)] text-xs font-mono text-[var(--text-secondary)]">
|
||||
{key}
|
||||
</kbd>
|
||||
<span className="text-[var(--text-muted)]">{desc}</span>
|
||||
@@ -153,9 +154,9 @@ export function HowItWorks() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Getting Started */}
|
||||
{/* Getting Started - AccessGuide style */}
|
||||
<section>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">Getting Started</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--accent)]">Getting Started</h4>
|
||||
<ol className="space-y-2 text-sm text-[var(--text-muted)] leading-relaxed">
|
||||
<li>
|
||||
<strong className="text-[var(--text-secondary)]">1. Pick your distro</strong> — Select your Linux distribution from the dropdown at the top. This determines which package manager commands TuxMate generates for you.
|
||||
@@ -172,9 +173,9 @@ export function HowItWorks() {
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{/* Notes */}
|
||||
{/* Notes - AccessGuide style */}
|
||||
<section>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3">Good to Know</h4>
|
||||
<h4 className="text-sm font-medium text-[var(--text-primary)] mb-3 pl-3 border-l-2 border-[var(--accent)]">Good to Know</h4>
|
||||
<ul className="space-y-2 text-sm text-[var(--text-muted)] leading-relaxed">
|
||||
<li>
|
||||
<strong className="text-[var(--text-secondary)]">Greyed out apps</strong> aren't available in your distro's official repositories. Try switching to Flatpak or Snap in the dropdown, or hover the info icon next to the app for alternative installation methods.
|
||||
@@ -189,7 +190,7 @@ export function HowItWorks() {
|
||||
<strong className="text-[var(--text-secondary)]">Auto-save</strong> — Your app selections are saved automatically in your browser. Come back anytime and your selections will still be there.
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-[var(--text-secondary)]">Script Safety</strong> — Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with <code className="px-1 py-0.5 rounded bg-[var(--bg-tertiary)] text-xs font-mono">bash tuxmate-*.sh</code> to safely install your selection.
|
||||
<strong className="text-[var(--text-secondary)]">Script Safety</strong> — Downloaded scripts are robust and idempotent. They include error handling, network retries, and system checks. Run them with <code className="px-1 py-0.5 bg-[var(--bg-secondary)] border-l-2 border-[var(--accent)] text-xs font-mono">bash tuxmate-*.sh</code> to safely install your selection.
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-20 h-10 p-1 rounded-full",
|
||||
"flex w-20 h-10 p-1 rounded-md",
|
||||
"bg-[var(--bg-secondary)] border border-[var(--border-primary)]",
|
||||
className
|
||||
)}
|
||||
@@ -38,7 +38,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-20 h-10 p-1 rounded-full cursor-pointer transition-[background-color,box-shadow] duration-300",
|
||||
"flex w-20 h-10 p-1 rounded-md cursor-pointer transition-[background-color,box-shadow] duration-300",
|
||||
"bg-[var(--bg-secondary)] border border-[var(--border-primary)]",
|
||||
className
|
||||
)}
|
||||
@@ -52,7 +52,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
<div className="flex justify-between items-center w-full">
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center w-8 h-8 rounded-full transition-transform duration-300",
|
||||
"flex justify-center items-center w-8 h-8 rounded-sm transition-transform duration-300",
|
||||
isDark ? "transform translate-x-0" : "transform translate-x-10",
|
||||
"bg-[var(--bg-tertiary)]"
|
||||
)}
|
||||
@@ -71,7 +71,7 @@ export function ThemeToggle({ className }: ThemeToggleProps) {
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex justify-center items-center w-8 h-8 rounded-full transition-transform duration-300",
|
||||
"flex justify-center items-center w-8 h-8 rounded-sm transition-transform duration-300",
|
||||
isDark
|
||||
? "bg-transparent"
|
||||
: "transform -translate-x-10"
|
||||
|
||||
@@ -54,11 +54,12 @@ export function useTooltip() {
|
||||
cancel();
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const clientX = e.clientX; // Capture mouse X position
|
||||
|
||||
showTimeout.current = setTimeout(() => {
|
||||
setTooltip({
|
||||
content,
|
||||
x: rect.left + rect.width / 2,
|
||||
x: clientX, // Use mouse X instead of element center
|
||||
y: rect.top,
|
||||
});
|
||||
}, 450);
|
||||
|
||||
Reference in New Issue
Block a user