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