# SaaS Platform UX Polish — Design Spec
**Date:** 2026-04-09
**Scope:** Bug fixes, design consistency, error handling, component quality for the cameleer-saas platform UI
**Out of scope:** Mobile responsiveness (deferred), new features (billing, team management), admin tenant creation (#37)
## Context
Playwright-driven audit of the live SaaS platform (22 screenshots) plus source code audit of `ui/src/` (3 pages, sign-in page, layout, auth components). The platform has only 3 pages — Dashboard, License, Admin Tenants — plus a custom Logto sign-in page. Issues are concentrated and structural.
Audit artifacts in `audit/`:
- `platform-ui-findings.md` — 30 issues from live UI audit
- `source-code-findings.md` — 22 issues from source code analysis
## Implementation Strategy
4 batches, ordered by impact. Smaller scope than the server UI polish (~25 items vs ~52).
---
## Batch 1: Layout Fixes
**Effort:** 0.5 days
### 1.1 Fix Label/Value Collision
**Problem:** Throughout Dashboard and License pages, labels and values run together: "Slugdefault", "Max Agents3", "Issued8. April 2026". The code uses `flex justify-between` but the flex container doesn't stretch to full Card width.
**Root cause (source audit):** The `
` elements are inside Card components. If the Card's inner container doesn't apply `w-full` or the flex children don't have enough space, `justify-between` collapses.
**Fix:** Ensure the container divs inside Cards have `w-full` (or `className="flex justify-between w-full"`). Check all label/value rows in:
- `DashboardPage.tsx:95-112` — Tenant Information section
- `LicensePage.tsx:94-115` — Validity section
- `LicensePage.tsx:145-158` — Limits section
If the Card component's children wrapper is the constraint, wrap the content in `
`.
**Files:** `ui/src/pages/DashboardPage.tsx`, `ui/src/pages/LicensePage.tsx`
### 1.2 Replace Hardcoded `text-white` with DS Variables
**Problem:** Every page uses Tailwind `text-white`, `text-white/60`, `text-white/80`, `bg-white/5`, `border-white/10` instead of DS CSS variables. This breaks light theme (TopBar has a working theme toggle).
**Fix:** Replace all hardcoded color classes with DS CSS variable equivalents using inline styles or a CSS module:
| Tailwind class | DS variable |
|---------------|-------------|
| `text-white` | `var(--text-primary)` |
| `text-white/80` | `var(--text-secondary)` |
| `text-white/60` | `var(--text-muted)` |
| `text-white/40` | `var(--text-faint)` |
| `bg-white/5` | `var(--bg-hover)` |
| `bg-white/10` | `var(--bg-inset)` |
| `border-white/10` | `var(--border-subtle)` |
| `divide-white/10` | `var(--border-subtle)` |
**Approach:** Create a shared CSS module (`ui/src/styles/platform.module.css`) with classes mapping to DS variables, or switch to inline `style={{ color: 'var(--text-primary)' }}`. The sign-in page already demonstrates the correct pattern with CSS modules + DS variables.
**Files:** `ui/src/pages/DashboardPage.tsx`, `ui/src/pages/LicensePage.tsx`, `ui/src/pages/AdminTenantsPage.tsx`
### 1.3 Reduce Redundant Dashboard Content
**Problem:** "Open Server Dashboard" appears 3 times. Status "ACTIVE" appears 3 times. Tier badge appears 2 times.
**Fix:**
- Remove the "Open Server Dashboard" primary button from the header area (keep the Server Management card + sidebar footer link — 2 locations max)
- Remove the status badge from the header area (keep KPI strip + Tenant Information)
- The tier badge next to the heading is fine (quick context)
**Files:** `ui/src/pages/DashboardPage.tsx`
---
## Batch 2: Header & Navigation
**Effort:** 1 day
### 2.1 Hide Server Controls on Platform Pages
**Problem:** TopBar always renders status filters (OK/Warn/Error/Running), time range pills (1h-7d), auto-refresh toggle, and command palette search. All are irrelevant on platform pages.
**Fix options (pick one):**
**Option A (recommended): Use TopBar props to hide sections.**
Check if the DS `TopBar` component accepts props to control which sections render. If it has `showFilters`, `showTimeRange`, `showAutoRefresh`, `showSearch` props — set them all to `false` in `Layout.tsx`.
**Option B: Remove providers that feed the controls.**
Don't wrap the platform app in `GlobalFilterProvider` and `CommandPaletteProvider` (in `main.tsx`). This may cause runtime errors if TopBar assumes they exist — test carefully.
**Option C: Custom simplified header.**
Replace `TopBar` with a simpler platform-specific header that only renders: breadcrumb, theme toggle, user menu. Use DS primitives (`Breadcrumb`, `Avatar`, `Dropdown`, `Button`) to compose it.
Investigate which option is viable by checking the DS `TopBar` component API.
**Files:** `ui/src/components/Layout.tsx`, possibly `ui/src/main.tsx`
### 2.2 Fix Sidebar Active State
**Problem:** `Sidebar.Section` used as navigation links via `onToggle` hack. No `active` prop set. Users can't tell which page they're on.
**Fix:** Pass `active={true}` to the current page's `Sidebar.Section` based on the route:
```tsx
const location = useLocation();
const isActive = (path: string) => location.pathname === path || location.pathname === path + '/';
}
label="Dashboard"
open={false}
active={isActive('/') || isActive('/platform')}
onToggle={() => navigate('/')}
>
{null}
}
label="License"
open={false}
active={isActive('/license')}
onToggle={() => navigate('/license')}
>
{null}
```
Check the DS `Sidebar.Section` props — if `active` doesn't exist on Section, check if there's a `Sidebar.Link` or `Sidebar.NavItem` component that supports it.
**Files:** `ui/src/components/Layout.tsx`
### 2.3 Add Breadcrumbs
**Problem:** `breadcrumb={[]}` is always empty.
**Fix:** Set breadcrumbs per page:
```tsx
// Layout.tsx:
const location = useLocation();
const breadcrumb = useMemo(() => {
if (location.pathname.includes('/license')) return [{ label: 'License' }];
if (location.pathname.includes('/admin')) return [{ label: 'Admin' }, { label: 'Tenants' }];
return [{ label: 'Dashboard' }];
}, [location.pathname]);
```
Check the DS `TopBar` breadcrumb prop type to match the expected shape.
**Files:** `ui/src/components/Layout.tsx`
### 2.4 Remove One "Open Server Dashboard" Button
**Problem:** 3 locations for the same action.
**Fix:** Keep:
1. Sidebar footer link (always accessible)
2. Server Management card on Dashboard (contextual with description)
Remove: The primary "Open Server Dashboard" button in the header area of DashboardPage (line ~81-87).
**Files:** `ui/src/pages/DashboardPage.tsx`
### 2.5 Fix Sidebar Collapse
**Problem:** `collapsed={false}` hardcoded, `onCollapseToggle` is no-op.
**Fix:** Add state:
```tsx
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
setSidebarCollapsed(c => !c)}>
```
**Files:** `ui/src/components/Layout.tsx`
---
## Batch 3: Error Handling & Components
**Effort:** 1 day
### 3.1 OrgResolver Error State
**Problem:** Returns `null` on error — blank screen with sidebar/TopBar but no content.
**Fix:** Replace `return null` with an error display:
```tsx
if (isError) return (
refetch()}>Retry}
/>
);
```
Import `EmptyState` and `Button` from DS.
**Files:** `ui/src/auth/OrgResolver.tsx`
### 3.2 DashboardPage Error Handling
**Problem:** No `isError` check. Silently renders with `-` fallback values.
**Fix:** Add error state similar to LicensePage:
```tsx
if (tenantError || licenseError) return (
);
```
**Files:** `ui/src/pages/DashboardPage.tsx`
### 3.3 Replace Raw HTML with DS Components
**Problem:** LicensePage uses raw `