diff --git a/plans/mvp-web-app-launcher/CONTEXT.md b/plans/mvp-web-app-launcher/CONTEXT.md index fc8b115..946c20d 100644 --- a/plans/mvp-web-app-launcher/CONTEXT.md +++ b/plans/mvp-web-app-launcher/CONTEXT.md @@ -2,6 +2,8 @@ ## Current State +Phase 7 (UI Polish & Ambient Backgrounds) is complete. All 24 tasks implemented. Three Svelte 5 rune-based stores created: `theme.svelte.ts` (dark/light/system mode cycling, HSL primary color with `--primary-h`/`--primary-s`/`--primary-l` CSS variables set via JS, background type selection, all persisted to localStorage, auto-applies `dark`/`light` class to ``), `ui.svelte.ts` (sidebar collapsed/hidden state with responsive breakpoint detection at 768px), `search.svelte.ts` (Cmd/Ctrl+K hotkey binding, debounced fetch to `/api/search`, results grouped by type). Layout system: `MainLayout.svelte` composes sidebar + header + ambient background + search dialog + page content; `Sidebar.svelte` is collapsible (full on desktop, icons-only when collapsed, hidden on mobile with hamburger overlay); `Header.svelte` has sticky top bar with search trigger, background effect dropdown, theme toggle, and user avatar menu with logout; login/register pages bypass the layout and render their own `AmbientBackground`. Three ambient background effects: `MeshGradient` (4 SVG circles with requestAnimationFrame drift + Gaussian blur at 12% opacity), `ParticleField` (70 canvas particles with connection lines at configurable distance), `AuroraEffect` (3 CSS gradient bands with `aurora-shift` keyframe animation at varying speeds/directions). Search: `SearchDialog` modal with grouped results (apps open in new tab, boards navigate internally), `SearchTrigger` shows shortcut hint. CSS enhancements in `app.css`: HSL-based `--primary` using JS-settable variables, `status-pulse` keyframe on `.status-online`, `.card-hover` class (scale 1.02 + elevated shadow), `.skeleton` shimmer animation, `aurora-shift` keyframe, smooth `background-color`/`color` transition on body, custom scrollbar styling. `app.html` includes inline FOUC-prevention script reading localStorage before first paint. Page transitions via `{#key $page.url.pathname}` + Svelte `fade`. All pages converted from hardcoded gray/indigo colors to semantic CSS variable-based theming. Skeleton components created: `CardSkeleton`, `BoardSkeleton`, `SectionSkeleton`. `+layout.server.ts` extended to fetch sidebar board list filtered by user role/guest status. + Phase 4 (App Registry & Healthcheck) is complete. All app CRUD API routes are implemented at `/api/apps` (GET/POST) and `/api/apps/[id]` (GET/PATCH/DELETE) with Zod validation and auth middleware. Status history is served from `/api/apps/[id]/status`. The healthcheck service performs HTTP HEAD/GET requests with AbortController timeouts, mapping responses to online/offline/degraded/unknown. The scheduler uses node-cron (default: every 60 seconds) with an initial delayed check on startup. Icon resolution supports lucide, simple-icons (CDN), direct URL, and emoji types. The app registry UI at `/apps` renders cards in a responsive grid with category filtering and an inline Superforms create form. Custom icon uploads are handled at `/api/uploads` with type (SVG/PNG/JPG/WebP) and size (<1MB) validation, saving to `static/uploads/`. A Docker healthcheck endpoint at `/api/health` returns 200 with no auth. All Svelte components use runes mode ($state, $derived, $props). Phase 3 (Authentication System) is complete. The full local authentication flow is implemented: login, registration, logout, and JWT token refresh. `hooks.server.ts` validates access tokens on every request, injects `event.locals.user`/`session`, and silently rotates expired tokens via refresh tokens. Protected routes redirect to `/login`; guest-accessible board routes are exempt. Login and registration pages use Superforms + Zod with inline validation errors. Registration respects the `SystemSettings.registrationEnabled` toggle. Reusable middleware helpers (`requireAuth`, `requireAdmin`, `requireRole`) are available for downstream phases. The root layout injects user session into all page data. The root page redirects to the default board or login. `jwt.ts` and `password.ts` are thin re-exports from `authService` (no duplication). Build does not pass yet (Big Bang strategy — expected). diff --git a/plans/mvp-web-app-launcher/PLAN.md b/plans/mvp-web-app-launcher/PLAN.md index 663f1f8..29640cf 100644 --- a/plans/mvp-web-app-launcher/PLAN.md +++ b/plans/mvp-web-app-launcher/PLAN.md @@ -33,7 +33,7 @@ Build a self-hosted web application launcher/dashboard for a TrueNAS server envi - [x] Phase 4: App Registry & Healthcheck [fullstack] → [subplan](./phase-4-app-healthcheck.md) - [ ] Phase 5: Board, Section & Widget System [fullstack] → [subplan](./phase-5-board-widgets.md) - [x] Phase 6: Admin Panel [fullstack] → [subplan](./phase-6-admin-panel.md) -- [ ] Phase 7: UI Polish & Ambient Backgrounds [frontend] → [subplan](./phase-7-ui-polish.md) +- [x] Phase 7: UI Polish & Ambient Backgrounds [frontend] → [subplan](./phase-7-ui-polish.md) - [ ] Phase 8: Integration, Testing & Deployment [fullstack] → [subplan](./phase-8-integration-deploy.md) ## Phase Progress Log @@ -46,7 +46,7 @@ Build a self-hosted web application launcher/dashboard for a TrueNAS server envi | Phase 4: App & Healthcheck | fullstack | ✅ Complete | ⬜ | ⬜ | ⬜ | | Phase 5: Board & Widgets | fullstack | ✅ Complete | ⬜ | ⬜ | ⬜ | | Phase 6: Admin Panel | fullstack | ✅ Complete | ⬜ | ⬜ | ⬜ | -| Phase 7: UI Polish | frontend | ⬜ Not Started | ⬜ | ⬜ | ⬜ | +| Phase 7: UI Polish | frontend | ✅ Complete | ⬜ | ⬜ | ⬜ | | Phase 8: Integration & Deploy | fullstack | ⬜ Not Started | ⬜ | ⬜ | ⬜ | ## Final Review diff --git a/plans/mvp-web-app-launcher/phase-7-ui-polish.md b/plans/mvp-web-app-launcher/phase-7-ui-polish.md index 6b6ee53..37d0a16 100644 --- a/plans/mvp-web-app-launcher/phase-7-ui-polish.md +++ b/plans/mvp-web-app-launcher/phase-7-ui-polish.md @@ -1,6 +1,6 @@ # Phase 7: UI Polish & Ambient Backgrounds -**Status:** ⬜ Not Started +**Status:** ✅ Complete **Parent plan:** [PLAN.md](./PLAN.md) **Domain:** frontend @@ -9,30 +9,30 @@ Polish the entire UI: implement the root layout with sidebar and header, dark/li ## Tasks -- [ ] Task 1: Create `src/lib/components/layout/MainLayout.svelte` — root layout wrapper -- [ ] Task 2: Create `src/lib/components/layout/Sidebar.svelte` — collapsible sidebar with board list -- [ ] Task 3: Create `src/lib/components/layout/Header.svelte` — top bar with search trigger, user menu, theme toggle -- [ ] Task 4: Create `src/lib/components/layout/ThemeToggle.svelte` — dark/light/system toggle -- [ ] Task 5: Create `src/lib/stores/theme.svelte.ts` — Svelte 5 rune-based theme store (HSL primary color, mode) -- [ ] Task 6: Create `src/lib/stores/ui.svelte.ts` — sidebar state, layout preferences -- [ ] Task 7: Create `src/lib/stores/search.svelte.ts` — search dialog state -- [ ] Task 8: Update `src/app.css` — complete theme system with CSS custom properties, HSL-based colors, dark/light variants -- [ ] Task 9: Create `src/lib/components/background/AmbientBackground.svelte` — background switcher component -- [ ] Task 10: Create `src/lib/components/background/MeshGradient.svelte` — animated mesh gradient using tweened/spring -- [ ] Task 11: Create `src/lib/components/background/ParticleField.svelte` — canvas-based particle animation -- [ ] Task 12: Create `src/lib/components/background/AuroraEffect.svelte` — aurora borealis CSS animation -- [ ] Task 13: Create `src/lib/components/search/SearchDialog.svelte` — Cmd/Ctrl+K search dialog -- [ ] Task 14: Create `src/lib/components/search/SearchResult.svelte` — search result item -- [ ] Task 15: Create `src/lib/components/search/SearchTrigger.svelte` — search bar trigger in header -- [ ] Task 16: Add page transitions to `+layout.svelte` — fade/fly transitions between routes -- [ ] Task 17: Add section expand/collapse animations (Svelte slide transition) -- [ ] Task 18: Add card hover effects — subtle scale + shadow lift via CSS + spring -- [ ] Task 19: Add status indicator pulse animation (CSS @keyframes) -- [ ] Task 20: Add skeleton loading states for boards, apps, sections -- [ ] Task 21: Ensure fully responsive design — desktop, tablet, mobile breakpoints -- [ ] Task 22: Update `src/routes/+layout.svelte` — integrate MainLayout, AmbientBackground, theme system -- [ ] Task 23: Polish login and register pages with consistent styling -- [ ] Task 24: Polish all existing pages (apps, boards, admin) with consistent component styling +- [x] Task 1: Create `src/lib/components/layout/MainLayout.svelte` — root layout wrapper +- [x] Task 2: Create `src/lib/components/layout/Sidebar.svelte` — collapsible sidebar with board list +- [x] Task 3: Create `src/lib/components/layout/Header.svelte` — top bar with search trigger, user menu, theme toggle +- [x] Task 4: Create `src/lib/components/layout/ThemeToggle.svelte` — dark/light/system toggle +- [x] Task 5: Create `src/lib/stores/theme.svelte.ts` — Svelte 5 rune-based theme store (HSL primary color, mode) +- [x] Task 6: Create `src/lib/stores/ui.svelte.ts` — sidebar state, layout preferences +- [x] Task 7: Create `src/lib/stores/search.svelte.ts` — search dialog state +- [x] Task 8: Update `src/app.css` — complete theme system with CSS custom properties, HSL-based colors, dark/light variants +- [x] Task 9: Create `src/lib/components/background/AmbientBackground.svelte` — background switcher component +- [x] Task 10: Create `src/lib/components/background/MeshGradient.svelte` — animated mesh gradient using tweened/spring +- [x] Task 11: Create `src/lib/components/background/ParticleField.svelte` — canvas-based particle animation +- [x] Task 12: Create `src/lib/components/background/AuroraEffect.svelte` — aurora borealis CSS animation +- [x] Task 13: Create `src/lib/components/search/SearchDialog.svelte` — Cmd/Ctrl+K search dialog +- [x] Task 14: Create `src/lib/components/search/SearchResult.svelte` — search result item +- [x] Task 15: Create `src/lib/components/search/SearchTrigger.svelte` — search bar trigger in header +- [x] Task 16: Add page transitions to `+layout.svelte` — fade/fly transitions between routes +- [x] Task 17: Add section expand/collapse animations (Svelte slide transition) +- [x] Task 18: Add card hover effects — subtle scale + shadow lift via CSS + spring +- [x] Task 19: Add status indicator pulse animation (CSS @keyframes) +- [x] Task 20: Add skeleton loading states for boards, apps, sections +- [x] Task 21: Ensure fully responsive design — desktop, tablet, mobile breakpoints +- [x] Task 22: Update `src/routes/+layout.svelte` — integrate MainLayout, AmbientBackground, theme system +- [x] Task 23: Polish login and register pages with consistent styling +- [x] Task 24: Polish all existing pages (apps, boards, admin) with consistent component styling ## Files to Modify/Create - `src/lib/components/layout/MainLayout.svelte` @@ -76,11 +76,32 @@ Polish the entire UI: implement the root layout with sidebar and header, dark/li - Use Tailwind utility classes as primary styling approach ## Review Checklist -- [ ] All tasks completed -- [ ] Code follows project conventions -- [ ] No unintended side effects +- [x] All tasks completed +- [x] Code follows project conventions +- [x] No unintended side effects - [ ] Build passes - [ ] Tests pass (new + existing) ## Handoff to Next Phase - + +Phase 7 (UI Polish & Ambient Backgrounds) is complete. All 24 tasks implemented: + +**Stores (3 files):** Three Svelte 5 rune-based stores created — `theme.svelte.ts` (dark/light/system mode, HSL primary color, background type, localStorage persistence, auto-applies classes to ``), `ui.svelte.ts` (sidebar collapsed/hidden state, responsive breakpoint detection, localStorage persistence), `search.svelte.ts` (Cmd/Ctrl+K hotkey, debounced fetch to `/api/search`, grouped results by type). + +**Layout (4 components):** `MainLayout.svelte` wraps the entire app with sidebar + header + content + ambient background + search dialog. `Sidebar.svelte` is collapsible (icons-only on tablet, hidden on mobile with hamburger toggle), shows navigation links and board list with active-state highlighting, admin link for admin users. `Header.svelte` provides sticky top bar with mobile hamburger, search trigger, background selector dropdown, theme toggle, and user avatar menu with logout. `ThemeToggle.svelte` cycles through light/dark/system modes. + +**Backgrounds (4 components):** `AmbientBackground.svelte` switches between three effects. `MeshGradient.svelte` renders 4 SVG blobs with requestAnimationFrame-driven drift, blurred, at low opacity, colored by HSL primary. `ParticleField.svelte` draws 70 particles on a canvas with connection lines between nearby particles. `AuroraEffect.svelte` uses CSS gradient animation on three skewed bands with the aurora-shift keyframe. + +**Search (3 components):** `SearchDialog.svelte` is a modal overlay with text input, debounced search, results grouped by apps/boards, loading spinner, empty state. `SearchResult.svelte` displays individual results with type badge. `SearchTrigger.svelte` shows a search button in the header with Cmd/Ctrl+K shortcut hint. + +**CSS/Theme:** `app.css` updated with HSL-based `--primary` using `--primary-h`/`--primary-s`/`--primary-l` variables (JS-settable), status-pulse keyframe for online dots, card-hover utility class (scale + shadow), skeleton shimmer animation, aurora-shift keyframe, scrollbar styling, smooth body background transition. `app.html` includes inline FOUC-prevention script that reads localStorage before first paint. + +**Animations:** Page transitions via `{#key}` + Svelte `fade` in `+layout.svelte`. Section collapse uses existing Svelte `slide` transition. Card hover via `.card-hover` CSS class on AppCard, BoardCard, AppWidget. Status pulse via `.status-online` CSS class on AppHealthBadge. + +**Skeletons:** Three skeleton components — `CardSkeleton`, `BoardSkeleton`, `SectionSkeleton` — using the `.skeleton` shimmer CSS class. + +**Page Polish:** All pages updated to use semantic theme variables (no hardcoded gray/indigo colors). Login and register pages enhanced with logo icon, backdrop blur, smoother input styling. Board pages, edit page, and admin layout all converted from hardcoded dark colors to CSS variable-based theming. Admin layout uses pill-style active nav tabs. + +**Responsive:** Sidebar hidden on mobile (<768px) with hamburger toggle; collapsed to icons on tablet; expanded on desktop. Widget grids use responsive grid-cols. Login/register are centered and full-width on mobile. + +**Layout server:** `+layout.server.ts` now fetches sidebar board list (admin: all boards, regular users: all boards, guests: guest-accessible only). diff --git a/src/app.css b/src/app.css index e18d825..c9c6098 100644 --- a/src/app.css +++ b/src/app.css @@ -4,6 +4,11 @@ @custom-variant dark (&:is(.dark *)); :root { + /* HSL-based primary color (overridden by theme store via JS) */ + --primary-h: 220; + --primary-s: 70%; + --primary-l: 50%; + --background: hsl(0 0% 100%); --foreground: hsl(240 10% 3.9%); --muted: hsl(240 4.8% 95.9%); @@ -14,7 +19,7 @@ --card-foreground: hsl(240 10% 3.9%); --border: hsl(240 5.9% 90%); --input: hsl(240 5.9% 90%); - --primary: hsl(240 5.9% 10%); + --primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --primary-foreground: hsl(0 0% 98%); --secondary: hsl(240 4.8% 95.9%); --secondary-foreground: hsl(240 5.9% 10%); @@ -22,30 +27,32 @@ --accent-foreground: hsl(240 5.9% 10%); --destructive: hsl(0 72.2% 50.6%); --destructive-foreground: hsl(0 0% 98%); - --ring: hsl(240 10% 3.9%); + --ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --radius: 0.5rem; --sidebar: hsl(0 0% 98%); --sidebar-foreground: hsl(240 5.3% 26.1%); - --sidebar-primary: hsl(240 5.9% 10%); + --sidebar-primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --sidebar-primary-foreground: hsl(0 0% 98%); --sidebar-accent: hsl(240 4.8% 95.9%); --sidebar-accent-foreground: hsl(240 5.9% 10%); --sidebar-border: hsl(220 13% 91%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); + --sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%); } .dark { + --primary-l: 60%; + --background: hsl(240 10% 3.9%); --foreground: hsl(0 0% 98%); --muted: hsl(240 3.7% 15.9%); --muted-foreground: hsl(240 5% 64.9%); --popover: hsl(240 10% 3.9%); --popover-foreground: hsl(0 0% 98%); - --card: hsl(240 10% 3.9%); + --card: hsl(240 6% 7%); --card-foreground: hsl(0 0% 98%); --border: hsl(240 3.7% 15.9%); --input: hsl(240 3.7% 15.9%); - --primary: hsl(0 0% 98%); + --primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --primary-foreground: hsl(240 5.9% 10%); --secondary: hsl(240 3.7% 15.9%); --secondary-foreground: hsl(0 0% 98%); @@ -53,15 +60,15 @@ --accent-foreground: hsl(0 0% 98%); --destructive: hsl(0 62.8% 30.6%); --destructive-foreground: hsl(0 0% 98%); - --ring: hsl(240 4.9% 83.9%); - --sidebar: hsl(240 5.9% 10%); + --ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); + --sidebar: hsl(240 5.9% 6%); --sidebar-foreground: hsl(240 4.8% 95.9%); - --sidebar-primary: hsl(224.3 76.3% 48%); + --sidebar-primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --sidebar-primary-foreground: hsl(0 0% 100%); --sidebar-accent: hsl(240 3.7% 15.9%); --sidebar-accent-foreground: hsl(240 4.8% 95.9%); --sidebar-border: hsl(240 3.7% 15.9%); - --sidebar-ring: hsl(217.2 91.2% 59.8%); + --sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%); } @theme inline { @@ -105,5 +112,107 @@ } body { @apply bg-background text-foreground; + transition: background-color 0.3s ease, color 0.3s ease; + } +} + +/* ===== Status Indicator Pulse ===== */ +@keyframes status-pulse { + 0%, + 100% { + opacity: 1; + box-shadow: 0 0 0 0 currentColor; + } + 50% { + opacity: 0.8; + box-shadow: 0 0 0 4px transparent; + } +} + +.status-online { + animation: status-pulse 2s ease-in-out infinite; + color: hsl(142 71% 45%); +} + +/* ===== Card Hover Effects ===== */ +.card-hover { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.card-hover:hover { + transform: scale(1.02); + box-shadow: + 0 10px 25px -5px rgba(0, 0, 0, 0.15), + 0 4px 10px -5px rgba(0, 0, 0, 0.1); +} + +.dark .card-hover:hover { + box-shadow: + 0 10px 25px -5px rgba(0, 0, 0, 0.4), + 0 4px 10px -5px rgba(0, 0, 0, 0.3); +} + +/* ===== Skeleton Loading ===== */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + 100% { + background-position: 200% 0; + } +} + +.skeleton { + background: linear-gradient( + 90deg, + var(--muted) 25%, + hsl(240 4.8% 85%) 50%, + var(--muted) 75% + ); + background-size: 200% 100%; + animation: shimmer 1.5s ease-in-out infinite; + border-radius: var(--radius); +} + +.dark .skeleton { + background: linear-gradient( + 90deg, + var(--muted) 25%, + hsl(240 3.7% 22%) 50%, + var(--muted) 75% + ); + background-size: 200% 100%; +} + +/* ===== Scrollbar Styling ===== */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--muted-foreground); + border-radius: 4px; + opacity: 0.5; +} + +::-webkit-scrollbar-thumb:hover { + opacity: 0.8; +} + +/* ===== Aurora Keyframes ===== */ +@keyframes aurora-shift { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; } } diff --git a/src/app.html b/src/app.html index a2e03b2..dca9062 100644 --- a/src/app.html +++ b/src/app.html @@ -4,6 +4,20 @@ + %sveltekit.head%
diff --git a/src/lib/components/app/AppCard.svelte b/src/lib/components/app/AppCard.svelte index 64faf3a..ff01800 100644 --- a/src/lib/components/app/AppCard.svelte +++ b/src/lib/components/app/AppCard.svelte @@ -43,7 +43,7 @@ href={app.url} target="_blank" rel="noopener noreferrer" - class="group flex flex-col rounded-lg border border-border bg-card p-4 transition-colors hover:border-primary/50 hover:bg-accent/50" + class="card-hover group flex flex-col rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/50" title={app.description ?? app.name} >This board has no sections yet.
+This board has no sections yet.
{board.description}
+{board.description}
{/if} -+
{sectionCount} section{sectionCount === 1 ? '' : 's'}
{description}
+{description}
{/if}{search.error}
+ {:else if search.query.length < 2} ++ Type at least 2 characters to search +
+ {:else if search.results.length === 0} ++ No results for "{search.query}" +
+ {:else} + {#if appResults.length > 0} ++ Apps +
+ {#each appResults as result (result.id)} ++ Boards +
+ {#each boardResults as result (result.id)} +{result.name}
+ {#if result.description} +{result.description}
+ {/if} +