improve mobile layout and UX

This commit is contained in:
N1C4T
2026-01-06 12:45:44 +04:00
parent 07773ed42e
commit 6c9b2123dd
12 changed files with 500 additions and 48 deletions

2
.gitignore vendored
View File

@@ -41,4 +41,4 @@ yarn-error.log*
next-env.d.ts
# local notes
.notes.md
# notes.md

370
.notes.md Normal file
View File

@@ -0,0 +1,370 @@
# Code Style & Patterns
Personal notes on code style for this project.
---
## Comments
### ✅ Do
```typescript
// Debounce rapid keypresses - nobody needs 60fps navigation
// AUR gets special amber styling because it's ✨special✨
// Skip if already installed - no point reinstalling
// The vim keybindings that make us feel like hackers
```
### ❌ Don't
```typescript
/**
* Debounces rapid keypresses to prevent performance issues
* @param delay - The delay in milliseconds
* @returns void
*/
// ─────────────────────────────────────────────────────────────────
// Section Header
// ─────────────────────────────────────────────────────────────────
```
### Rules
1. Single-line `//` comments, not JSDoc blocks
2. No decorative section dividers
3. Explain "why", not "what"
4. Occasional humor when it fits
5. Short comments - if you need a paragraph, simplify the code
---
## Component Structure
### Size Limits
- **Functions**: ~30 lines
- **Components**: ~150 lines
- **Files**: ~300 lines
### Organization
- Logic in hooks, rendering in components
- One component per file
- Barrel files get one-liner comments
```typescript
// Header area components
export { HowItWorks } from './HowItWorks';
```
### File Naming
- `PascalCase.tsx` for components
- `camelCase.ts` for hooks/utilities
- `kebab-case` for directories
---
## TypeScript
```typescript
// Let inference work
const [selected, setSelected] = useState<Set<string>>(new Set());
// Destructure props
function AppItem({ app, isSelected }: AppItemProps) { ... }
// Union types over enums
type DistroId = 'ubuntu' | 'debian' | 'arch';
// Avoid any
const data: any = fetchData(); // ❌
```
---
## React Patterns
### Memoization
```typescript
// Memo for expensive children with frequent parent re-renders
const AppItem = memo(function AppItem({ ... }) { ... });
// useMemo for expensive calculations
const filtered = useMemo(() => apps.filter(...), [apps, query]);
// useCallback for handlers to memoized children
const handleToggle = useCallback((id) => { ... }, [deps]);
```
### State
```typescript
// Derive state, don't store computed values
const count = selected.size; // ✅ derived
const [count, setCount] = ... // ❌ redundant state
// Keep state minimal and colocated
```
### Early Returns
```typescript
if (!apps.length) return null;
if (isLoading) return <Skeleton />;
// Then the happy path
```
---
## Accessibility
### Keyboard Navigation
```typescript
// All interactive elements need keyboard support
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') handleClick();
}}
// Focus management after modals/overlays
useEffect(() => { inputRef.current?.focus(); }, [isOpen]);
```
### Semantic HTML
```typescript
// Use correct elements
<button> not <div onClick>
<nav>, <main>, <aside>, <article>
role="button" tabIndex={0} if custom element needed
```
### Focus Visibility
```css
/* Always visible focus states */
:focus-visible { outline: 2px solid var(--accent); }
```
---
## Performance
### Bundle Size
- Question every new dependency
- Use dynamic imports for heavy components
- Tree-shake: import `{ Check }` not `import * as Icons`
### Rendering
```typescript
// Avoid layout thrashing
const rect = element.getBoundingClientRect(); // batch reads
element.style.top = rect.top + 'px'; // then writes
// Debounce expensive handlers
const debouncedSearch = useMemo(
() => debounce(handleSearch, 300),
[]
);
```
### Loading States
- Skeleton loaders for structure
- Avoid layout shift (reserve space)
- Show progress for long operations
---
## Shell Script Security
```typescript
// ALWAYS escape user input
`install_pkg "${escapeShellString(app.name)}" "${pkg}"`
// Scripts start safe
set -euo pipefail
// No eval. Ever.
// No backticks for command substitution
```
### Error Messages
```bash
# Casual but informative
error "Run as regular user, not root."
warn "Update failed, continuing..."
# Not robotic
error "ERROR: Elevated privileges detected." # ❌
```
---
## Naming
| Type | Pattern | Example |
|------|---------|---------|
| Boolean | `is`, `has`, `should`, `can` | `isAur`, `hasYay` |
| Handler | `handle` prefix | `handleClick` |
| Setter | `set` prefix | `setTheme` |
| Ref | `Ref` suffix | `inputRef` |
**Concise**: `isAur` not `isArchUserRepositoryPackage`
---
## CSS & Theming
### Variables
```css
--bg-primary, --bg-secondary, --bg-tertiary
--text-primary, --text-secondary, --text-muted
--accent, --border-primary
```
### Rules
- Use variables, never hardcode colors
- `transition-colors` not `transition-all`
- Mobile-first breakpoints: `sm:`, `md:`, `lg:`
---
## Responsive Design
### Touch Targets
- Minimum 44x44px for touch
- Adequate spacing between targets
### Breakpoints
```css
/* Mobile first */
.element { /* base mobile */ }
@media (min-width: 640px) { /* sm */ }
@media (min-width: 768px) { /* md */ }
@media (min-width: 1024px) { /* lg */ }
```
---
## Error Handling
```typescript
// User-facing: friendly
toast.error("Couldn't copy. Try again?")
// Console: detailed
console.error('Clipboard API failed:', error);
// Never swallow
try { ... } catch { } // ❌
try { ... } catch (e) { console.error(e); } // ✅
```
---
## Dependencies
### Before Adding
1. Can we do it in <20 lines ourselves?
2. What's the bundle size impact?
3. Is it maintained?
4. Do we need the whole package or just one function?
### Existing Stack
- `framer-motion` - animations
- `lucide-react` - icons
- `gsap` - complex animations (use sparingly)
- `clsx` + `tailwind-merge` - className utilities
---
## Adding New Features
### New App
1. Add to `data.ts` in correct category
2. Include all available distro targets
3. Add `unavailableReason` with helpful links
4. Test generated scripts
### New Distro
1. Add to `DistroId` type
2. Add to `distros` array
3. Create script generator in `lib/scripts/`
4. Update `generateInstallScript.ts`
### New Component
1. Create in appropriate directory
2. Add to barrel `index.ts`
3. Keep under 150 lines
4. Extract hooks if logic-heavy
---
## Git Commits
```bash
feat: add keyboard navigation
fix: prevent hydration mismatch
refactor: extract AUR logic to hook
style: adjust tooltip positioning
docs: update README shortcuts
chore: bump dependencies
```
---
## Debugging
### Console Patterns
```typescript
// Conditional logging
if (process.env.NODE_ENV === 'development') {
console.log('Debug:', data);
}
// Structured logging
console.group('useLinuxInit');
console.log('distro:', distro);
console.log('selected:', selected.size);
console.groupEnd();
```
### React DevTools
- Check for unnecessary re-renders
- Verify memo is working
- Profile expensive components
---
## Anti-Patterns
| Don't | Do Instead |
|-------|------------|
| `any` type | Proper types or `unknown` |
| Prop drilling 5+ | Context or composition |
| `useEffect` for derived state | Compute during render |
| Giant switch | Object lookup |
| `// TODO: fix later` | Fix now or create issue |
| `transition-all` | `transition-colors` |
| Hardcoded colors | CSS variables |
| Store computed state | Derive it |
---
## Quick Reference
| ✅ Do | ❌ Don't |
|------|---------|
| `// brief comment` | `/** Long JSDoc */` |
| Early return | Nested ifs |
| `isAur` | `isArchUserRepositoryPackage` |
| Extract hooks | 500-line components |
| CSS variables | Hardcoded colors |
| `escapeShellString()` | String concatenation |
| Union types | Enums |
| Derive state | Store computed |
| `<button>` | `<div onClick>` |
| 44px touch targets | Tiny buttons |
---
## Philosophy
Code should feel like it was written by someone who:
- Uses Arch btw
- Has vim keybindings muscle memory
- Ships fast, iterates faster
- Writes for humans first
Not like enterprise software following a 200-page style guide.

