diff --git a/ui/docs/design-decisions.md b/ui/docs/design-decisions.md index af84664e..605568b0 100644 --- a/ui/docs/design-decisions.md +++ b/ui/docs/design-decisions.md @@ -13,6 +13,27 @@ The 6 open questions from the brainstorm phase, resolved before implementation. --- +## v1.1 revision (2026-04-25) — identity-strip patterns + +Phase 2 attempted to migrate `home` and `cliproxy` to a one-size-fits-all `PageShell + PageHeader` chrome. Both regressed on density: + +- **home** had a single-row hero (logo + title + version + 4 inline stats) — splitting it into a stacked PageHeader + KpiRow doubled vertical footprint and lost scannability +- **cliproxy** had identity in the left rail — adding a top PageHeader duplicated branding and stole ~80px from the 3-pane body + +**Resolution:** the design system is restructured around three identity-strip patterns extracted from the existing canonical references: + +| # | Pattern | Reference | When | +|---|---------|-----------|------| +| 7a | `HeroBar` (1-row dense) | `pages/home.tsx` | Dashboard pages with ≤4 hero stats | +| 7b | Rail-anchored identity (no top chrome) | `pages/cliproxy.tsx` | Multi-entity Config pages where rail carries brand | +| 7c | `PageHeader` (current) | `pages/health.tsx` | Pages where description / status info is non-redundant | + +Phase 2's home + cliproxy migrations are reverted. Health stays migrated (Monitor archetype + PageHeader works there). Future page migrations adapt to whichever pattern fits, NOT the other way around. + +**Why bottom-up:** the existing references already proved their patterns work in production. The job of the design system is to formalize what works, not impose what should. + +--- + ## How to revisit If a decision turns out wrong in practice, update this doc and bump the affected primitive — don't silently drift. Each row above should be appended with a "Revised: · " line if changed. diff --git a/ui/docs/design-system-phase2-preview.html b/ui/docs/design-system-phase2-preview.html index d728a858..92292b29 100644 --- a/ui/docs/design-system-phase2-preview.html +++ b/ui/docs/design-system-phase2-preview.html @@ -42,21 +42,21 @@ th{color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;le

Hero stat tiles → KpiRow. AuthMonitor + ErrorLogsMonitor wrapped as MonitorCards. Logs callout preserved.

-
+

2 · cliproxy — Config archetype (multi-entity, ListPane)

Master-detail layout wrapped in PageShell + PageHeader + ConfigLayout. Brand strip moved into PageHeader; sidebar keeps the existing ProviderSidebarItem / VariantSidebarItem composition + ProxyStatusWidget. ProviderEditor unchanged. All sub-flows preserved (Quick Setup wizard, Add Account dialog, Account Safety warning).

-
+

3 · health — Monitor archetype + visual improvements

