# 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 (