diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 00000000..b8b508f6 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,186 @@ +# OpenPets Control Center: Design System & Engineering Manual + +This document outlines the visual direction, component patterns, and front-end engineering guidelines for the OpenPets Control Center. It serves as a practical implementation guide for migrating existing views (Settings, Plugins, Integrations, Onboarding) and building new pages using a cohesive, high-fidelity tactile design system. + +--- + +## 1. Visual Direction & Heritage + +The OpenPets Control Center blends the nostalgic, playful charm of late-90s/early-2000s desktop pet managers with a modern, glassmorphic utility interface. + +- **The Core Tension**: Playful nostalgia meets crisp, modern desktop productivity. +- **Atmospheric Depth**: Instead of flat, sterile surfaces, interfaces leverage rich color gradients, subtle mesh backdrops, structured panels, and tactile control elements. +- **Physicality**: Interactive components feel physical. They have distinct borders, inset highlights, drop shadows, and visual feedback on click/press. + +--- + +## 2. Backgrounds & Surface Treatments + +Surfaces are layered to establish clear spatial hierarchy and keep the interface readable. + +### Page Backdrop +- **Formula**: A radial ambient glow on top of a subtle linear gradient. +- **Implementation**: + ```css + background: radial-gradient(circle at 12% 8%, rgba(219, 234, 254, 0.9), transparent 24%), + linear-gradient(180deg, #f8fbff 0%, #eff7ff 54%, #e9f3ff 100%); + ``` + +### Glass Panels (`.glass` / `GlassCard`) +- Panels use semi-transparent white fills coupled with a distinct double-border look (using inset box shadows) to simulate thick, polished acrylic sheets. +- **Specifications**: + - Background: `rgba(255, 255, 255, 0.76)` + - Border: `1px solid rgba(126, 161, 210, 0.48)` + - Inner Highlight: `inset 0 1px 0 rgba(255, 255, 255, 0.96)` + - Shadow: `0 24px 60px rgba(61, 99, 160, 0.15)` + - Blur: `backdrop-blur-xl` + - Border Radius: `28px` (`rounded-[28px]`) + +--- + +## 3. Page Shell, Layout & Grids + +The desktop shell is designed to be self-contained, fitting exactly within the viewport height without scrolling the entire window. + +- **Page Shell**: Max-width of `1160px` centered, utilizing a full-height flexbox column (`h-screen p-6`). +- **Hero Header**: Consists of a high-fidelity app icon (`rounded-3xl shadow-glass h-16 w-16`) paired with a tight, high-contrast typographic stack. +- **Layout Split**: A clean grid split, typically: + - **Left Gallery/List Panel**: `1.25fr` โ for browsing, searching, and filtering. + - **Right Detail/Action Panel**: `0.75fr` โ for contextual inspection and heavy operations. +- **Scroll Containers**: Sub-panels (like grids and lists) scroll internally (`overflow-y-auto`) with customized scrollbars so that headers, filters, and primary action bars remain pinned. + +--- + +## 4. Typography & Tone + +- **Font Family**: Refined sans-serif (e.g., `Inter`, `system-ui`) for maximum readability in body copy, labels, and metadata. +- **Display Typography**: Strong, characterful monospace or display-sans fonts (e.g., Lucida Console, geometric display fonts) for headers and uppercase labels to evoke retro desktop applications. +- **Typographic Hierarchy**: + - **Hero Title**: `text-5xl font-black tracking-tight` + - **Panel Title**: `text-3xl font-black` + - **Eyebrow / Section Header**: `text-xs font-bold uppercase tracking-[.18em] text-brand` + - **Card Title**: `text-sm font-semibold text-navy` + - **Metadata / Descriptions**: `text-xs text-slate-500` +- **Copy Tone**: Conversational, clean, and helpful. Avoid overly dense technical jargon; explain state transitions clearly (e.g., *"Ready to become your default pet"* instead of *"State: INSTALLED, ACTIVE: FALSE"*). + +--- + +## 5. Colors & Semantic Palette + +All colors are mapped to functional roles to ensure consistency across themes. + +| Role | Color / Hex | Tailwind Equivalent | Semantic Usage | +| :--- | :--- | :--- | :--- | +| **Brand Primary** | `#176df2` / `#3b96ff` | `bg-brand` / `text-brand` | Active filters, primary buttons, hero accents | +| **Dark Neutral** | `#102149` | `text-navy` | Main body text, card titles, heavy headings | +| **Light Neutral** | `#f8fbff` to `#e9f3ff` | `bg-slate-50` / `bg-blue-50` | Page backdrops, card backgrounds, empty states | +| **Slate Copy** | `#5c6e91` | `text-slatecopy` | Subtitles, helper text, inactive buttons, labels | +| **Success / Ready** | `#059669` / `#34d399` | `emerald` | Active indicators, successfully installed badges | +| **Warning / Import** | `#d97706` / `#fbbf24` | `amber` | Codex source indicators, imports, attention states | +| **Danger / Remove** | `#dc2626` / `#ff6b6b` | `red` | Destructive actions, broken states, error messages | + +--- + +## 6. Tactile Button System + +Buttons are the core interactive elements. They reject flat modern trends in favor of a chunky, tangible 3D appearance inspired by physical pet hardware and OS controls. + +### Visual Architecture +- **Borders**: Darker semi-transparent bottom/side borders (`rgba(..., 0.32)`). +- **Gradients**: Linear vertical gradients that transition from a lighter top to a richer bottom. +- **Inset Highlights**: A subtle light line (`inset 0 1px 0 rgba(255,255,255,0.38)`) on the top edge to simulate light catching the button's crown. +- **Shadows**: Keep buttons grounded. Use subtle neutral depth plus inset highlights; avoid large colored glow shadows. + +### Interactive Rules +- **Hover States**: Under current design guidelines, **avoid dramatic hover glows or float lifts**. Hovering should simply shift the gradient/background color slightly to maintain a grounded, crisp, and predictable interface. +- **Active Click State**: On press, the button scales down slightly to `0.96` to provide a satisfying physical click response. + - **Constraint**: Always use targeted transitions (`transition-[transform,background-color,border-color,box-shadow]`) instead of `transition-all` to prevent layout reflow stutter. + +### Button Variants + +1. **Primary** (`.btn-primary`): + - Gradient: `#3b96ff` to `#176df2` + - Shadow: `inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10)` +2. **Secondary** (`.btn-secondary`): + - Surface: `rgba(255,255,255,0.76)` with text `#176df2` + - Border: `rgba(37, 99, 235, 0.42)` +3. **Danger** (`.btn-danger`): + - Gradient: `#ff6b6b` to `#dc2626` + - Shadow: same subtle neutral depth as primary; do not add red glow +4. **Warning** (`.btn-warning`): + - Gradient: `#fbbf24` to `#d97706` +5. **Success** (`.btn-success`): + - Gradient: `#34d399` to `#059669` +6. **Compact Button** (`.btn-compact`): + - Designed for pagination (`.pager`) and secondary rows. + - Reduces size to `min-h-[30px]`, padding to `px-2.5 py-1`, and font size to `text-xs` with a tighter border-radius (`rounded-xl`). + +--- + +## 7. Controls & Navigation Elements + +### Filters (`.filter`) +- Instead of simple pill badges, filters are structured as miniature tactile buttons. +- Inactive filters use a soft white-to-slate gradient with a light blue-grey border. +- Active filters swap to full brand gradients (e.g., blue for all, orange for original) with only subtle grounded depth. + +### Search Inputs (`.search`) +- Rendered as soft, inset text boxes. +- Styled with `bg-white/80` and `shadow-inner` to look recessed into the panel. +- Focus states use a visible but restrained brand ring (`focus:ring-brand/15 focus:border-brand`) for accessibility. + +### Pager (`.pager`) +- Placed cleanly at the **bottom** of list/grid containers. +- Merges compact secondary buttons with centered, bold, uppercase status text (`text-xs font-bold tracking-wider`). + +--- + +## 8. Icons & Visual Assets + +- **Coherency**: Use clean, outline-style vectors with a consistent stroke weight (`stroke-width="2.5"` or `3` for smaller shapes). +- **Style Alignment**: Align with the **Lucide / Iconify** icon libraries. +- **Implementation**: Embed icons as stateless, inline React SVG components or a localized icon catalog within the component file to avoid heavy runtime dependencies. +- **Sizing**: + - Action/Button Icons: `16px x 16px` + - Compact/Filter Icons: `12px x 12px` or `14px x 14px` + +--- + +## 9. React & Tailwind Implementation Notes + +### The Static Purge Constraint +Tailwind CSS extracts classes statically by analyzing source code strings. **Do not construct class names dynamically**. + +```tsx +// โ BAD: Will be purged by Tailwind in production +const color = "blue"; +return Label; + +// GOOD: Statically discoverable mapping +const statusPillToneClass = { + blue: "pill-blue", + green: "pill-green", +} as const; +return Label; +``` + +--- + +## 10. Migration Playbook for Future Pages + +When refactoring other Control Center views, apply the following structural mapping: + +### A. Settings Page +- **Old Layout**: Long scrolling list of form controls. +- **New Layout**: Split-pane layout. Left side lists settings categories (General, Hotkeys, Performance) using `.filter` tactile navigation. Right side contains the active form wrapped in a `.glass` card. +- **Inputs**: Swap default inputs to the recessed `.search` input style. Use `.btn-primary` for the final "Save" action. + +### B. Plugins Page +- **Old Layout**: Flat list of available integrations. +- **New Layout**: Grid-based layout matching the `.pets-grid` structure. +- **Cards**: Use the `.pet-card` styling with a thumbnail on the left, author/description text on the right, and semantic status pills (e.g., `.pill-green` for "Active", `.pill-slate` for "Disabled"). + +### C. Onboarding +- **Old Layout**: Full screen wizard. +- **New Layout**: Centered, floating glass card (`.glass` with `max-w-xl shadow-glass`) layered over the atmospheric radial backdrop. +- **Interaction**: Use staggered CSS keyframes for a smooth entrance, and a prominent `.btn-primary` with active scaling for the "Get Started" call to action. diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index ea245cb1..9afd408b 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -25,10 +25,10 @@ const InstallIcon = () => ( const ImportIcon = () => ( ); @@ -40,69 +40,83 @@ const SetDefaultIcon = () => ( const RemoveIcon = () => ( ); const RefreshIcon = () => ( ); const PrevIcon = () => ( ); const NextIcon = () => ( ); const FilterAllIcon = () => ( ); const FilterInstalledIcon = () => ( ); const FilterOriginalIcon = () => ( ); const FilterWesternIcon = () => ( ); const FilterAsianIcon = () => ( ); const FilterCodexIcon = () => ( ); @@ -134,6 +148,7 @@ const statusPillToneClass = { function Button({ children, variant = "primary", + size = "normal", onClick, disabled, icon, @@ -142,6 +157,7 @@ function Button({ }: { children: React.ReactNode; variant?: "primary" | "secondary" | "danger" | "success" | "warning"; + size?: "normal" | "compact"; onClick?: () => void; disabled?: boolean; icon?: React.ReactNode; @@ -150,13 +166,13 @@ function Button({ }) { return ( ); } @@ -425,7 +441,7 @@ function App() { {error &&
Pet detail
{selected.description || selected.id}
@@ -545,7 +565,7 @@ function App() { )} {selected.installed && selected.id !== defaultId && !selected.broken && ( } disabled={!!busy} diff --git a/apps/desktop/src/renderer/src/styles.css b/apps/desktop/src/renderer/src/styles.css index 6d2b32eb..a5a7423d 100644 --- a/apps/desktop/src/renderer/src/styles.css +++ b/apps/desktop/src/renderer/src/styles.css @@ -3,7 +3,7 @@ @tailwind utilities; @layer base { - body { margin:0; min-height:100vh; overflow:hidden; color:#102149; background: radial-gradient(circle at 20% 0%, rgba(59,150,255,.2), transparent 32rem), linear-gradient(135deg,#f8fbff,#eff7ff 45%,#e9f3ff); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; } + body { margin:0; min-height:100vh; overflow:hidden; color:#102149; background: radial-gradient(circle at 12% 8%, rgba(219, 234, 254, 0.9), transparent 24%), linear-gradient(180deg, #f8fbff 0%, #eff7ff 54%, #e9f3ff 100%); font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-font-smoothing: antialiased; } ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: rgba(96, 165, 250, 0.25); border-radius: 100px; } @@ -14,18 +14,22 @@ .hero { @apply mb-6 flex items-center gap-5; } .hero img { @apply h-16 w-16 rounded-3xl shadow-glass; } .hero h1 { @apply m-0 font-monoDisplay text-5xl font-black tracking-tight; } .hero p { @apply m-0 text-slatecopy; } .eyebrow { @apply mb-2 font-monoDisplay text-xs font-black uppercase tracking-[.18em] text-brand; } .layout { @apply grid min-h-0 flex-1 grid-cols-[1.25fr_.75fr] gap-5 max-[900px]:grid-cols-1; } - .glass { @apply rounded-[28px] border border-[rgba(126,161,210,.44)] bg-white/65 p-5 shadow-glass backdrop-blur-xl; box-shadow: inset 0 1px 0 rgba(255,255,255,.85), 0 24px 70px rgba(50,104,180,.18); } + .glass { @apply rounded-[28px] border border-[rgba(126,161,210,.48)] p-5 shadow-glass backdrop-blur-xl; background: rgba(255, 255, 255, 0.76); box-shadow: inset 0 1px 0 rgba(255,255,255,.96), 0 24px 60px rgba(61,99,160,.15); } .gallery { @apply flex min-h-0 flex-col overflow-hidden; } .toolbar { @apply flex items-center gap-3; } .search { @apply min-w-0 flex-1 rounded-2xl border border-blue-300/80 bg-white/80 px-4 py-3 text-navy outline-none shadow-inner transition-shadow duration-150 focus:border-brand focus:bg-white focus:ring-4 focus:ring-brand/15; } - .filters { @apply my-4 flex flex-wrap gap-2; } - .filter { @apply rounded-full border border-slate-200 bg-slate-50/40 px-4 py-2 font-monoDisplay text-xs font-bold uppercase tracking-wide text-slate-600 active:scale-[0.96] transition-[transform,background-color,border-color,box-shadow] duration-150 flex items-center gap-1.5 cursor-pointer select-none h-10 shadow-sm; } - .filter:hover:not(.active) { @apply bg-slate-100/80 border-slate-300 text-slate-800; } - .filter.active { @apply border-brand bg-brand text-white shadow-md; } - .filter.original.active { @apply border-orange-500 bg-orange-500 text-white shadow-md; } + .filters { @apply my-3 flex flex-wrap gap-1.5; } + .filter { @apply rounded-xl border px-3 py-1.5 font-monoDisplay text-xs font-black uppercase tracking-wide text-slate-600 active:scale-[0.96] transition-[transform,background-color,border-color,box-shadow,translate] duration-150 flex items-center gap-1.5 cursor-pointer select-none h-8 shadow-sm; border-color: rgba(126, 161, 210, 0.4); background: linear-gradient(180deg, rgba(255, 255, 255, 0.9) 0%, rgba(241, 245, 249, 0.9) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.06); } + .filter:hover:not(.active) { @apply text-brand; border-color: rgba(37, 99, 235, 0.35); background: linear-gradient(180deg, rgba(255, 255, 255, 1) 0%, rgba(239, 246, 255, 0.9) 100%); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.06); } + .filter:active { translate: 0 0; } + .filter.active { @apply text-white; border-color: rgba(29, 78, 216, 0.5); background: linear-gradient(180deg, #3b96ff 0%, #176df2 100%); box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.35), 0 2px 4px rgba(61,99,160,0.10); } + .filter.active:hover { background: linear-gradient(180deg, #55a6ff 0%, #176df2 100%); } + .filter.active:active { translate: 0 0; } + .filter.original.active { border-color: rgba(194, 65, 12, 0.5); background: linear-gradient(180deg, #fb923c 0%, #ea580c 100%); box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.35), 0 2px 4px rgba(61,99,160,0.10); } + .filter.original.active:hover { background: linear-gradient(180deg, #ffaa47 0%, #ea580c 100%); } .filter-icon-wrapper { @apply flex items-center justify-center shrink-0 opacity-70; } .filter.active .filter-icon-wrapper { @apply opacity-100; } .filter-icon { @apply h-3.5 w-3.5 stroke-[2.5]; } - .pager { @apply mb-4 flex items-center justify-between gap-3 rounded-2xl border border-blue-200/60 bg-blue-50/30 p-2 text-sm font-semibold text-slatecopy; } + .pager { @apply mt-3 flex items-center justify-between gap-2.5 rounded-xl border border-blue-200/40 bg-blue-50/20 p-1.5 text-xs font-bold text-slatecopy; } .pets-grid { @apply grid min-h-0 flex-1 grid-cols-2 gap-3 overflow-y-auto pr-2 max-[680px]:grid-cols-1; overscroll-behavior: contain; scrollbar-color: rgba(96, 165, 250, 0.42) transparent; } .pet-card { @apply flex flex-row items-center gap-3.5 rounded-2xl border border-slate-200 bg-white/80 p-3 text-left shadow-sm transition-[transform,border-color,background-color,box-shadow] hover:-translate-y-0.5 hover:border-brand hover:bg-white active:scale-[0.96] min-h-[92px] w-full cursor-pointer; } .pet-card.selected { @apply border-brand bg-blue-50/90 ring-4 ring-blue-200/50 shadow-md; } @@ -49,12 +53,18 @@ @media (prefers-reduced-motion: reduce) { .sprite-frame { animation: none !important; } .sprite-frame animate { display: none; } } @media (max-width: 900px) { body { overflow:auto; } .app-shell { @apply h-auto min-h-screen; } .layout, .gallery, .pets-grid, .detail { @apply overflow-visible; } } .meta { @apply flex flex-wrap gap-2; } - .btn { @apply rounded-2xl px-4 py-3 font-monoDisplay text-sm font-black uppercase tracking-wide disabled:cursor-not-allowed disabled:opacity-60 active:scale-[0.96] transition-[transform,background-color,border-color,box-shadow] duration-150 flex items-center justify-center min-h-[44px] cursor-pointer select-none; } - .btn-primary { @apply bg-gradient-to-r from-brand to-[#0d56c6] text-white shadow-md hover:from-brand-light hover:to-brand focus:ring-2 focus:ring-brand/40 border-b-2 border-blue-700; } - .btn-secondary { @apply border border-slate-200 bg-white text-slate-700 shadow-sm hover:bg-slate-50 hover:border-slate-300 focus:ring-2 focus:ring-slate-300/50; } - .btn-danger { @apply border border-red-200 bg-red-50/20 text-red-600 shadow-sm hover:bg-red-50 hover:border-red-400 focus:ring-2 focus:ring-red-500/30; } - .btn-success { @apply bg-gradient-to-r from-emerald-600 to-emerald-700 text-white shadow-md hover:from-emerald-500 hover:to-emerald-600 focus:ring-2 focus:ring-emerald-500/40 border-b-2 border-emerald-800; } - .btn-warning { @apply bg-gradient-to-r from-amber-500 to-amber-600 text-white shadow-md hover:from-amber-400 hover:to-amber-500 focus:ring-2 focus:ring-amber-500/40 border-b-2 border-amber-700; } + .btn { @apply rounded-2xl px-4 py-3 font-monoDisplay text-sm font-black uppercase tracking-wide disabled:cursor-not-allowed disabled:opacity-60 active:scale-[0.96] transition-[transform,translate,background-color,border-color,box-shadow] duration-150 flex items-center justify-center min-h-[44px] cursor-pointer select-none; } + .btn-compact { @apply rounded-xl px-2.5 py-1 text-xs min-h-[30px]; } + .btn-primary { border: 1px solid rgba(37, 99, 235, 0.32); background: linear-gradient(180deg, #3b96ff, #176df2); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); } + .btn-primary:hover:not(:disabled) { background: linear-gradient(180deg, #55a6ff, #176df2); } + .btn-secondary { background: rgba(255,255,255,0.76); color: #176df2; border: 1px solid rgba(37, 99, 235, 0.42); box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 2px 4px rgba(61,99,160,0.08); } + .btn-secondary:hover:not(:disabled) { background: rgba(255,255,255,0.95); border-color: rgba(37, 99, 235, 0.55); box-shadow: inset 0 1px 0 rgba(255,255,255,1), 0 2px 4px rgba(61,99,160,0.10); } + .btn-danger { border: 1px solid rgba(220, 38, 38, 0.32); background: linear-gradient(180deg, #ff6b6b, #dc2626); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); } + .btn-danger:hover:not(:disabled) { background: linear-gradient(180deg, #ff8787, #dc2626); } + .btn-success { border: 1px solid rgba(5, 150, 105, 0.32); background: linear-gradient(180deg, #34d399, #059669); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); } + .btn-success:hover:not(:disabled) { background: linear-gradient(180deg, #6ee7b7, #059669); } + .btn-warning { border: 1px solid rgba(217, 119, 6, 0.32); background: linear-gradient(180deg, #fbbf24, #d97706); color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,0.38), 0 2px 4px rgba(61,99,160,0.10); } + .btn-warning:hover:not(:disabled) { background: linear-gradient(180deg, #fcd34d, #d97706); } .btn-icon-wrapper { @apply flex items-center justify-center shrink-0; } .btn-icon { @apply h-4 w-4 stroke-[2.5]; } diff --git a/assets/logo.png b/assets/logo.png new file mode 100644 index 00000000..819e8d27 Binary files /dev/null and b/assets/logo.png differ