HealthGauge becomes the hero inside a MonitorCard variant="terminal". HealthStatsBar replaced by KpiRow (Checks · Passed · Warnings · Errors) — clear severity hierarchy. Each health group becomes its own MonitorCard. Terminal aesthetic preserved per the locked decision (#4).

-
+

4 · LOC delta

diff --git a/ui/docs/design-system.md b/ui/docs/design-system.md index edae68af..932c84cf 100644 --- a/ui/docs/design-system.md +++ b/ui/docs/design-system.md @@ -1,36 +1,102 @@ # CCS Dashboard Design System -The dashboard ships **two page archetypes**. Every page picks exactly one. All pages share the same outer chrome via `PageShell` + `PageHeader`. +A page-level design system extracted from the **canonical reference pages** — `home` and `cliproxy` — that already prove the patterns work in production. New pages should adapt to these references, not the other way around. + +Health is the second-tier reference for pages whose content shape (gauge + KPIs + group accordions) doesn't fit either canonical hero — it uses the `MonitorLayout` archetype. > Live preview in dev: `bun run dev` then visit `/_styleguide`. --- -## 1. The shell — every page +## 1. Identity-strip patterns (pick one per page) + +Three patterns cover every page in the dashboard. The choice depends on what your page already has. + +### 1a. `HeroBar` — single-row dense hero + +**Canonical reference:** `pages/home.tsx` ``` -PageShell -├─ PageHeader (title · description · status · actions) -└─ +┌────────────────────────────────────────────────────────────────────┐ +│ [logo] Title [version] ┃ [Stat] [Stat] [Stat] [Stat] │ +└────────────────────────────────────────────────────────────────────┘ ``` -```tsx - - - {/* OR */} - +One row packs logo + title + version + ≤4 inline stats. Optional subtle dotted-pattern background. Stats are clickable when they double as navigation entry points. + +**Use it when:** +- The page is a dashboard / monitor with a clear product identity +- ≤4 hero stats summarize the page in numbers +- Vertical real estate matters (this is half the height of a stacked PageHeader + KpiRow) + +**Building blocks:** +- `` — logo + title + subtitle from `components/layout/hero-section.tsx` +- `` — clickable stat tile (extracted from `home.tsx`); promote to a shared primitive when a 2nd page adopts it + +### 1b. Rail-anchored identity — no top chrome + +**Canonical reference:** `pages/cliproxy.tsx` + +``` +┌──────────┬─────────────────────────────────────────────────────────┐ +│ ⚡ Brand │ │ +│ subtitle │ │ +│ [QSetup] │ full-height 3-pane body │ +│ │ │ +│ • prov A │ (form + raw json fill the entire viewport) │ +│ • prov B │ │ +│ … │ │ +│ [status] │ │ +└──────────┴─────────────────────────────────────────────────────────┘ ``` -Header rules: -- `title` is mandatory; `description` optional 1-line subtitle. -- `status` slot: small badges/chips (e.g. "Running · :8317"). -- `actions` slot: button group; **primary action belongs in FormPane footer**, not here, for Config pages. +Page identity (brand + page-level CTA + status) lives **inside the left rail**. Zero top chrome — the body archetype gets the full vertical viewport. + +**Use it when:** +- The page is a multi-entity Config (3-pane: list / form / json) +- The rail naturally carries the page name (you'd duplicate it in a top header) +- Vertical real estate is at a premium because the body has dense form content + +**Building blocks:** +- The left rail's own header section (in-place markup, no extracted primitive yet — keep it bespoke until a 2nd page adopts the pattern) +- Recommended order in the rail: brand strip → primary CTA → entity list → status widget → footer summary + +### 1c. `PageHeader` — title-row chrome + +**Canonical reference:** `pages/health.tsx` + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ Title [v-badge] [action] [action] │ +│ Description / last-update / status info │ +└────────────────────────────────────────────────────────────────────┘ +``` + +Traditional title row with description and trailing actions. + +**Use it when:** +- The page does NOT fit either canonical hero +- The description carries genuinely non-redundant context (last refresh, page hierarchy, filter state, version) +- Body archetype below benefits from a clear identity strip + +**API:** `` — title + description on left, status badges + action buttons on right. + +### Decision table + +| Page shape | Identity strip | +|------------|---------------| +| Dashboard / overview with ≤4 hero stats | **HeroBar** (home pattern) | +| Multi-entity Config (3-pane: list/form/json) | **Rail-anchored** (cliproxy pattern, no top chrome) | +| Single-entity Config OR Monitor with a real hero viz | **PageHeader** + body archetype | +| Wizard / login / dialog | None — bespoke shell | --- -## 2. Config archetype — 3-pane +## 2. Body archetypes -Used by every page that configures one or more entities (cliproxy, accounts, codex, copilot, cursor, droid, claude-extension, api, shared, profiles). +### 2a. Config — 3-pane + +**Canonical reference:** `pages/cliproxy.tsx` ``` ┌──────────┬──────────────────┬──────────┐ @@ -39,141 +105,74 @@ Used by every page that configures one or more entities (cliproxy, accounts, cod └──────────┴──────────────────┴──────────┘ ``` -One component, prop-controlled left rail: +Left rail = `ListPane` (multi-entity) or `SectionRail` (single-entity, with `IntersectionObserver` scroll-spy). Form and JSON panes are middle and right respectively. ```tsx -// Multi-entity (cliproxy, accounts, providers) } + left={} // multi-entity + // OR + left={} // single-entity form={} json={} /> - -// Single-entity (codex, copilot, cursor, droid, claude-extension) -} - form={} - json={} -/> - -// No rail (rare — only if page has neither entities to pick nor sections to navigate) -…} json={} /> ``` -### Rules +**Rules:** +- Save action lives **only** in `FormPane` footer +- `<1024px`: collapses to tabs (Browse | Configure | JSON) +- `JsonPane` is read-only by default; opt-in `editable` for cliproxy-style inline editing -- **Layout**: left 260px / form flex / json 360px on `>=1024px`. -- **<1024px**: collapses to tabs (`Browse | Configure | JSON`). -- **Save action**: lives **only** in `FormPane footer`. Never duplicate in PageHeader. -- **Per-entity actions** (delete, duplicate, sync): ListPane row trailing actions or FormPane header secondary actions — never both. -- **JsonPane is read-only by default**. Pass `editable` prop only on pages that need inline editing (cliproxy is the canonical example). +### 2b. Monitor — KPI row + 12-col grid -### SectionRail - -For single-entity pages. Provides anchor nav with `IntersectionObserver` scroll-spy. Each item must match the `id` of a `` in the FormPane. - -```tsx - - -// FormPane: - - -``` - ---- - -## 3. Monitor archetype — KPI row + 12-col grid - -Used by every page that shows live state (home, analytics, health, logs, future status pages). +**Canonical reference:** `pages/health.tsx` ``` -┌────────────────────────────────────────────────┐ -│ KpiRow: [KPI] [KPI] [KPI] [KPI] │ (optional, ≤4 hero numbers) -├────────────────────────────────────────────────┤ -│ MonitorGrid (12-col): │ -│ primary viz │ -│ side widget │ -│ ... │ -│ ... │ -└────────────────────────────────────────────────┘ +┌────────────────────────────────────────┐ +│ KpiRow (≤4 hero numbers) │ +├────────────────────────────────────────┤ +│ MonitorGrid (12-col): │ +│ │ +└────────────────────────────────────────┘ ``` ```tsx - - - … - - } -> +…}> - - - - + ``` -### Rules +**Rules:** +- `KpiRow` only when ≤4 hero numbers; more → group inside the grid +- One primary viz per page, span ≥8 cols +- `variant="terminal"` for live-log / `health --watch` aesthetics -- **KpiRow** only when there are ≤4 hero numbers. More than 4 → use `MonitorCard`s inside the grid instead. -- **One primary viz per page** — span ≥8 cols. Preserves the "punch" of home and analytics. -- **`variant="terminal"`** = dark monospace card, for live-log / `health --watch` aesthetics. Opt-in only. -- **Spans clamp at 6 cols on tablet**, single column on mobile. +--- + +## 3. Composing a new page + +```tsx +// Example: a new dashboard-style page + + {/* or PageHeader, or rail-anchored identity */} + {/* or ConfigLayout */} + +``` + +Target LOC for a new page: **~80** for typical config, **~120** for monitor with hero strip. Target LOC for an outlier rewrite: **<400**. --- ## 4. When NOT to use either archetype -These remain bespoke: -- `/login` — minimal centered shell, no header/grid. -- Setup wizard — modal overlay, not a page. -- Dialogs — radix `Dialog`, not a layout. - -If you find yourself fighting the shell, the answer is usually "this isn't a Config or Monitor page" — talk to the maintainers before inventing a third archetype. +These remain bespoke and are out of scope: +- `/login` — minimal centered shell +- Setup wizard — modal overlay +- Dialogs — Radix `Dialog` --- -## 5. Composing a new page +## 5. Decisions -The recipe for any new page is: - -```tsx -import { PageShell, PageHeader } from '@/components/page-shell'; -import { ConfigLayout, /*…*/ } from '@/components/config-layout'; - -export function MyPage() { - return ( - - } /> - …} - json={} - /> - - ); -} -``` - -Target LOC for a new page: **~80** for typical config, **~120** for monitor. If your page exceeds 400 LOC it should be split into `pages//` with section files. - ---- - -## 6. Lint / enforcement - -Phase 4 adds a lint rule: every file under `src/pages/*.tsx` (or `src/pages/*/index.tsx`) must import `PageShell`. Until then, code review enforces the contract. - ---- - -## 7. Decisions - -See [`design-decisions.md`](./design-decisions.md) for the resolutions of the 6 open questions from the brainstorm phase. +See [`design-decisions.md`](./design-decisions.md) for the resolved open questions and the v1.1 revision rationale. diff --git a/ui/docs/screenshots-phase2/after-01-home.png b/ui/docs/screenshots-phase2/after-01-home.png index e1f42a01..060572c3 100644 Binary files a/ui/docs/screenshots-phase2/after-01-home.png and b/ui/docs/screenshots-phase2/after-01-home.png differ diff --git a/ui/docs/screenshots-phase2/after-02-cliproxy.png b/ui/docs/screenshots-phase2/after-02-cliproxy.png index be9198e5..fe5d077e 100644 Binary files a/ui/docs/screenshots-phase2/after-02-cliproxy.png and b/ui/docs/screenshots-phase2/after-02-cliproxy.png differ diff --git a/ui/docs/screenshots-phase2/after-03-health.png b/ui/docs/screenshots-phase2/after-03-health.png index 52ae20ed..f0c64b7b 100644 Binary files a/ui/docs/screenshots-phase2/after-03-health.png and b/ui/docs/screenshots-phase2/after-03-health.png differ diff --git a/ui/src/pages/_styleguide.tsx b/ui/src/pages/_styleguide.tsx index 349f3505..53c36f35 100644 --- a/ui/src/pages/_styleguide.tsx +++ b/ui/src/pages/_styleguide.tsx @@ -121,9 +121,15 @@ function Intro() {

CCS Dashboard Design System

- Two archetypes — Config (3-pane: list | form | JSON) and{' '} - Monitor (KPI row + grid) — wrapped by PageShell. Every page - picks one archetype. + Three identity-strip patterns — HeroBar (one-row dense, see /{' '} + home page), rail-anchored (no top chrome, see /cliproxy), and{' '} + PageHeader (title + description + actions, see /health) — and + two body archetypes: Config (3-pane: list / form / JSON) and{' '} + Monitor (KPI row + grid). Pick the identity strip that matches your + page's shape; pick the body archetype your content needs. +

+

+ See ui/docs/design-system.md for the decision table.

); diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 6b8bbfa4..962591e2 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -11,8 +11,6 @@ import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; import { Skeleton } from '@/components/ui/skeleton'; import { Check, X, RefreshCw, Sparkles, Zap, GitBranch, Trash2 } from 'lucide-react'; -import { PageShell, PageHeader } from '@/components/page-shell'; -import { ConfigLayout } from '@/components/config-layout'; import { QuickSetupWizard } from '@/components/quick-setup-wizard'; import { AddAccountDialog } from '@/components/account/add-account-dialog'; import { AccountSafetyWarningCard } from '@/components/account/account-safety-warning-card'; @@ -354,238 +352,234 @@ export function CliproxyPage() { setSelectedProvider(null); }; - const sidebar = ( -
- {/* Header inside the rail: Quick Setup CTA */} -
- -
- - {/* Providers List */} - -
-
- {t('cliproxyPage.providers')} + return ( +
+ {/* Left Sidebar */} +
+ {/* Header */} +
+
+
+ +

{updateCheck?.backendLabel ?? 'CLIProxy'}

+
+
- {authLoading ? ( -
- {[1, 2, 3, 4].map((i) => ( - - ))} -
- ) : ( -
- {providerSections.map((section) => ( -
-
-
- {t(section.labelKey)} -
-

- {t(section.hintKey)} -

-
-
- {section.items.map((status) => ( - handleSelectProvider(status.provider)} - /> - ))} -
-
- ))} -
- )} +

+ {t('cliproxyPage.accountManagement')} +

- {/* Variants Section */} - {variants.length > 0 && ( - <> -
- - {t('cliproxyPage.variants')} -
-
- {variants.map((variant) => ( - p.provider === variant.provider)} - isSelected={selectedVariant === variant.name} - onSelect={() => handleSelectVariant(variant.name)} - onDelete={() => deleteMutation.mutate(variant.name)} - isDeleting={deleteMutation.isPending} - /> + +
+ + {/* Providers List */} + +
+
+ {t('cliproxyPage.providers')} +
+ {authLoading ? ( +
+ {[1, 2, 3, 4].map((i) => ( + ))}
- - )} + ) : ( +
+ {providerSections.map((section) => ( +
+
+
+ {t(section.labelKey)} +
+

+ {t(section.hintKey)} +

+
+
+ {section.items.map((status) => ( + handleSelectProvider(status.provider)} + /> + ))} +
+
+ ))} +
+ )} + + {/* Variants Section */} + {variants.length > 0 && ( + <> +
+ + {t('cliproxyPage.variants')} +
+
+ {variants.map((variant) => ( + p.provider === variant.provider)} + isSelected={selectedVariant === variant.name} + onSelect={() => handleSelectVariant(variant.name)} + onDelete={() => deleteMutation.mutate(variant.name)} + isDeleting={deleteMutation.isPending} + /> + ))} +
+ + )} +
+
+ + {/* Proxy Status Widget */} +
+
- - {/* Proxy Status Widget */} -
- -
- - {/* Footer Stats */} -
-
- {t('cliproxyPage.providerCount', { count: providers.length })} - - - {t('cliproxyPage.connectedCount', { - count: providers.filter((p) => p.authenticated).length, - })} - + {/* Footer Stats */} +
+
+ {t('cliproxyPage.providerCount', { count: providers.length })} + + + {t('cliproxyPage.connectedCount', { + count: providers.filter((p) => p.authenticated).length, + })} + +
-
- ); - const detail = ( -
- {selectedVariantData && parentAuthForVariant ? ( - <> - - ) : undefined - } - onAddAccount={() => - setAddAccountProvider({ + {/* Right Panel */} +
+ {selectedVariantData && parentAuthForVariant ? ( + <> + - setDefaultMutation.mutate({ - provider: selectedVariantData.provider, - accountId, - }) - } - onRemoveAccount={(accountId) => - removeMutation.mutate({ - provider: selectedVariantData.provider, - accountId, - }) - } - onPauseToggle={(accountId, paused) => - handlePauseToggle(selectedVariantData.provider, accountId, paused) - } - onSoloMode={(accountId) => handleSoloMode(selectedVariantData.provider, accountId)} - onBulkPause={(accountIds) => handleBulkPause(selectedVariantData.provider, accountIds)} - onBulkResume={(accountIds) => - handleBulkResume(selectedVariantData.provider, accountIds) - } - isRemovingAccount={removeMutation.isPending} - isPausingAccount={pauseMutation.isPending || resumeMutation.isPending} - isSoloingAccount={soloMutation.isPending} - isBulkPausing={bulkPauseMutation.isPending} - isBulkResuming={bulkResumeMutation.isPending} - /> - - ) : selectedStatus ? ( - <> - - ) : undefined - } - onAddAccount={() => - setAddAccountProvider({ - provider: selectedStatus.provider, - displayName: selectedStatus.displayName, - isFirstAccount: (selectedStatus.accounts?.length || 0) === 0, - }) - } - onSetDefault={(accountId) => - setDefaultMutation.mutate({ - provider: selectedStatus.provider, - accountId, - }) - } - onRemoveAccount={(accountId) => - removeMutation.mutate({ - provider: selectedStatus.provider, - accountId, - }) - } - onPauseToggle={(accountId, paused) => - handlePauseToggle(selectedStatus.provider, accountId, paused) - } - onSoloMode={(accountId) => handleSoloMode(selectedStatus.provider, accountId)} - onBulkPause={(accountIds) => handleBulkPause(selectedStatus.provider, accountIds)} - onBulkResume={(accountIds) => handleBulkResume(selectedStatus.provider, accountIds)} - isRemovingAccount={removeMutation.isPending} - isPausingAccount={pauseMutation.isPending || resumeMutation.isPending} - isSoloingAccount={soloMutation.isPending} - isBulkPausing={bulkPauseMutation.isPending} - isBulkResuming={bulkResumeMutation.isPending} - /> - - ) : ( - setWizardOpen(true)} /> - )} -
- ); - - return ( - - - - {updateCheck?.backendLabel ?? 'CLIProxy'} - - } - description={t('cliproxyPage.accountManagement')} - actions={ - - } - /> - + })} + authStatus={parentAuthForVariant} + catalog={catalogs[selectedVariantData.provider]} + routing={routingHints[selectedVariantData.provider]} + logoProvider={selectedVariantData.provider} + baseProvider={selectedVariantData.provider} + defaultTarget={selectedVariantData.target} + isRemoteMode={isRemoteMode} + port={selectedVariantData.port} + topNotice={ + showAccountSafetyWarning ? ( + + ) : undefined + } + onAddAccount={() => + setAddAccountProvider({ + provider: selectedVariantData.provider, + displayName: parentAuthForVariant.displayName, + isFirstAccount: (parentAuthForVariant.accounts?.length || 0) === 0, + }) + } + onSetDefault={(accountId) => + setDefaultMutation.mutate({ + provider: selectedVariantData.provider, + accountId, + }) + } + onRemoveAccount={(accountId) => + removeMutation.mutate({ + provider: selectedVariantData.provider, + accountId, + }) + } + onPauseToggle={(accountId, paused) => + handlePauseToggle(selectedVariantData.provider, accountId, paused) + } + onSoloMode={(accountId) => handleSoloMode(selectedVariantData.provider, accountId)} + onBulkPause={(accountIds) => + handleBulkPause(selectedVariantData.provider, accountIds) + } + onBulkResume={(accountIds) => + handleBulkResume(selectedVariantData.provider, accountIds) + } + isRemovingAccount={removeMutation.isPending} + isPausingAccount={pauseMutation.isPending || resumeMutation.isPending} + isSoloingAccount={soloMutation.isPending} + isBulkPausing={bulkPauseMutation.isPending} + isBulkResuming={bulkResumeMutation.isPending} + /> + + ) : selectedStatus ? ( + <> + + ) : undefined + } + onAddAccount={() => + setAddAccountProvider({ + provider: selectedStatus.provider, + displayName: selectedStatus.displayName, + isFirstAccount: (selectedStatus.accounts?.length || 0) === 0, + }) + } + onSetDefault={(accountId) => + setDefaultMutation.mutate({ + provider: selectedStatus.provider, + accountId, + }) + } + onRemoveAccount={(accountId) => + removeMutation.mutate({ + provider: selectedStatus.provider, + accountId, + }) + } + onPauseToggle={(accountId, paused) => + handlePauseToggle(selectedStatus.provider, accountId, paused) + } + onSoloMode={(accountId) => handleSoloMode(selectedStatus.provider, accountId)} + onBulkPause={(accountIds) => handleBulkPause(selectedStatus.provider, accountIds)} + onBulkResume={(accountIds) => handleBulkResume(selectedStatus.provider, accountIds)} + isRemovingAccount={removeMutation.isPending} + isPausingAccount={pauseMutation.isPending || resumeMutation.isPending} + isSoloingAccount={soloMutation.isPending} + isBulkPausing={bulkPauseMutation.isPending} + isBulkResuming={bulkResumeMutation.isPending} + /> + + ) : ( + setWizardOpen(true)} /> + )} +
{/* Dialogs */} setWizardOpen(false)} /> @@ -601,6 +595,6 @@ export function CliproxyPage() { } isFirstAccount={addAccountProvider?.isFirstAccount || false} /> - +
); } diff --git a/ui/src/pages/home.tsx b/ui/src/pages/home.tsx index cd055a5b..50b879ad 100644 --- a/ui/src/pages/home.tsx +++ b/ui/src/pages/home.tsx @@ -1,38 +1,68 @@ import { useNavigate } from 'react-router-dom'; -import { useTranslation } from 'react-i18next'; -import { - ActivityIcon, - AlertTriangle, - ArrowRight, - KeyIcon, - ScrollText, - ShieldCheck, - UsersIcon, - Zap, -} from 'lucide-react'; +import { HeroSection } from '@/components/layout/hero-section'; import { AuthMonitor } from '@/components/monitoring/auth-monitor'; import { ErrorLogsMonitor } from '@/components/error-logs-monitor'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; +import { Key, Zap, Users, Activity, AlertTriangle, ArrowRight, ScrollText } from 'lucide-react'; import { useOverview } from '@/hooks/use-overview'; import { useSharedSummary } from '@/hooks/use-shared'; -import { PageShell, PageHeader } from '@/components/page-shell'; -import { - MonitorLayout, - KpiRow, - KpiCard, - MonitorGrid, - MonitorCard, -} from '@/components/monitor-layout'; +import { cn } from '@/lib/utils'; +import type { LucideIcon } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; -const HEALTH_TONE = { - ok: 'positive', +const HEALTH_VARIANTS = { + ok: 'success', warning: 'warning', - error: 'negative', + error: 'error', } as const; +type StatVariant = 'default' | 'success' | 'warning' | 'error' | 'accent'; + +const variantStyles: Record = { + default: { iconBg: 'bg-muted', iconColor: 'text-muted-foreground' }, + success: { iconBg: 'bg-green-600/15', iconColor: 'text-green-700 dark:text-green-500' }, + warning: { iconBg: 'bg-amber-500/15', iconColor: 'text-amber-700 dark:text-amber-400' }, + error: { iconBg: 'bg-red-600/15', iconColor: 'text-red-700 dark:text-red-500' }, + accent: { iconBg: 'bg-accent/15', iconColor: 'text-accent' }, +}; + +function InlineStat({ + title, + value, + icon: Icon, + variant = 'default', + onClick, +}: { + title: string; + value: number | string; + icon: LucideIcon; + variant?: StatVariant; + onClick?: () => void; +}) { + const styles = variantStyles[variant]; + + return ( + + ); +} + export function HomePage() { const { t } = useTranslation(); const navigate = useNavigate(); @@ -40,138 +70,139 @@ export function HomePage() { const { data: shared, isLoading: isSharedLoading } = useSharedSummary(); if (isOverviewLoading || isSharedLoading) { - return ; + return ( +
+ {/* Hero Row Skeleton */} +
+
+ +
+ + +
+
+
+ {[1, 2, 3, 4].map((i) => ( + + ))} +
+
+ + {/* Auth Monitor Skeleton */} +
+
+ + +
+
+ +
+ {[1, 2, 3].map((i) => ( +
+ + + + +
+ ))} +
+
+ ); } - const healthTone = overview?.health - ? HEALTH_TONE[overview.health.status as keyof typeof HEALTH_TONE] - : 'default'; + const healthVariant = overview?.health + ? HEALTH_VARIANTS[overview.health.status as keyof typeof HEALTH_VARIANTS] + : undefined; return ( - - - v{overview.version} - - ) - } - /> - - + {/* Hero Row: Logo/Title + Inline Stats */} +
+ {/* Subtle background pattern */} +
+
+
+ + {/* Single Row Layout */} +
+ {/* Left: Logo + Title */} + + + {/* Right: Inline Stats */} +
+ } + icon={Key} + variant="accent" onClick={() => navigate('/providers')} /> - } + icon={Zap} + variant="accent" onClick={() => navigate('/cliproxy')} /> - } + icon={Users} + variant="default" onClick={() => navigate('/accounts')} /> - } + navigate('/health')} /> - - } - > - {shared?.symlinkStatus && !shared.symlinkStatus.valid && ( - - - {t('home.configurationRequired')} - {shared.symlinkStatus.message} - - )} +
+
+
- - {/* Live account monitor — primary viz */} - - - Live account monitor - - } - > - - + {/* Configuration Warning */} + {shared?.symlinkStatus && !shared.symlinkStatus.valid && ( + + + {t('home.configurationRequired')} + {shared.symlinkStatus.message} + + )} - {/* Logs callout */} - -
-
-
- -
-
-

{t('homePageV2.logsMoved')}

-

- Use the unified logs page for source-level filtering, structured entry - inspection, and retention policy edits without crowding the home dashboard. -

-
-
- + {/* Auth Monitor */} + + +
+
+
+
+
- +
+

{t('homePageV2.logsMoved')}

+

+ Use the unified logs page for source-level filtering, structured entry inspection, + and retention policy edits without crowding the home dashboard. +

+
+
+ +
+
- {/* Recent errors */} - - - Recent errors - - } - > - - - - - - ); -} - -function HomeLoadingSkeleton() { - return ( - - } /> - - {[1, 2, 3, 4].map((i) => ( - - ))} - - } - > - - - - - - - + +
); }