mirror of
https://github.com/abusoww/tuxmate.git
synced 2026-04-17 21:53:12 +02:00
improve mobile layout and UX
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,4 +41,4 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
# local notes
|
||||
.notes.md
|
||||
# notes.md
|
||||
|
||||
370
.notes.md
Normal file
370
.notes.md
Normal 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.
|
||||
@@ -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.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={`
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user