diff --git a/docs/superpowers/plans/2026-04-22-fredy-ui-redesign.md b/docs/superpowers/plans/2026-04-22-fredy-ui-redesign.md new file mode 100644 index 0000000..c5dcb12 --- /dev/null +++ b/docs/superpowers/plans/2026-04-22-fredy-ui-redesign.md @@ -0,0 +1,2371 @@ +# Fredy UI Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Apply the cronpilot visual identity (dark design system with red accent) to Fredy's React frontend screen-by-screen using Semi UI component overrides and LESS tokens. + +**Architecture:** Add a `tokens.less` file as the single source of truth for all design variables; override Semi UI's CSS custom properties globally in `Index.less`; apply the new CI to every view and shared component. No state management changes. No routing changes. + +**Tech Stack:** React 18, `@douyinfe/semi-ui-19` (v2.95), LESS (BEM), Zustand, React Router, MapLibre GL. + +--- + +## File Map + +| File | Action | Purpose | +|---|---|---| +| `index.html` | Modify | Add Google Fonts link | +| `ui/src/tokens.less` | Create | All LESS/CSS design tokens | +| `ui/src/Index.less` | Modify | Semi UI CSS variable overrides, body bg, scrollbar | +| `ui/src/App.less` | Modify | App shell layout | +| `ui/src/components/navigation/Navigation.jsx` | Modify | New sidebar with footer bar, collapse logic | +| `ui/src/components/navigation/Navigate.less` | Modify | Sidebar LESS | +| `ui/src/components/footer/FredyFooter.jsx` | Modify | "Made with heart by Christian Kellner" link | +| `ui/src/components/footer/FredyFooter.less` | Modify | Footer bar styles | +| `ui/src/components/headline/Headline.jsx` | Modify | Become `PageHeading` with gradient line + optional action slot | +| `ui/src/views/login/Login.jsx` | Modify | New centered glass-card login | +| `ui/src/views/login/login.less` | Modify | New login styles | +| `ui/src/components/cards/KpiCard.jsx` | Modify | Compact 112px card, glow hover only | +| `ui/src/components/cards/DashboardCard.less` | Modify | New KPI card LESS | +| `ui/src/views/dashboard/Dashboard.jsx` | Modify | Add PageHeading, remove section-label typography | +| `ui/src/views/dashboard/Dashboard.less` | Modify | Dashboard layout LESS | +| `ui/src/components/grid/jobs/JobGrid.jsx` | Modify | New topbar layout + table layout (replace Card grid) | +| `ui/src/components/grid/jobs/JobGrid.less` | Modify | Table + topbar LESS | +| `ui/src/views/jobs/Jobs.jsx` | Modify | Add PageHeading | +| `ui/src/components/segment/SegmentPart.jsx` | Modify | New section-card design | +| `ui/src/components/segment/SegmentParts.less` | Modify | Section card LESS | +| `ui/src/views/jobs/mutation/JobMutation.jsx` | Modify | Add PageHeading + back button | +| `ui/src/views/jobs/mutation/JobMutation.less` | Modify | Minor layout updates | +| `ui/src/components/grid/listings/ListingsGrid.jsx` | Modify | New card design with action bar, watermark | +| `ui/src/components/grid/listings/ListingsGrid.less` | Modify | New listing card LESS | +| `ui/src/views/listings/Map.jsx` | Modify | Add PageHeading, styled filter panel | +| `ui/src/views/listings/Map.less` | Modify | Filter panel overlay styles | +| `ui/src/views/user/Users.jsx` | Modify | Add PageHeading, styled table | +| `ui/src/views/user/Users.less` | Modify | Users table LESS | +| `ui/src/components/table/UserTable.jsx` | Modify | Avatar initials, admin badge, styled rows | +| `ui/src/views/user/mutation/UserMutator.jsx` | Modify | Add PageHeading + back button | +| `ui/src/views/user/mutation/UserMutator.less` | Modify | Section card layout | +| `ui/src/views/generalSettings/GeneralSettings.jsx` | Modify | Tab styles, section cards, Save button with icon | +| `ui/src/views/generalSettings/GeneralSettings.less` | Modify | Tab + section card LESS | + +--- + +## Task 1: Design Tokens + Global Styles + +**Files:** +- Create: `ui/src/tokens.less` +- Modify: `index.html` +- Modify: `ui/src/Index.less` + +This is the foundation. Every subsequent task imports `tokens.less`. + +- [ ] **Step 1: Create `ui/src/tokens.less`** + +```less +// ── Backgrounds ────────────────────────────────────────── +@color-base: #0d0d0d; +@color-surface: #161616; +@color-elevated: #1e1e1e; +@color-border: #2a2a2a; +@color-border-bright: #383838; + +// ── Accent ─────────────────────────────────────────────── +@color-accent: #e04a38; +@color-accent-dim: #c13827; +@color-accent-glow: rgba(224, 74, 56, 0.13); + +// ── Text ───────────────────────────────────────────────── +@color-text: #efefef; +@color-muted: #909090; +@color-faint: #505050; + +// ── Semantic ───────────────────────────────────────────── +@color-success: #34d399; +@color-success-dim: #065f46; +@color-error: #fb7185; +@color-error-dim: #881337; +@color-warning: #fbbf24; +@color-info: #60a5fa; + +// ── KPI card accents ───────────────────────────────────── +@color-blue-text: #60a5fa; @color-blue-border: #3b6ea8; @color-blue-bg: rgba(96,165,250,0.10); +@color-orange-text: #fb923c; @color-orange-border: #c2622a; @color-orange-bg: rgba(251,146,60,0.10); +@color-green-text: #34d399; @color-green-border: #2a8a61; @color-green-bg: rgba(52,211,153,0.10); +@color-purple-text: #a78bfa; @color-purple-border: #6d4fc2; @color-purple-bg: rgba(167,139,250,0.10); +@color-gray-text: #94a3b8; @color-gray-border: #323a47; @color-gray-bg: rgba(148,163,184,0.10); + +// ── Typography ─────────────────────────────────────────── +@font-ui: 'Outfit', system-ui, sans-serif; +@font-mono: 'JetBrains Mono', monospace; + +@text-xs: 11px; +@text-sm: 12px; +@text-base: 14px; +@text-md: 16px; +@text-lg: 20px; +@text-xl: 24px; + +// ── Spacing ────────────────────────────────────────────── +@space-1: 4px; +@space-2: 8px; +@space-3: 12px; +@space-4: 16px; +@space-5: 20px; +@space-6: 24px; +@space-8: 32px; +@space-12: 48px; + +// ── Radius ─────────────────────────────────────────────── +@radius-input: 10px; +@radius-card: 10px; +@radius-btn: 6px; +@radius-pill: 9999px; +@radius-chip: 4px; + +// ── Transitions ────────────────────────────────────────── +@transition-fast: 0.15s ease-in-out; +@transition-card: 0.18s ease-in-out; +@transition-sidebar: width 0.25s ease-in-out; +``` + +- [ ] **Step 2: Add Google Fonts to `index.html`** + +Open `index.html` and add inside `` before the closing `` tag: + +```html + + + +``` + +- [ ] **Step 3: Replace `ui/src/Index.less` with new global styles** + +```less +@import './tokens.less'; + +body, +html { + margin: 0; + height: 100%; + width: 100%; + font-family: @font-ui; + background-color: @color-base; + background-image: radial-gradient(ellipse at 60% 0%, rgba(224,74,56,0.05) 0%, transparent 55%); + background-attachment: fixed; +} + +body { + // Semi UI theme overrides + --semi-color-bg-0: #0d0d0d; + --semi-color-bg-1: #161616; + --semi-color-bg-2: #1e1e1e; + --semi-color-bg-3: #2a2a2a; + --semi-color-border: #2a2a2a; + --semi-color-primary: #e04a38; + --semi-color-primary-hover: #c13827; + --semi-color-primary-active: #c13827; + --semi-color-primary-light-default: rgba(224,74,56,0.12); + --semi-color-primary-light-hover: rgba(224,74,56,0.18); + --semi-color-primary-light-active: rgba(224,74,56,0.22); + --semi-color-text-0: #efefef; + --semi-color-text-1: #efefef; + --semi-color-text-2: #909090; + --semi-color-text-3: #505050; + --semi-color-fill-0: rgba(255,255,255,0.04); + --semi-color-fill-1: rgba(255,255,255,0.06); + --semi-color-fill-2: rgba(255,255,255,0.08); + --semi-font-family: 'Outfit', system-ui, sans-serif; +} + +// Semi table row overrides +.semi-table-row-head { + background-color: rgba(255,255,255,0.06) !important; +} +.semi-table-row-head .semi-table-row-cell { + background-color: rgba(255,255,255,0.06) !important; + color: @color-muted !important; + font-size: @text-xs; + text-transform: uppercase; + letter-spacing: 0.04em; +} +.semi-table-row-cell { + background-color: @color-surface !important; +} +.semi-table-tbody .semi-table-row:nth-child(even) .semi-table-row-cell { + background-color: @color-base !important; +} +.semi-table-tbody .semi-table-row:hover .semi-table-row-cell { + background-color: @color-elevated !important; +} + +// Scrollbar +::-webkit-scrollbar { width: 6px; } +::-webkit-scrollbar-track { background: @color-surface; } +::-webkit-scrollbar-thumb { background: @color-border-bright; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: @color-muted; } + +.semi-icon:not(.semi-tabs-bar .semi-tabs-tab .semi-icon) { + vertical-align: middle; +} +``` + +- [ ] **Step 4: Update `ui/src/App.less` to import tokens and fix layout** + +```less +@import './tokens.less'; + +.app { + height: 100vh; + width: 100vw; + + &__main { + height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; + } + + &__content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + position: relative; + padding: @space-6; + background-color: transparent; + box-sizing: border-box; + display: flex; + flex-direction: column; + + @media (max-width: 768px) { + padding: @space-3; + } + } +} +``` + +- [ ] **Step 5: Replace `ui/src/components/cards/DashboardCardColors.less`** + +The file currently defines all KPI color variables. Replace its entire content with just the import so there is one source of truth: + +```less +@import '../../tokens.less'; +``` + +All `@color-blue-*`, `@color-orange-*`, etc. now come from `tokens.less`. The import chain `DashboardCard.less → DashboardCardColors.less → tokens.less` makes every token available in DashboardCard.less. + +- [ ] **Step 6: Add Semi Modal overrides to `ui/src/Index.less`** + +Append to the end of `Index.less` (inside or after the body rule): + +```less +// Semi Modal dark theme overrides +.semi-modal-content { + background: #161616 !important; + border: 1px solid @color-border !important; + border-radius: 14px !important; +} + +.semi-modal-header { + background: linear-gradient(135deg, #1e1e1e 0%, #161616 100%) !important; + border-bottom: 1px solid @color-border !important; + border-left: 3px solid @color-accent !important; + border-radius: 14px 14px 0 0 !important; +} + +.semi-modal-mask { + background: rgba(0,0,0,0.7) !important; + backdrop-filter: blur(4px); +} +``` + +- [ ] **Step 7: Verify the app compiles** + +```bash +yarn dev +``` + +Expected: Vite dev server starts without errors. The page should be mostly dark with the Outfit font visible. + +- [ ] **Step 8: Commit** + +```bash +git add index.html ui/src/tokens.less ui/src/Index.less ui/src/App.less ui/src/components/cards/DashboardCardColors.less +git commit -m "feat: add design tokens and Semi UI theme overrides" +``` + +--- + +## Task 2: App Shell — Sidebar Navigation + +**Files:** +- Modify: `ui/src/components/navigation/Navigation.jsx` +- Modify: `ui/src/components/navigation/Navigate.less` + +The sidebar expands to 220px / collapses to 60px. Auto-collapses at <=850px. Active item has red accent. Footer has logout + collapse toggle. + +- [ ] **Step 1: Replace `ui/src/components/navigation/Navigate.less`** + +```less +@import '../../tokens.less'; + +.navigate { + display: flex; + flex-direction: column; + height: 100%; + width: 100%; + background: @color-surface; + border-right: 1px solid @color-border; + transition: @transition-sidebar; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + justify-content: center; + padding: 20px 16px 16px; + min-height: 64px; + flex-shrink: 0; + + img { + transition: width @transition-fast, opacity @transition-fast; + } + } + + &__footer { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 12px 8px; + margin-top: auto; + flex-shrink: 0; + } +} + +// Semi Nav overrides +.semi-navigation { + background: @color-surface !important; + border-right: none !important; +} + +.semi-navigation-item { + border-radius: @radius-btn !important; + color: @color-muted !important; + transition: background @transition-fast, color @transition-fast !important; + margin: 2px 8px !important; + + &:hover { + background: #23242a !important; + color: @color-text !important; + } + + &.semi-navigation-item-selected, + &[aria-selected="true"] { + background: rgba(224,74,56,0.12) !important; + border: 1px solid rgba(224,74,56,0.25) !important; + color: @color-text !important; + + .semi-navigation-item-icon { + color: @color-accent !important; + } + } +} + +.semi-navigation-sub-title { + color: @color-muted !important; +} +``` + +- [ ] **Step 2: Update `ui/src/components/navigation/Navigation.jsx`** + +No structural changes needed in JSX — the Semi `Nav` component is kept. Just update the `style` prop and add the `navigate` class: + +```jsx +/* + * Copyright (c) 2026 by Christian Kellner. + * Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause + */ + +import { useEffect, useState } from 'react'; +import { Button, Nav } from '@douyinfe/semi-ui-19'; +import { IconStar, IconSetting, IconTerminal, IconHistogram, IconSidebar } from '@douyinfe/semi-icons'; +import logoWhite from '../../assets/logo_white.png'; +import heart from '../../assets/heart.png'; +import Logout from '../logout/Logout.jsx'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import './Navigate.less'; +import { useScreenWidth } from '../../hooks/screenWidth.js'; + +export default function Navigation({ isAdmin }) { + const navigate = useNavigate(); + const location = useLocation(); + + const width = useScreenWidth(); + const [collapsed, setCollapsed] = useState(width <= 850); + + useEffect(() => { + if (width <= 850) { + setCollapsed(true); + } + }, [width]); + + const items = [ + { itemKey: '/dashboard', text: 'Dashboard', icon: }, + { itemKey: '/jobs', text: 'Jobs', icon: }, + { + itemKey: 'listings', + text: 'Listings', + icon: , + items: [ + { itemKey: '/listings', text: 'Overview' }, + { itemKey: '/map', text: 'Map View' }, + ], + }, + ]; + + if (isAdmin) { + items.push({ + itemKey: 'settings', + text: 'Settings', + icon: , + items: [ + { itemKey: '/users', text: 'User Management' }, + { itemKey: '/generalSettings', text: 'Settings' }, + ], + }); + } else { + items.push({ + itemKey: 'settings', + text: 'Settings', + icon: , + items: [{ itemKey: '/generalSettings', text: 'Settings' }], + }); + } + + function parsePathName(name) { + const split = name.split('/').filter((s) => s.length !== 0); + return '/' + split[0]; + } + + const sidebarWidth = collapsed ? '60px' : '220px'; + + return ( +