# 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 ` // AFTER: ``` Replace raw code block (line ~174-178) with DS `CodeBlock` if available, or at minimum use DS CSS variables instead of hardcoded Tailwind colors. **Files:** `ui/src/pages/LicensePage.tsx` ### 3.4 Add Copy-to-Clipboard for License Token **Problem:** Users must manually select and copy the token. **Fix:** Add a copy button next to the token: ```tsx ``` Import `Copy` from `lucide-react` and `useToast` from DS. **Files:** `ui/src/pages/LicensePage.tsx` ### 3.5 Fix Username Null = No Logout **Problem:** When `username` is null, no user indicator or logout button appears. **Fix:** Always pass a user object to TopBar — fallback to email or "User": ```tsx const displayName = username || user?.email || 'User'; ``` **Files:** `ui/src/components/Layout.tsx` ### 3.6 Add Password Visibility Toggle to Sign-In **Problem:** No eye icon to reveal password. **Fix:** The DS `Input` component may support a `type` toggle. If not, wrap with a show/hide toggle: ```tsx const [showPassword, setShowPassword] = useState(false);
``` **Files:** `ui/sign-in/src/SignInPage.tsx` ### 3.7 Admin Page Error Fallback **Problem:** `/platform/admin/tenants` returns HTTP error with no graceful fallback. **Fix:** Add error boundary or error state in AdminTenantsPage: ```tsx if (isError) return ( ); ``` **Files:** `ui/src/pages/AdminTenantsPage.tsx` --- ## Batch 4: Polish **Effort:** 0.5 days ### 4.1 Unify `tierColor()` Mapping **Problem:** Defined twice with different tier names: - `DashboardPage.tsx:12-18` maps enterprise/pro/starter - `LicensePage.tsx:25-33` maps BUSINESS/HIGH/MID/LOW **Fix:** Extract a single `tierColor()` to a shared utility (`ui/src/utils/tier.ts`). Map all known tier names: ```typescript export function tierColor(tier: string): BadgeColor { switch (tier?.toUpperCase()) { case 'BUSINESS': case 'ENTERPRISE': return 'success'; case 'HIGH': case 'PRO': return 'primary'; case 'MID': case 'STARTER': return 'warning'; case 'LOW': case 'FREE': return 'auto'; default: return 'auto'; } } ``` Import from both pages. **Files:** New `ui/src/utils/tier.ts`, modify `DashboardPage.tsx`, `LicensePage.tsx` ### 4.2 Fix Feature Badge Colors **Problem:** Disabled features use `color="auto"` (hash-based, inconsistent). Should use muted neutral. **Fix:** Check if DS Badge supports a `neutral` or `default` color variant. If not, use the closest muted option. The goal: enabled = green success, disabled = gray/muted (not red, not random). **Files:** `ui/src/pages/LicensePage.tsx` ### 4.3 AdminTenantsPage Improvements **Problem:** Row click silently switches tenant context. `createdAt` renders raw ISO. No empty state. **Fix:** - Add confirmation before tenant switch: `if (!confirm('Switch to tenant "X"?')) return;` (or DS AlertDialog) - Format date: `new Date(row.createdAt).toLocaleDateString()` - Add empty state: `` **Files:** `ui/src/pages/AdminTenantsPage.tsx` ### 4.4 Replace Custom SVG Icons with Lucide **Problem:** Layout.tsx has 4 inline SVG icon components instead of using lucide-react. **Fix:** Replace with lucide icons: - `DashboardIcon` -> `` - `LicenseIcon` -> `` - `PlatformIcon` -> `` - `ServerIcon` -> `` Import from `lucide-react`. **Files:** `ui/src/components/Layout.tsx` ### 4.5 Sign-In Branding **Problem:** Login says "cameleer" — internal repo name, not product brand. **Fix:** Change to "Cameleer" (product name). Update the page title from "Sign in — cameleer" to "Sign in — Cameleer". **Files:** `ui/sign-in/src/SignInPage.tsx` --- ## Implementation Order | Order | Batch | Items | Effort | |-------|-------|-------|--------| | 1 | **Batch 1: Layout Fixes** | 3 | 0.5 days | | 2 | **Batch 2: Header & Navigation** | 5 | 1 day | | 3 | **Batch 3: Error Handling & Components** | 7 | 1 day | | 4 | **Batch 4: Polish** | 5 | 0.5 days | **Total: ~20 items across 4 batches, ~3 days of work.** --- ## Related Issues | Issue | Relevance | |-------|-----------| | #1 | Epic: SaaS Management Platform — this spec covers polish only | | #37 | Admin: Tenant creation UI — not covered (feature work) | | #38 | Cross-app session management — not covered (parked) | ## Out of Scope - Mobile responsiveness (deferred per user request) - New features (billing, team management, tenant creation) - Admin tenant CRUD workflow (#37) - Cross-app session sync (#38)