View File

@@ -14,17 +14,17 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "TuxMate - Linux App Installer Command Generator",
title: "TuxMate - LINUX BULK APP INSTALLER",
description: "TuxMate helps you generate terminal commands to install your favorite apps on any Linux distribution. Select your distro, pick your apps, and get your install command.",
openGraph: {
title: "TuxMate - Linux App Installer",
title: "TuxMate - Linux Bulk App Installer",
description: "Generate install commands for 180+ apps on Ubuntu, Debian, Arch, Fedora, and more.",
type: "website",
url: "https://tuxmate.abusov.com",
url: "https://tuxmate.com",
},
twitter: {
card: "summary_large_image",
title: "TuxMate - Linux App Installer",
title: "TuxMate - Linux Bulk App Installer",
description: "Generate install commands for 180+ apps on any Linux distro.",
},
};

View File

@@ -220,7 +220,7 @@ export default function Home() {
The Linux Bulk App Installer.
</p>
<span className="hidden sm:inline text-[var(--text-muted)] opacity-30 text-[10px]"></span>
<div className="-ml-1 sm:ml-0 scale-90 sm:scale-100 origin-left opacity-80 hover:opacity-100 transition-opacity">
<div className="hidden sm:block">
<HowItWorks />
</div>
</div>
@@ -229,14 +229,18 @@ export default function Home() {
</div>
{/* Header Controls */}
<div className="header-controls flex items-center gap-3 sm:gap-4">
{/* Links */}
<div className="header-controls flex items-center justify-between sm:justify-end gap-3 sm:gap-4">
{/* Left side on mobile: Help + Links */}
<div className="flex items-center gap-3 sm:gap-4">
{/* Help - mobile only here, desktop is in title area */}
<div className="sm:hidden">
<HowItWorks />
</div>
<GitHubLink />
<ContributeLink />
</div>
{/* Control buttons */}
{/* Right side: Theme + Distro (with separator on desktop) */}
<div className="flex items-center gap-2 pl-2 sm:pl-3 border-l border-[var(--border-primary)]">
<ThemeToggle />
<DistroSelector selectedDistro={selectedDistro} onSelect={setSelectedDistro} />
@@ -249,7 +253,46 @@ export default function Home() {
{/* App Grid */}
<main className="px-4 sm:px-6 pb-40 relative" style={{ zIndex: 1 }}>
<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">
{/* Mobile: 2-column grid with balanced distribution */}
<div className="grid grid-cols-2 gap-x-4 md:hidden items-start">
{(() => {
// Tetris packing for 2 columns on mobile
const mobileColumns: Array<typeof allCategoriesWithApps> = [[], []];
const heights = [0, 0];
allCategoriesWithApps.forEach(catData => {
const minIdx = heights[0] <= heights[1] ? 0 : 1;
mobileColumns[minIdx].push(catData);
heights[minIdx] += catData.apps.length + 2;
});
return mobileColumns.map((columnCategories, colIdx) => (
<div key={`mobile-col-${colIdx}`}>
{columnCategories.map(({ category, apps: categoryApps }, catIdx) => (
<CategorySection
key={`${category}-${categoryApps.length}`}
category={category}
categoryApps={categoryApps}
selectedApps={selectedApps}
isAppAvailable={isAppAvailable}
selectedDistro={selectedDistro}
toggleApp={toggleApp}
isExpanded={expandedCategories.has(category)}
onToggleExpanded={() => toggleCategoryExpanded(category)}
focusedId={focusedItem?.id}
focusedType={focusedItem?.type}
onTooltipEnter={showTooltip}
onTooltipLeave={hideTooltip}
categoryIndex={catIdx}
onCategoryFocus={() => setFocusByItem('category', category)}
onAppFocus={(appId) => setFocusByItem('app', appId)}
/>
))}
</div>
));
})()}
</div>
{/* Desktop: Grid with Tetris packing */}
<div className="hidden md:grid md:grid-cols-4 lg:grid-cols-5 gap-x-8 items-start">
{columns.map((columnCategories, colIdx) => {
// Calculate starting index for this column (for staggered animation)
let globalIdx = 0;

View File

@@ -98,7 +98,7 @@ function CategorySectionComponent({
}, [categoryApps.length]);
return (
<div ref={sectionRef} className="mb-5 category-section">
<div ref={sectionRef} className="mb-3 md:mb-5 category-section">
<CategoryHeader
category={category}
isExpanded={isExpanded}

View File

@@ -80,9 +80,9 @@ export function AurFloatingCard({
}, 3000);
return (
<div className="fixed top-4 right-4 z-30">
<div className="fixed top-4 left-1/2 -translate-x-1/2 md:left-auto md:right-4 md:translate-x-0 z-30">
<p
className="text-[13px] text-[var(--text-muted)]"
className="text-[13px] text-[var(--text-muted)] text-center md:text-right"
style={{
animation: 'slideInFromRight 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards'
}}
@@ -96,7 +96,7 @@ export function AurFloatingCard({
// Hide cards while exiting
if (isExiting && helperChosen) {
return (
<div className="fixed top-4 right-4 z-30 flex flex-col gap-3 items-end">
<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' }}
@@ -116,7 +116,7 @@ export function AurFloatingCard({
}
return (
<div className="fixed top-4 right-4 z-30 flex flex-col gap-3 items-end">
<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? */}
<div
className={`

View File

@@ -1,5 +1,6 @@
'use client';
import { useState, useRef } from 'react';
import { Check, Copy, X, Download } from 'lucide-react';
import { AurDrawerSettings } from './AurDrawerSettings';
@@ -38,6 +39,32 @@ export function CommandDrawer({
selectedHelper,
setSelectedHelper,
}: CommandDrawerProps) {
// Swipe-to-dismiss state
const [dragOffset, setDragOffset] = useState(0);
const [isDragging, setIsDragging] = useState(false);
const dragStartY = useRef(0);
const DISMISS_THRESHOLD = 100; // px to drag before closing
const handleTouchStart = (e: React.TouchEvent) => {
dragStartY.current = e.touches[0].clientY;
setIsDragging(true);
};
const handleTouchMove = (e: React.TouchEvent) => {
if (!isDragging) return;
const delta = e.touches[0].clientY - dragStartY.current;
// Only allow dragging down (positive delta)
setDragOffset(Math.max(0, delta));
};
const handleTouchEnd = () => {
setIsDragging(false);
if (dragOffset > DISMISS_THRESHOLD) {
onClose();
}
setDragOffset(0);
};
if (!isOpen) return null;
const handleCopyAndClose = () => {
@@ -66,16 +93,22 @@ export function CommandDrawer({
style={{
animation: isClosing
? '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'
: 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)'
}}
>
{/* 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"
{/* Drawer Handle - mobile only, draggable */}
<div
className="flex justify-center pt-3 pb-2 md:hidden cursor-grab active:cursor-grabbing touch-none"
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
>
<div
className="w-12 h-1.5 bg-[var(--text-muted)]/40 rounded-full"
onClick={onClose}
aria-label="Close drawer"
/>
</div>
@@ -87,7 +120,10 @@ export function CommandDrawer({
</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>
<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>
</div>
<button
@@ -153,24 +189,24 @@ export function CommandDrawer({
</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)]">
{/* 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)]">
<button
onClick={onDownload}
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"
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"
>
<Download className="w-5 h-5" />
Download Script
<Download className="w-4 h-4" />
Download
</button>
<button
onClick={handleCopyAndClose}
className={`flex-1 h-14 flex items-center justify-center gap-2 rounded-xl font-medium text-base transition-colors ${copied
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'
}`}
>
{copied ? <Check className="w-5 h-5" /> : <Copy className="w-5 h-5" />}
{copied ? 'Copied!' : 'Copy Command'}
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
{copied ? 'Copied!' : 'Copy'}
</button>
</div>
</div>

View File

@@ -203,11 +203,11 @@ export function CommandFooter({
{/* Command Bar */}
<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">
{/* Preview button */}
{/* Preview button (hidden on mobile) */}
<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-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-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' : ''}`}
title="Toggle Preview (Tab)"
>
<ChevronUp className="w-3.5 h-3.5 shrink-0" />
@@ -227,11 +227,11 @@ export function CommandFooter({
</code>
</div>
{/* Clear button */}
{/* Clear button (hidden on mobile) */}
<button
onClick={clearAll}
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
className={`hidden md:flex items-center gap-1.5 px-3 py-3 border-l border-[var(--border-primary)]/30 transition-colors ${selectedCount > 0
? 'text-[var(--text-muted)] hover:bg-rose-500/10 hover:text-rose-400'
: 'text-[var(--text-muted)] opacity-50 cursor-not-allowed'
}`}
@@ -241,11 +241,11 @@ export function CommandFooter({
<span className="hidden sm:inline whitespace-nowrap">Clear</span>
</button>
{/* Download button */}
{/* Download button (hidden on mobile) */}
<button
onClick={handleDownload}
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
className={`hidden md: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'
}`}
@@ -255,11 +255,11 @@ export function CommandFooter({
<span className="hidden sm:inline whitespace-nowrap">Download</span>
</button>
{/* Copy button */}
{/* Copy button (hidden on mobile) */}
<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
className={`hidden md: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')

View File

@@ -37,8 +37,8 @@ export function ShortcutsBar({
<div className="flex items-stretch justify-between">
{/* LEFT SECTION */}
<div className="flex items-stretch">
{/* Mode Badge - like nvim NORMAL/INSERT */}
<div className="bg-[var(--text-primary)] text-[var(--bg-primary)] px-3 py-1 font-bold flex items-center whitespace-nowrap">
{/* 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">
{distroName.toUpperCase()}
</div>
@@ -98,8 +98,8 @@ export function ShortcutsBar({
)}
</div>
{/* RIGHT SECTION - Compact Shortcuts */}
<div className="flex items-stretch">
{/* RIGHT SECTION - Compact Shortcuts (hidden on mobile) */}
<div className="hidden md:flex items-stretch">
<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>

View File

@@ -66,10 +66,11 @@ export function Tooltip({ tooltip, onEnter, onLeave }: TooltipProps) {
});
};
// Hide tooltips on mobile - they don't work with touch
return (
<div
role="tooltip"
className="fixed z-50 pointer-events-auto"
className="hidden md:block fixed z-50 pointer-events-auto"
style={{
left: left,
top: top,

View File

@@ -124,7 +124,7 @@ export function DistroSelector({
<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>
<span className="text-sm font-medium text-[var(--text-primary)] hidden sm:inline">{currentDistro?.name}</span>
<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' : ''}`} />
</button>
{mounted && typeof document !== 'undefined' && createPortal(dropdown, document.body)}

View File

@@ -200,10 +200,12 @@ export function HowItWorks() {
<button
ref={triggerRef}
onClick={handleOpen}
className={`flex items-center gap-1.5 text-sm transition-[color,transform] duration-200 hover:scale-105 ${isOpen ? 'text-[var(--text-primary)]' : 'text-[var(--text-muted)] hover:text-[var(--text-secondary)]'}`}
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs sm:text-sm transition-all duration-200 hover:scale-105 ${isOpen
? 'bg-[var(--accent)]/20 text-[var(--text-primary)]'
: 'bg-[var(--bg-tertiary)] text-[var(--text-muted)] hover:text-[var(--text-secondary)] hover:bg-[var(--bg-hover)]'}`}
>
<HelpCircle className="w-4 h-4" />
<span className="hidden sm:inline whitespace-nowrap">How it works?</span>
<HelpCircle className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
<span className="whitespace-nowrap">Help</span>
</button>
{isOpen && mounted && typeof document !== 'undefined' && createPortal(modal, document.body)}
</>