From 971c1155be6bd7bc3f0076df0e748a97a65c6268 Mon Sep 17 00:00:00 2001 From: Stalin S Date: Thu, 21 May 2026 03:07:10 +0530 Subject: [PATCH] Design: Complete redesign to compact, high-density Arch Linux package manager UI --- frontend/src/components/LoadingSpinner.jsx | 6 +- frontend/src/components/PackageCard.jsx | 84 ++-- frontend/src/components/PackageGrid.jsx | 26 +- frontend/src/components/SearchBar.jsx | 8 +- frontend/src/components/Sidebar.jsx | 63 ++- frontend/src/index.css | 487 ++++++++++----------- frontend/src/layouts/MainLayout.jsx | 57 +-- frontend/src/pages/Categories.jsx | 78 ++-- frontend/src/pages/Home.jsx | 175 ++++---- frontend/src/pages/Installed.jsx | 91 +++- frontend/src/pages/PackageView.jsx | 165 ++++--- frontend/src/pages/Search.jsx | 87 ++-- frontend/src/pages/Settings.jsx | 62 +-- frontend/src/pages/Updates.jsx | 91 ++-- 14 files changed, 743 insertions(+), 737 deletions(-) diff --git a/frontend/src/components/LoadingSpinner.jsx b/frontend/src/components/LoadingSpinner.jsx index 94a48c5..4955388 100644 --- a/frontend/src/components/LoadingSpinner.jsx +++ b/frontend/src/components/LoadingSpinner.jsx @@ -1,9 +1,9 @@ export default function LoadingSpinner({ size = 'md', text = '' }) { - const px = { sm: 20, md: 28, lg: 40 }[size] || 28; + const px = { sm: 16, md: 20, lg: 28 }[size] || 20; return ( -
+
- {text &&

{text}

} + {text &&

{text}

}
); } diff --git a/frontend/src/components/PackageCard.jsx b/frontend/src/components/PackageCard.jsx index 47d8b4b..3f4cd13 100644 --- a/frontend/src/components/PackageCard.jsx +++ b/frontend/src/components/PackageCard.jsx @@ -1,69 +1,69 @@ import { useNavigate } from 'react-router-dom'; -import { CheckCircle, AlertTriangle, Star, ArrowRight } from 'lucide-react'; +import { CheckCircle, AlertTriangle, Star, Download } from 'lucide-react'; export default function PackageCard({ pkg }) { const navigate = useNavigate(); - const sourceBadge = pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'; const sourceLabel = pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman'); return (
navigate(`/package/${pkg.name}`)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && navigate(`/package/${pkg.name}`)} > -
- {/* Top: Name and badges */} -
-
-
-

- {pkg.name} -

- {pkg.installed && } - {pkg.out_of_date && } -
- - {pkg.version || '—'} + {/* Header */} +
+
+
+ + {pkg.name} + {pkg.installed && } + {pkg.out_of_date && }
- {sourceLabel} + + {pkg.version || '—'} +
- - {/* Mid: Description */} -

- {pkg.description || 'No description available'} -

+ {sourceLabel}
- {/* Bottom: Stats & Action */} -
+ {pkg.description || 'No description available'} +

+ + {/* Footer */} +
-
+
{pkg.votes !== undefined && pkg.votes > 0 && ( - - {pkg.votes} + + {pkg.votes} )} - {pkg.popularity > 0 && ( - Pop: {pkg.popularity.toFixed(1)} - )} + {pkg.popularity > 0 && {pkg.popularity.toFixed(1)}}
- - - Details - + {pkg.installed ? ( + Installed + ) : ( + + Install + + )}
); diff --git a/frontend/src/components/PackageGrid.jsx b/frontend/src/components/PackageGrid.jsx index a28fc90..15551d6 100644 --- a/frontend/src/components/PackageGrid.jsx +++ b/frontend/src/components/PackageGrid.jsx @@ -6,18 +6,14 @@ export default function PackageGrid({ packages, loading }) { return (
{Array.from({ length: 6 }).map((_, i) => ( -
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
))}
@@ -26,13 +22,13 @@ export default function PackageGrid({ packages, loading }) { if (!packages || packages.length === 0) { return ( -
-
+
- +
-

No packages found

-

Try adjusting your search or filters

+

No packages found

+

Try adjusting your search or filters

); } diff --git a/frontend/src/components/SearchBar.jsx b/frontend/src/components/SearchBar.jsx index d4b3483..cdd2d10 100644 --- a/frontend/src/components/SearchBar.jsx +++ b/frontend/src/components/SearchBar.jsx @@ -12,15 +12,15 @@ export default function SearchBar({ onSearch, initialQuery = '' }) { return (
setQuery(e.target.value)} diff --git a/frontend/src/components/Sidebar.jsx b/frontend/src/components/Sidebar.jsx index 5f1ce86..f73f86b 100644 --- a/frontend/src/components/Sidebar.jsx +++ b/frontend/src/components/Sidebar.jsx @@ -1,7 +1,5 @@ import { NavLink } from 'react-router-dom'; -import { - Home, Search, Package, RefreshCw, Grid3X3, Settings, X -} from 'lucide-react'; +import { Home, Search, Package, RefreshCw, Grid3X3, Settings, X } from 'lucide-react'; import { useState, useEffect } from 'react'; import api from '../api/client'; @@ -25,38 +23,35 @@ export default function Sidebar({ isOpen, onClose }) { return ( ); diff --git a/frontend/src/index.css b/frontend/src/index.css index 2fc8da1..43acfc7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,105 +1,97 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap"); @import "tailwindcss"; /* ═══════════════════════════════════════════════ - ArchStore — Premium Design System - Dark & Light theme with CSS custom properties + ArchStore — Compact Dev-Tool Design System ═══════════════════════════════════════════════ */ :root { - /* Default: Dark Theme */ - --bg-base: #080c14; - --bg-primary: #0e1420; - --bg-secondary: #151d30; - --bg-tertiary: #1b263f; - --bg-card: #0e1420; - --bg-card-hover: #151d30; - --bg-elevated: #1b263f; - --bg-input: #080c14; - --bg-sidebar: rgba(14, 20, 32, 0.9); + --bg-base: #07090e; + --bg-primary: #0b0f18; + --bg-secondary: #101624; + --bg-tertiary: #161e30; + --bg-card: #0d1220; + --bg-card-hover: #111828; + --bg-elevated: #161e30; + --bg-input: #0a0e18; + --bg-sidebar: rgba(11, 15, 24, 0.95); --bg-overlay: rgba(0, 0, 0, 0.7); - --topbar-bg: rgba(14, 20, 32, 0.85); + --topbar-bg: rgba(11, 15, 24, 0.85); - --border-primary: #1b263f; - --border-secondary: #2c3b5e; + --border-primary: rgba(255, 255, 255, 0.06); + --border-secondary: rgba(255, 255, 255, 0.1); + --border-glow: rgba(56, 189, 248, 0.15); - --text-primary: #f8fafc; - --text-secondary: #cbd5e1; - --text-tertiary: #94a3b8; + --text-primary: #e2e8f0; + --text-secondary: #94a3b8; + --text-tertiary: #64748b; --text-inverse: #0f172a; - --accent-h: 199; - --accent-s: 89%; - --accent-l: 48%; - --accent: hsl(var(--accent-h), var(--accent-s), var(--accent-l)); - --accent-hover: hsl(var(--accent-h), var(--accent-s), 56%); - --accent-muted: hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.12); - --accent-glow: hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.25); + --accent: #38bdf8; + --accent-hover: #7dd3fc; + --accent-muted: rgba(56, 189, 248, 0.1); + --accent-glow: rgba(56, 189, 248, 0.2); - --green: #10b981; - --green-muted: rgba(16, 185, 129, 0.15); - --amber: #f59e0b; - --amber-muted: rgba(245, 158, 11, 0.15); - --red: #ef4444; - --red-muted: rgba(239, 68, 68, 0.15); - --blue: #3b82f6; - --blue-muted: rgba(59, 130, 246, 0.15); - --violet: #8b5cf6; + --green: #34d399; + --green-muted: rgba(52, 211, 153, 0.1); + --amber: #fbbf24; + --amber-muted: rgba(251, 191, 36, 0.1); + --red: #f87171; + --red-muted: rgba(248, 113, 113, 0.08); + --blue: #60a5fa; + --blue-muted: rgba(96, 165, 250, 0.1); + --violet: #a78bfa; + --violet-muted: rgba(167, 139, 250, 0.1); - --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3); - --shadow-md: 0 4px 20px -4px rgba(0, 0, 0, 0.4); - --shadow-lg: 0 12px 40px -8px rgba(0, 0, 0, 0.5); - --shadow-glow: 0 0 30px -5px var(--accent-glow); + --shadow-sm: 0 1px 2px rgba(0,0,0,0.4); + --shadow-md: 0 4px 12px rgba(0,0,0,0.3); + --shadow-lg: 0 8px 30px rgba(0,0,0,0.4); + --shadow-glow: 0 0 20px rgba(56,189,248,0.08); - --radius-sm: 8px; - --radius-md: 12px; - --radius-lg: 16px; - --radius-xl: 24px; + --radius-sm: 6px; + --radius-md: 8px; + --radius-lg: 10px; + --radius-xl: 14px; --radius-full: 9999px; - --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, monospace; - --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-normal: 250ms cubic-bezier(0.4, 0, 0.2, 1); - --transition-spring: 350ms cubic-bezier(0.16, 1, 0.3, 1); + --font-sans: 'Inter', system-ui, -apple-system, sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, monospace; + + --transition-fast: 120ms ease; + --transition-normal: 200ms ease; } .light { - /* Light Theme */ - --bg-base: #f8fafc; - --bg-primary: #f1f5f9; + --bg-base: #f1f5f9; + --bg-primary: #e2e8f0; --bg-secondary: #ffffff; --bg-tertiary: #f8fafc; --bg-card: #ffffff; --bg-card-hover: #f8fafc; --bg-elevated: #ffffff; - --bg-input: #f1f5f9; - --bg-sidebar: rgba(255, 255, 255, 0.9); - --bg-overlay: rgba(15, 23, 42, 0.3); - --topbar-bg: rgba(255, 255, 255, 0.85); + --bg-input: #e2e8f0; + --bg-sidebar: rgba(255, 255, 255, 0.95); + --bg-overlay: rgba(15, 23, 42, 0.2); + --topbar-bg: rgba(255, 255, 255, 0.9); - --border-primary: #e2e8f0; - --border-secondary: #cbd5e1; + --border-primary: rgba(0, 0, 0, 0.06); + --border-secondary: rgba(0, 0, 0, 0.1); + --border-glow: rgba(2, 132, 199, 0.15); - --text-primary: #0f172a; - --text-secondary: #334155; - --text-tertiary: #64748b; - --text-inverse: #f8fafc; + --text-primary: #1e293b; + --text-secondary: #475569; + --text-tertiary: #94a3b8; + --text-inverse: #f1f5f9; - --accent: hsl(var(--accent-h), var(--accent-s), 42%); - --accent-hover: hsl(var(--accent-h), var(--accent-s), 35%); - --accent-muted: hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.08); - --accent-glow: hsla(var(--accent-h), var(--accent-s), var(--accent-l), 0.15); + --accent: #0284c7; + --accent-hover: #0369a1; + --accent-muted: rgba(2, 132, 199, 0.08); + --accent-glow: rgba(2, 132, 199, 0.12); - --green-muted: rgba(16, 185, 129, 0.08); - --amber-muted: rgba(245, 158, 11, 0.08); - --red-muted: rgba(239, 68, 68, 0.06); - --blue-muted: rgba(59, 130, 246, 0.08); - - --shadow-sm: 0 1px 3px rgba(15, 23, 42, 0.05); - --shadow-md: 0 4px 20px -4px rgba(15, 23, 42, 0.08); - --shadow-lg: 0 12px 40px -8px rgba(15, 23, 42, 0.1); - --shadow-glow: 0 0 30px -5px var(--accent-glow); + --shadow-sm: 0 1px 2px rgba(0,0,0,0.04); + --shadow-md: 0 4px 12px rgba(0,0,0,0.06); + --shadow-lg: 0 8px 30px rgba(0,0,0,0.08); + --shadow-glow: 0 0 20px rgba(2,132,199,0.06); } @theme { @@ -113,20 +105,16 @@ --color-bg-input: var(--bg-input); --color-bg-sidebar: var(--bg-sidebar); --color-bg-overlay: var(--bg-overlay); - --color-border-primary: var(--border-primary); --color-border-secondary: var(--border-secondary); - --color-text-primary: var(--text-primary); --color-text-secondary: var(--text-secondary); --color-text-tertiary: var(--text-tertiary); --color-text-inverse: var(--text-inverse); - --color-accent: var(--accent); --color-accent-hover: var(--accent-hover); --color-accent-muted: var(--accent-muted); --color-accent-glow: var(--accent-glow); - --color-green: var(--green); --color-green-muted: var(--green-muted); --color-amber: var(--amber); @@ -136,49 +124,32 @@ --color-blue: var(--blue); --color-blue-muted: var(--blue-muted); --color-violet: var(--violet); - + --color-violet-muted: var(--violet-muted); --font-sans: var(--font-sans); --font-mono: var(--font-mono); } -/* ═══════════════════════════════════════════════ - Base Resets - ═══════════════════════════════════════════════ */ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} +/* ── Reset ── */ +*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: var(--font-sans); - background-color: var(--bg-base); + background: var(--bg-base); color: var(--text-primary); - line-height: 1.6; + font-size: 13px; + line-height: 1.5; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - transition: background-color var(--transition-normal), color var(--transition-normal); } -/* Custom Scrollbar */ -::-webkit-scrollbar { - width: 8px; - height: 8px; -} -::-webkit-scrollbar-track { - background: transparent; -} -::-webkit-scrollbar-thumb { - background: var(--border-primary); - border-radius: var(--radius-full); -} -::-webkit-scrollbar-thumb:hover { - background: var(--border-secondary); -} +/* ── Scrollbar ── */ +::-webkit-scrollbar { width: 4px; height: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--border-secondary); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); } /* ═══════════════════════════════════════════════ - App Structure + Layout ═══════════════════════════════════════════════ */ .app-layout { @@ -188,229 +159,222 @@ body { .sidebar { position: fixed; - left: 0; - top: 0; - bottom: 0; - width: 280px; + left: 0; top: 0; bottom: 0; + width: 220px; z-index: 50; display: flex; flex-direction: column; - padding: 32px 24px; + padding: 16px 12px; background: var(--bg-sidebar); border-right: 1px solid var(--border-primary); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - transition: transform var(--transition-spring), background var(--transition-normal); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + overflow-y: auto; + transition: transform 0.2s ease; } .main-content { - margin-left: 280px; + margin-left: 220px; flex: 1; min-height: 100vh; - padding: 40px 48px; - background-color: var(--bg-base); - transition: margin-left var(--transition-spring); + display: flex; + flex-direction: column; } -@media (max-width: 1024px) { - .sidebar { - transform: translateX(-100%); - } - .sidebar.open { - transform: translateX(0); - } - .main-content { - margin-left: 0; - padding: 24px 20px; - } +.content-shell { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + display: flex; + flex-direction: column; + flex: 1; } -/* ═══════════════════════════════════════════════ - Sidebar Items - ═══════════════════════════════════════════════ */ - -.nav-link { +.topbar { + position: sticky; + top: 0; + z-index: 40; display: flex; align-items: center; gap: 12px; - padding: 10px 16px; + padding: 10px 24px; + background: var(--topbar-bg); + border-bottom: 1px solid var(--border-primary); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); +} + +.topbar-search { flex: 1; max-width: 420px; } + +.page-shell { + padding: 20px 0 32px; + flex: 1; +} + +@media (max-width: 1024px) { + .sidebar { transform: translateX(-100%); } + .sidebar.open { transform: translateX(0); } + .main-content { margin-left: 0; } + .content-shell { padding: 0 16px; } + .topbar { padding: 10px 16px; } +} + +/* ── Nav Links ── */ +.nav-link { + display: flex; + align-items: center; + gap: 10px; + padding: 7px 12px; border-radius: var(--radius-md); - color: var(--text-secondary); + color: var(--text-tertiary); text-decoration: none; - font-size: 0.92rem; + font-size: 13px; font-weight: 500; transition: all var(--transition-fast); + position: relative; } .nav-link:hover { + color: var(--text-secondary); background: var(--accent-muted); - color: var(--text-primary); } .nav-link.active { - background: var(--accent-muted); color: var(--accent); + background: var(--accent-muted); font-weight: 600; } +.nav-link.active::before { + content: ''; + position: absolute; + left: 0; top: 50%; + transform: translateY(-50%); + width: 3px; height: 16px; + background: var(--accent); + border-radius: 0 3px 3px 0; +} /* ═══════════════════════════════════════════════ - Cards & Elements + Cards ═══════════════════════════════════════════════ */ .card { background: var(--bg-card); border: 1px solid var(--border-primary); border-radius: var(--radius-lg); - box-shadow: var(--shadow-sm); - transition: transform var(--transition-spring), - box-shadow var(--transition-spring), - background var(--transition-normal), - border-color var(--transition-fast); -} -.card-interactive { - cursor: pointer; + transition: all var(--transition-fast); } +.card-interactive { cursor: pointer; } .card-interactive:hover { - transform: translateY(-2px); background: var(--bg-card-hover); border-color: var(--border-secondary); - box-shadow: var(--shadow-md); + box-shadow: var(--shadow-glow); + transform: translateY(-1px); } +/* ── Badges ── */ .badge { display: inline-flex; align-items: center; - padding: 3px 10px; + padding: 2px 7px; border-radius: var(--radius-full); - font-size: 0.7rem; + font-size: 10px; font-weight: 700; - letter-spacing: 0.03em; + letter-spacing: 0.04em; text-transform: uppercase; } -.badge-pacman { - background: var(--accent-muted); - color: var(--accent); - border: 1px solid rgba(2, 132, 199, 0.25); -} -.badge-aur { - background: var(--amber-muted); - color: var(--amber); - border: 1px solid rgba(245, 158, 11, 0.25); -} -.badge-installed { - background: var(--green-muted); - color: var(--green); - border: 1px solid rgba(16, 185, 129, 0.25); -} +.badge-pacman { background: var(--accent-muted); color: var(--accent); } +.badge-aur { background: var(--amber-muted); color: var(--amber); } +.badge-installed { background: var(--green-muted); color: var(--green); } -/* Buttons */ +/* ── Buttons ── */ .btn { display: inline-flex; align-items: center; justify-content: center; - gap: 8px; - padding: 10px 20px; + gap: 6px; + padding: 6px 14px; border-radius: var(--radius-md); - font-size: 0.88rem; + font-size: 12px; font-weight: 600; cursor: pointer; border: none; + font-family: var(--font-sans); transition: all var(--transition-fast); + white-space: nowrap; } -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} +.btn:disabled { opacity: 0.5; cursor: not-allowed; } -.btn-primary { - background: var(--accent); - color: #ffffff; -} -.btn-primary:hover:not(:disabled) { - background: var(--accent-hover); -} +.btn-primary { background: var(--accent); color: #fff; } +.btn-primary:hover:not(:disabled) { background: var(--accent-hover); box-shadow: 0 0 12px var(--accent-glow); } -.btn-secondary { - background: var(--bg-secondary); - color: var(--text-primary); - border: 1px solid var(--border-primary); -} -.btn-secondary:hover:not(:disabled) { - background: var(--bg-tertiary); - border-color: var(--border-secondary); -} +.btn-secondary { background: var(--bg-secondary); color: var(--text-secondary); border: 1px solid var(--border-primary); } +.btn-secondary:hover:not(:disabled) { border-color: var(--border-secondary); color: var(--text-primary); } -.btn-danger { - background: var(--red-muted); - color: var(--red); - border: 1px solid rgba(239, 68, 68, 0.3); -} -.btn-danger:hover:not(:disabled) { - background: rgba(239, 68, 68, 0.25); -} +.btn-danger { background: var(--red-muted); color: var(--red); } +.btn-danger:hover:not(:disabled) { background: rgba(248,113,113,0.15); } -.btn-ghost { - background: transparent; - color: var(--text-secondary); -} -.btn-ghost:hover:not(:disabled) { - background: var(--bg-secondary); - color: var(--text-primary); -} +.btn-ghost { background: transparent; color: var(--text-tertiary); padding: 6px 10px; } +.btn-ghost:hover:not(:disabled) { color: var(--text-secondary); background: var(--bg-secondary); } -/* Inputs */ +/* ── Inputs ── */ .input { width: 100%; - padding: 10px 16px; + padding: 7px 12px; background: var(--bg-input); border: 1px solid var(--border-primary); border-radius: var(--radius-md); color: var(--text-primary); font-family: var(--font-sans); - font-size: 0.9rem; + font-size: 13px; outline: none; transition: all var(--transition-fast); } -.input::placeholder { - color: var(--text-tertiary); -} -.input:focus { - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); -} +.input::placeholder { color: var(--text-tertiary); } +.input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-glow); } -/* Typography Helpers */ +.searchbar { width: 100%; position: relative; } +.input-search { height: 34px; padding-top: 0; padding-bottom: 0; } + +/* ── Typography ── */ .page-title { - font-size: 1.8rem; - font-weight: 800; + font-size: 18px; + font-weight: 700; letter-spacing: -0.02em; color: var(--text-primary); + line-height: 1.3; } .page-subtitle { - font-size: 0.95rem; + font-size: 12px; color: var(--text-tertiary); + margin-top: 2px; } -/* Layout Grids */ +/* ── Grids ── */ .pkg-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 16px; -} -@media (max-width: 640px) { - .pkg-grid { - grid-template-columns: 1fr; - } + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 10px; } +@media (max-width: 640px) { .pkg-grid { grid-template-columns: 1fr; } } .cat-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 10px; } -/* Shimmer Loader */ +.update-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + transition: background var(--transition-fast); +} +.update-row:hover { background: var(--bg-card-hover); } + +/* ── Utilities ── */ .shimmer { - background: linear-gradient(90deg, - var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%); + background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%); background-size: 200% 100%; animation: shimmer-anim 1.5s infinite; border-radius: var(--radius-md); @@ -421,21 +385,16 @@ body { } .spinner { - width: 28px; - height: 28px; - border: 3px solid var(--border-primary); + width: 20px; height: 20px; + border: 2px solid var(--border-primary); border-top-color: var(--accent); border-radius: 50%; - animation: spin-anim 0.8s linear infinite; -} -@keyframes spin-anim { - to { transform: rotate(360deg); } + animation: spin-anim 0.6s linear infinite; } +@keyframes spin-anim { to { transform: rotate(360deg); } } -/* Theme Toggle Button */ .theme-toggle { - width: 40px; - height: 40px; + width: 32px; height: 32px; border-radius: var(--radius-md); background: var(--bg-secondary); border: 1px solid var(--border-primary); @@ -443,11 +402,37 @@ body { display: flex; align-items: center; justify-content: center; - color: var(--text-secondary); + color: var(--text-tertiary); transition: all var(--transition-fast); } -.theme-toggle:hover { - border-color: var(--border-secondary); - color: var(--text-primary); - background: var(--bg-tertiary); +.theme-toggle:hover { border-color: var(--border-secondary); color: var(--text-primary); } + +.gradient-text { + background: linear-gradient(135deg, var(--accent), var(--violet)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; } + +/* ── Animations ── */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(4px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes slideUp { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +.animate-fade-in { animation: fadeIn 0.2s ease-out both; } +.animate-slide-up { animation: slideUp 0.25s ease-out both; } + +.stagger > * { animation: fadeIn 0.2s ease-out both; } +.stagger > *:nth-child(1) { animation-delay: 20ms; } +.stagger > *:nth-child(2) { animation-delay: 40ms; } +.stagger > *:nth-child(3) { animation-delay: 60ms; } +.stagger > *:nth-child(4) { animation-delay: 80ms; } +.stagger > *:nth-child(5) { animation-delay: 100ms; } +.stagger > *:nth-child(6) { animation-delay: 120ms; } +.stagger > *:nth-child(7) { animation-delay: 140ms; } +.stagger > *:nth-child(8) { animation-delay: 160ms; } +.stagger > *:nth-child(n+9) { animation-delay: 180ms; } diff --git a/frontend/src/layouts/MainLayout.jsx b/frontend/src/layouts/MainLayout.jsx index 844d313..122ceb7 100644 --- a/frontend/src/layouts/MainLayout.jsx +++ b/frontend/src/layouts/MainLayout.jsx @@ -11,11 +11,8 @@ export default function MainLayout() { useEffect(() => { const root = document.documentElement; - if (theme === 'light') { - root.classList.add('light'); - } else { - root.classList.remove('light'); - } + if (theme === 'light') root.classList.add('light'); + else root.classList.remove('light'); localStorage.setItem('archstore-theme', theme); }, [theme]); @@ -30,7 +27,6 @@ export default function MainLayout() { return (
- {/* Mobile overlay */} {sidebarOpen && (
setSidebarOpen(false)} />
+ {/* Sticky Topbar */} +
+ + +
+ +
+ + +
+ + {/* Page Content */}
- {/* ── Top Bar ── */} -
- {/* Mobile menu */} - - - {/* Search */} -
- -
- - {/* Theme toggle */} - -
- - {/* ── Page ── */}
diff --git a/frontend/src/pages/Categories.jsx b/frontend/src/pages/Categories.jsx index eedbd5a..21288ed 100644 --- a/frontend/src/pages/Categories.jsx +++ b/frontend/src/pages/Categories.jsx @@ -8,14 +8,14 @@ import { } from 'lucide-react'; const catMeta = { - Development: { icon: Code, color: '#3b82f6', bg: 'rgba(59,130,246,0.1)' }, - System: { icon: Monitor, color: '#64748b', bg: 'rgba(100,116,139,0.1)' }, - Network: { icon: Wifi, color: '#10b981', bg: 'rgba(16,185,129,0.1)' }, - Multimedia: { icon: Music, color: '#a855f7', bg: 'rgba(168,85,247,0.1)' }, - Games: { icon: Gamepad2, color: '#ef4444', bg: 'rgba(239,68,68,0.1)' }, - Desktop: { icon: LayoutDashboard, color: '#6366f1', bg: 'rgba(99,102,241,0.1)' }, - Fonts: { icon: Type, color: '#f59e0b', bg: 'rgba(245,158,11,0.1)' }, - Security: { icon: ShieldCheck, color: '#06b6d4', bg: 'rgba(6,182,212,0.1)' }, + Development: { icon: Code, color: '#60a5fa', bg: 'rgba(96,165,250,0.08)', desc: 'Programming tools, compilers, and IDEs' }, + System: { icon: Monitor, color: '#94a3b8', bg: 'rgba(148,163,184,0.08)', desc: 'Core system utilities and tools' }, + Network: { icon: Wifi, color: '#34d399', bg: 'rgba(52,211,153,0.08)', desc: 'Networking tools, browsers, and servers' }, + Multimedia: { icon: Music, color: '#c084fc', bg: 'rgba(192,132,252,0.08)', desc: 'Audio, video, and image tools' }, + Games: { icon: Gamepad2, color: '#f87171', bg: 'rgba(248,113,113,0.08)', desc: 'Games and gaming tools' }, + Desktop: { icon: LayoutDashboard, color: '#818cf8', bg: 'rgba(129,140,248,0.08)', desc: 'Desktop environments and window managers' }, + Fonts: { icon: Type, color: '#fbbf24', bg: 'rgba(251,191,36,0.08)', desc: 'Fonts and typography' }, + Security: { icon: ShieldCheck, color: '#22d3ee', bg: 'rgba(34,211,238,0.08)', desc: 'Security and privacy tools' }, }; export default function Categories() { @@ -45,68 +45,76 @@ export default function Categories() { finally { setLoading(false); } } + /* ── Category detail view ── */ if (categoryName) { const meta = catMeta[categoryName] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)' }; const Icon = meta.icon; return ( -
- -
-
+
- +

{categoryName}

Browse popular {categoryName.toLowerCase()} packages

- {error &&
{error}
} + {error &&
{error}
}
); } + /* ── Category list view ── */ return ( -
-
+
+

Categories

Explore software by type

- {error &&
{error}
} + {error &&
{error}
} {loading ? (
{Array.from({ length: 8 }).map((_, i) => ( -
-
-
-
+
+
+
+
+
+
+
+
))}
) : ( -
- {categories.map((cat) => { - const meta = catMeta[cat.name] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)' }; +
+ {(categories.length > 0 ? categories : Object.keys(catMeta).map(n => ({ name: n }))).map((cat) => { + const meta = catMeta[cat.name] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)', desc: 'Explore packages' }; const Icon = meta.icon; return (
navigate(`/categories/${cat.name}`)}> -
- +
+
+ +
+
+

{cat.name}

+

+ {cat.description || meta.desc} +

+
-

- {cat.name} -

-

- {cat.description || 'Explore packages'} -

); })} diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index 090188e..3a3fe5a 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,21 +1,22 @@ import { useState, useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { - Search, TrendingUp, Package, ArrowRight, Sparkles, Shield, - Code, Monitor, Wifi, Music, Gamepad2, LayoutDashboard, Type, ShieldCheck + Search, TrendingUp, Package, ArrowRight, Shield, + Code, Monitor, Wifi, Music, Gamepad2, LayoutDashboard, Type, ShieldCheck, + Download, RefreshCw, Zap } from 'lucide-react'; import api from '../api/client'; import PackageGrid from '../components/PackageGrid'; const catMeta = { - Development: { icon: Code, color: '#3b82f6', bg: 'rgba(59,130,246,0.1)' }, - System: { icon: Monitor, color: '#64748b', bg: 'rgba(100,116,139,0.1)' }, - Network: { icon: Wifi, color: '#10b981', bg: 'rgba(16,185,129,0.1)' }, - Multimedia: { icon: Music, color: '#a855f7', bg: 'rgba(168,85,247,0.1)' }, - Games: { icon: Gamepad2, color: '#ef4444', bg: 'rgba(239,68,68,0.1)' }, - Desktop: { icon: LayoutDashboard, color: '#6366f1', bg: 'rgba(99,102,241,0.1)' }, - Fonts: { icon: Type, color: '#f59e0b', bg: 'rgba(245,158,11,0.1)' }, - Security: { icon: ShieldCheck, color: '#06b6d4', bg: 'rgba(6,182,212,0.1)' }, + Development: { icon: Code, color: '#60a5fa', bg: 'rgba(96,165,250,0.08)' }, + System: { icon: Monitor, color: '#94a3b8', bg: 'rgba(148,163,184,0.08)' }, + Network: { icon: Wifi, color: '#34d399', bg: 'rgba(52,211,153,0.08)' }, + Multimedia: { icon: Music, color: '#c084fc', bg: 'rgba(192,132,252,0.08)' }, + Games: { icon: Gamepad2, color: '#f87171', bg: 'rgba(248,113,113,0.08)' }, + Desktop: { icon: LayoutDashboard, color: '#818cf8', bg: 'rgba(129,140,248,0.08)' }, + Fonts: { icon: Type, color: '#fbbf24', bg: 'rgba(251,191,36,0.08)' }, + Security: { icon: ShieldCheck, color: '#22d3ee', bg: 'rgba(34,211,238,0.08)' }, }; export default function Home() { @@ -24,18 +25,21 @@ export default function Home() { const [featured, setFeatured] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); + const [updateCount, setUpdateCount] = useState(0); useEffect(() => { loadData(); }, []); async function loadData() { setLoading(true); try { - const [catRes, featRes] = await Promise.allSettled([ + const [catRes, featRes, updRes] = await Promise.allSettled([ api.listCategories(), api.searchPackages('firefox chromium vlc', 'all'), + api.checkUpdates(), ]); if (catRes.status === 'fulfilled') setCategories(catRes.value.results || []); if (featRes.status === 'fulfilled') setFeatured((featRes.value.results || []).slice(0, 6)); + if (updRes.status === 'fulfilled') setUpdateCount(updRes.value.count || 0); } catch { /* silent */ } finally { setLoading(false); } } @@ -45,115 +49,110 @@ export default function Home() { if (query.trim()) navigate(`/search?q=${encodeURIComponent(query.trim())}`); }; - return ( -
- {/* ════════════════ Hero ════════════════ */} -
- {/* Decorative blobs */} -
-
+ const stats = [ + { icon: Package, label: 'Official', value: 'Pacman', color: 'var(--accent)' }, + { icon: TrendingUp, label: 'AUR', value: '80,000+', color: 'var(--amber)' }, + { icon: Shield, label: 'Security', value: 'Scanner', color: 'var(--green)' }, + { icon: RefreshCw, label: 'Updates', value: updateCount > 0 ? `${updateCount} available` : 'Up to date', color: 'var(--violet)' }, + ]; -
-
- - - Welcome to ArchStore + return ( +
+ {/* ── Welcome Bar ── */} +
+
+
+ + + ArchStore
- -

- Discover packages for{' '} - Arch Linux +

+ Discover packages for Arch Linux

- -

- Browse, install, and manage software from official pacman repositories - and the AUR — all in one beautiful interface. +

+ Search official repos and the AUR in one place.

- - -
- - setQuery(e.target.value)} - /> -
- -
-
+
+
+ + setQuery(e.target.value)} + /> +
+ +
+
- {/* ════════════════ Stats ════════════════ */} -
- {[ - { icon: Package, label: 'Pacman Repos', value: 'Official', color: 'var(--accent)' }, - { icon: TrendingUp, label: 'AUR Packages', value: '80,000+', color: 'var(--amber)' }, - { icon: Shield, label: 'Security Scan', value: 'Built-in', color: 'var(--green)' }, - { icon: Sparkles, label: 'Updates', value: 'Real-time', color: 'var(--violet)' }, - ].map(({ icon: Icon, label, value, color }) => ( -
- -

{value}

-

{label}

+ {/* ── Stats Row ── */} +
+ {stats.map(({ icon: Icon, label, value, color }) => ( +
+
+ +
+
+

{value}

+

{label}

+
))} -
+
- {/* ════════════════ Categories ════════════════ */} -
-
-

Browse Categories

-
-
{(categories.length > 0 ? categories : Object.keys(catMeta).map(n => ({ name: n }))).map((cat) => { const meta = catMeta[cat.name] || { icon: Package, color: 'var(--accent)', bg: 'var(--accent-muted)' }; const Icon = meta.icon; return (
navigate(`/categories/${cat.name}`)}> -
- +
+
+ +
+
+

{cat.name}

+

+ {cat.description || 'Explore packages'} +

+
-

{cat.name}

-

- {cat.description || 'Explore packages'} -

); })}
- {/* ════════════════ Featured ════════════════ */} + {/* ── Featured Packages ── */} {featured.length > 0 && (
-
-

Popular Packages

-
diff --git a/frontend/src/pages/Installed.jsx b/frontend/src/pages/Installed.jsx index ce8def7..1e56fa1 100644 --- a/frontend/src/pages/Installed.jsx +++ b/frontend/src/pages/Installed.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import api from '../api/client'; import PackageGrid from '../components/PackageGrid'; -import { RefreshCw, Search } from 'lucide-react'; +import { RefreshCw, Search, LayoutGrid, List } from 'lucide-react'; export default function Installed() { const [packages, setPackages] = useState([]); @@ -9,6 +9,7 @@ export default function Installed() { const [loading, setLoading] = useState(true); const [filter, setFilter] = useState(''); const [error, setError] = useState(null); + const [viewMode, setViewMode] = useState('grid'); useEffect(() => { load(); }, []); @@ -22,50 +23,100 @@ export default function Installed() { async function load() { setLoading(true); setError(null); - try { - const data = await api.listInstalled(); - setPackages(data.results || []); - } catch (err) { setError(err.message); } + try { const data = await api.listInstalled(); setPackages(data.results || []); } + catch (err) { setError(err.message); } finally { setLoading(false); } } return ( -
-
+
+ {/* Header */} +

Installed Packages

- {packages.length > 0 ? `${packages.length} packages installed on your system` : 'Loading installed packages...'} + {packages.length > 0 ? `${packages.length} packages on your system` : 'Loading...'}

- +
+ {/* View toggle */} +
+ + +
+ +
{error && ( -
+
{error}
)} + {/* Filter */} {!loading && packages.length > 0 && ( -
- +
+ setFilter(e.target.value)} />
)} - + {/* Package List */} + {viewMode === 'grid' ? ( + + ) : ( + /* List view */ + loading ? ( +
+ {Array.from({ length: 8 }).map((_, i) => ( +
+
+
+
+
+
+ ))} +
+ ) : filtered.length === 0 ? ( +

No packages found

+ ) : ( +
+ {filtered.map((pkg, i) => ( +
window.location.href = `/package/${pkg.name}`}> + {pkg.name} + {pkg.version} + + {pkg.description || '—'} + + + {pkg.source === 'aur' ? 'AUR' : 'pacman'} + +
+ ))} +
+ ) + )}
); } diff --git a/frontend/src/pages/PackageView.jsx b/frontend/src/pages/PackageView.jsx index 734667d..38a7880 100644 --- a/frontend/src/pages/PackageView.jsx +++ b/frontend/src/pages/PackageView.jsx @@ -62,10 +62,10 @@ export default function PackageView() { if (error && !pkg) { return ( -
- -
-

Package not found

+
+ +
+

Package not found

{error}

@@ -78,35 +78,36 @@ export default function PackageView() { : 'var(--green)'; return ( -
- +
+
+ +
{error && ( -
+
{error}
)} {acting && ( -
-
-
+
+
+
Working...
-
             {log}
           
)} - {/* ═══ Main Info Card ═══ */} -
-
- {/* Left */} + {/* Main Info Card */} +
+
-
-

+
+

{pkg.name}

@@ -115,17 +116,16 @@ export default function PackageView() { {pkg.installed && Installed} {pkg.out_of_date && ( - Out of Date + Out of Date )}
-

+

{pkg.description || 'No description available.'}

- {/* Meta grid */} -
+
{pkg.source === 'aur' ? ( <> @@ -143,78 +143,70 @@ export default function PackageView() {
- {/* Right — Actions */} -
+ {/* Action column */} +
{pkg.installed ? ( - ) : ( - )} {pkg.url && ( - Website + className="btn btn-secondary w-full py-2 no-underline text-center justify-center flex items-center gap-1.5"> + Website )}

- {/* ═══ Security Scan (AUR only) ═══ */} + {/* Security Analysis */} {pkg.source === 'aur' && ( -
-

+

- - Security Scan -

+ + Security Scan (AUR PKGBUILD) + {scanning ? ( ) : scan?.scanned ? ( -
- {/* Score bar */} -
+
-
- {scan.risk_score >= 70 ? - : scan.risk_score >= 40 ? - : } -
-

- {scan.risk_level} -

-

PKGBUILD analysis

-
+
+ + {scan.risk_level} Risk +
- {scan.risk_score} - /100 + {scan.risk_score} + /100
- {/* Findings */} {scan.findings.filter(f => f.severity !== 'info').length > 0 ? ( -
+
{scan.findings.filter(f => f.severity !== 'info').map((f, i) => ( -
-
- {f.severity} - {f.line_number > 0 && L{f.line_number}} +
+ {f.severity} + {f.line_number > 0 && Line {f.line_number}}

{f.description}

{f.line_content && ( -
+                        
                           {f.line_content}
                         
)} @@ -222,45 +214,45 @@ export default function PackageView() { ))}
) : ( -
- No security issues detected. Safe to install. +
+ No security issues found in static PKGBUILD check.
)}
) : ( -

- Scan not available for this package. +

+ Scan not available.

)}
)} - {/* ═══ Metadata ═══ */} -
-
-

+
+

- Metadata + Package Metadata

-
+
{pkg.maintainer && } - {pkg.last_modified > 0 && } - {pkg.first_submitted > 0 && } - {pkg.package_base && } + {pkg.last_modified > 0 && } + {pkg.first_submitted > 0 && } + {pkg.package_base && }
-
-

+

- Dependencies + Dependency Handling

-
- - - Dependencies are automatically resolved by pacman/yay during installation. + + + Dependencies are automatically analyzed, resolved, and processed by standard package management operations during final deployment.
@@ -269,23 +261,22 @@ export default function PackageView() { ); } -/* ── Helper components ── */ function MetaBox({ label, value, mono, color, span }) { return ( -
-

{label}

-

{value}

+

{label}

+

{value || '—'}

); } function MetaRow({ label, value }) { return ( -
+
{label} - {value} + {value}
); } diff --git a/frontend/src/pages/Search.jsx b/frontend/src/pages/Search.jsx index e2553cc..4b6bf7e 100644 --- a/frontend/src/pages/Search.jsx +++ b/frontend/src/pages/Search.jsx @@ -18,17 +18,12 @@ export default function SearchPage() { }, [query, source]); async function performSearch(q, s) { - setLoading(true); - setError(null); + setLoading(true); setError(null); try { const data = await api.searchPackages(q, s); setResults(data.results || []); - } catch (err) { - setError(err.message); - setResults([]); - } finally { - setLoading(false); - } + } catch (err) { setError(err.message); setResults([]); } + finally { setLoading(false); } } const tabs = [ @@ -38,44 +33,42 @@ export default function SearchPage() { ]; return ( -
+
{/* Header */} -
-

Search Results

-

- {query ? <>Showing results for "{query}" : 'Enter a query to search packages'} -

+
+
+

Search Results

+

+ {query ? <>Results for "{query}" : 'Enter a query to search'} +

+
+ {query && ( +
+
+ {tabs.map((tab) => ( + + ))} +
+ + {results.length} pkg{results.length !== 1 ? 's' : ''} + +
+ )}
- {query && ( -
- {/* Source tabs */} -
- {tabs.map((tab) => ( - - ))} -
- -
- - {results.length} package{results.length !== 1 ? 's' : ''} -
-
- )} - {error && ( -
+
{error}
)} @@ -83,13 +76,13 @@ export default function SearchPage() { {query ? ( ) : ( -
-
+
- +
-

Start searching

-

Type a package name or keyword above

+

Start searching

+

Type a package name above

)}
diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index b1b6de4..12b9a3c 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -27,44 +27,44 @@ export default function Settings() { } return ( -
-
+
+

Settings

Configure ArchStore preferences

{msg && ( -
- + {msg.text}
)} -
+
{/* AUR Helper */} -
-

- +
+

+ AUR Helper

-

- The helper used to build and install AUR packages. +

+ The helper tool utilized to build and install AUR packages.

-
+
{['yay', 'paru'].map((t) => ( -
{t} - {t === 'yay' && Active}
))} @@ -72,36 +72,36 @@ export default function Settings() {
{/* Cache */} -
-

- +
+

+ Cache

-

- Search results and metadata are cached locally for 15 minutes. +

+ Search results and package metadata are cached locally to reduce API overhead.

-
{/* Health */} -
-

- +
+

+ Backend Status

-
FastAPI Server {health ? ( health.status === 'healthy' ? ( - - Online + + Online ) : ( - - Offline + + Offline ) ) : ( @@ -111,9 +111,9 @@ export default function Settings() {
{/* Footer */} -
+
- Made with for Arch Linux + Made with for Arch Linux ArchStore v1.0.0
diff --git a/frontend/src/pages/Updates.jsx b/frontend/src/pages/Updates.jsx index 2749893..91857d9 100644 --- a/frontend/src/pages/Updates.jsx +++ b/frontend/src/pages/Updates.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import api from '../api/client'; import LoadingSpinner from '../components/LoadingSpinner'; -import { RefreshCw, ArrowUpCircle, Info, CheckCircle2 } from 'lucide-react'; +import { RefreshCw, ArrowUpCircle, Info, CheckCircle2, ArrowRight } from 'lucide-react'; export default function Updates() { const [updates, setUpdates] = useState([]); @@ -32,86 +32,89 @@ export default function Updates() { } return ( -
-
+
+ {/* Header */} +

System Updates

Keep your system and AUR packages current

-
- {updates.length > 0 && ( - )}
{error && ( -
+
{error}
)} + {/* Upgrade Log */} {updating && ( -
-

- - Upgrading... -

-
+          
+ Upgrading... +
+
             {log}
           
)} + {/* Content */} {loading ? ( ) : updates.length === 0 ? ( -
-
- +
+
+
-

All up to date

-

No updates available right now

+

All up to date

+

No updates available right now

) : ( -
-
- +
+ {/* Info banner */} +
+
-

{updates.length} update{updates.length !== 1 ? 's' : ''} available

-

System administrator privileges required to install.

+ + {updates.length} update{updates.length !== 1 ? 's' : ''} available + + + Root privileges required. +
+ {/* Update list */}
{updates.map((u, i) => (
-
-
- {u.name} - - {u.source === 'aur' ? 'AUR' : 'pacman'} - -
-
- {u.current_version} - - {u.new_version} -
+ style={{ borderBottom: i < updates.length - 1 ? '1px solid var(--border-primary)' : 'none' }}> +
+ {u.name} + + {u.source === 'aur' ? 'AUR' : 'pacman'} + +
+
+ {u.current_version} + + {u.new_version}
))}