Add SaaS platform UX polish implementation plan (8 tasks)
Detailed step-by-step plan covering layout fixes (label/value collision, DS variable adoption), header/navigation (sidebar active state, breadcrumbs, collapse), error handling, DS component adoption, sign-in improvements, and polish (tier colors, badges, confirmations). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
760
docs/superpowers/plans/2026-04-09-saas-ux-polish-plan.md
Normal file
760
docs/superpowers/plans/2026-04-09-saas-ux-polish-plan.md
Normal file
@@ -0,0 +1,760 @@
|
||||
# SaaS Platform UX Polish — 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:** Fix layout bugs, replace hardcoded dark-only colors with design system tokens, improve navigation/header, add error handling, and adopt design system components consistently across the SaaS platform UI.
|
||||
|
||||
**Architecture:** All changes are in the existing SaaS platform UI (`ui/src/`) and sign-in page (`ui/sign-in/src/`). The platform uses `@cameleer/design-system` components and Tailwind CSS. The key issue is that pages use hardcoded `text-white` Tailwind classes instead of DS CSS variables, and the DS `TopBar` renders server-specific controls that are irrelevant on platform pages.
|
||||
|
||||
**Tech Stack:** React 19, TypeScript, Tailwind CSS, `@cameleer/design-system`, React Router v6, Logto SDK
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-09-saas-ux-polish-design.md`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Fix label/value collision and replace hardcoded colors
|
||||
|
||||
**Spec items:** 1.1, 1.2
|
||||
|
||||
**Files:**
|
||||
- Create: `ui/src/styles/platform.module.css`
|
||||
- Modify: `ui/src/pages/DashboardPage.tsx`
|
||||
- Modify: `ui/src/pages/LicensePage.tsx`
|
||||
- Modify: `ui/src/pages/AdminTenantsPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Create shared platform CSS module**
|
||||
|
||||
Create `ui/src/styles/platform.module.css` with DS-variable-based classes replacing the hardcoded Tailwind colors:
|
||||
|
||||
```css
|
||||
.heading {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.textPrimary {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.textSecondary {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.textMuted {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.kvRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.kvLabel {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.kvValue {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.kvValueMono {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.dividerList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dividerList > * + * {
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.dividerRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
}
|
||||
|
||||
.dividerRow:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.dividerRow:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tokenBlock {
|
||||
margin-top: 0.5rem;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-inset);
|
||||
border: 1px solid var(--border-subtle);
|
||||
padding: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.tokenCode {
|
||||
font-size: 0.75rem;
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-secondary);
|
||||
word-break: break-all;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update DashboardPage to use CSS module + fix label/value**
|
||||
|
||||
In `ui/src/pages/DashboardPage.tsx`:
|
||||
|
||||
1. Add import:
|
||||
```typescript
|
||||
import s from '../styles/platform.module.css';
|
||||
```
|
||||
|
||||
2. Replace all hardcoded color classes:
|
||||
- Line 71: `text-2xl font-semibold text-white` → `className={s.heading}`
|
||||
- Lines 96, 100, 107: `className="flex justify-between text-white/80"` → `className={s.kvRow}`
|
||||
- Inner label spans: wrap with `className={s.kvLabel}`
|
||||
- Inner value spans: wrap with `className={s.kvValueMono}` (for mono) or `className={s.kvValue}`
|
||||
- Line 116: `text-sm text-white/60` → `className={s.description}`
|
||||
|
||||
3. The label/value collision fix: the `kvRow` class uses explicit `display: flex; width: 100%; justify-content: space-between` which ensures the flex container stretches to full Card width regardless of Card's inner layout.
|
||||
|
||||
- [ ] **Step 3: Update LicensePage to use CSS module**
|
||||
|
||||
In `ui/src/pages/LicensePage.tsx`:
|
||||
|
||||
1. Add import: `import s from '../styles/platform.module.css';`
|
||||
|
||||
2. Replace all hardcoded color classes:
|
||||
- Line 85: heading → `className={s.heading}`
|
||||
- Lines 95-115 (Validity rows): `flex items-center justify-between` → `className={s.kvRow}`, labels → `className={s.kvLabel}`, values → `className={s.kvValue}`
|
||||
- Lines 121-136 (Features): `divide-y divide-white/10` → `className={s.dividerList}`, rows → `className={s.dividerRow}`, feature name `text-sm text-white` → `className={s.textPrimary}` + `text-sm`
|
||||
- Lines 142-157 (Limits): same dividerList/dividerRow pattern, label → `className={s.kvLabel}`, value → `className={s.kvValueMono}`
|
||||
- Line 163: description text → `className={s.description}`
|
||||
- Lines 174-178: token code block → `className={s.tokenBlock}` on outer div, `className={s.tokenCode}` on code element
|
||||
|
||||
- [ ] **Step 4: Update AdminTenantsPage to use CSS module**
|
||||
|
||||
In `ui/src/pages/AdminTenantsPage.tsx`:
|
||||
- Line 62: `text-2xl font-semibold text-white` → `className={s.heading}`
|
||||
|
||||
- [ ] **Step 5: Verify in both themes**
|
||||
|
||||
1. Open the platform dashboard in browser
|
||||
2. Check label/value pairs have proper spacing (Slug on left, "default" on right)
|
||||
3. Toggle to light theme via TopBar toggle
|
||||
4. Verify all text is readable in light mode (no invisible white-on-white)
|
||||
5. Toggle back to dark mode — should look the same as before
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/styles/platform.module.css ui/src/pages/DashboardPage.tsx ui/src/pages/LicensePage.tsx ui/src/pages/AdminTenantsPage.tsx
|
||||
git commit -m "fix: replace hardcoded text-white with DS variables, fix label/value layout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: Remove redundant dashboard elements
|
||||
|
||||
**Spec items:** 1.3, 2.4
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/src/pages/DashboardPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Remove primary "Open Server Dashboard" button from header**
|
||||
|
||||
In `ui/src/pages/DashboardPage.tsx`, find the header area (lines ~75-88). Remove the primary Button for "Open Server Dashboard" (lines ~81-87). Keep:
|
||||
- The Server Management Card with its secondary button (lines ~113-126)
|
||||
- The sidebar footer link (in Layout.tsx — don't touch)
|
||||
|
||||
The header area should just have the tenant name heading + tier badge, no button.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/pages/DashboardPage.tsx
|
||||
git commit -m "fix: remove redundant Open Server Dashboard button from dashboard header"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Fix header controls and sidebar navigation
|
||||
|
||||
**Spec items:** 2.1, 2.2, 2.3, 2.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/src/components/Layout.tsx`
|
||||
- Modify: `ui/src/main.tsx` (possibly)
|
||||
|
||||
- [ ] **Step 1: Investigate TopBar props for hiding controls**
|
||||
|
||||
The DS `TopBar` interface (from types):
|
||||
```typescript
|
||||
interface TopBarProps {
|
||||
breadcrumb: BreadcrumbItem[];
|
||||
environment?: ReactNode;
|
||||
user?: { name: string };
|
||||
onLogout?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
```
|
||||
|
||||
The TopBar has NO props to hide status filters, time range, auto-refresh, or search. These are hardcoded inside the component.
|
||||
|
||||
**Options:**
|
||||
1. Check if removing `GlobalFilterProvider` and `CommandPaletteProvider` from `main.tsx` makes TopBar gracefully hide those sections (test this first)
|
||||
2. If that causes errors, add `display: none` CSS overrides for the irrelevant sections
|
||||
3. If neither works, build a simplified platform header
|
||||
|
||||
Try option 1 first. In `main.tsx`, remove `GlobalFilterProvider` and `CommandPaletteProvider` from the provider stack. Test if the app still renders. If TopBar crashes without them, revert and try option 2.
|
||||
|
||||
- [ ] **Step 2: Add sidebar active state**
|
||||
|
||||
In `ui/src/components/Layout.tsx`, add route-based active state:
|
||||
|
||||
```typescript
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
// Inside the Layout component:
|
||||
const location = useLocation();
|
||||
```
|
||||
|
||||
Update each `Sidebar.Section`:
|
||||
```tsx
|
||||
<Sidebar.Section
|
||||
icon={<DashboardIcon />}
|
||||
label="Dashboard"
|
||||
open={false}
|
||||
active={location.pathname === '/' || location.pathname === ''}
|
||||
onToggle={() => navigate('/')}
|
||||
>
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
|
||||
<Sidebar.Section
|
||||
icon={<LicenseIcon />}
|
||||
label="License"
|
||||
open={false}
|
||||
active={location.pathname === '/license'}
|
||||
onToggle={() => navigate('/license')}
|
||||
>
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
|
||||
{scopes.has('platform:admin') && (
|
||||
<Sidebar.Section
|
||||
icon={<PlatformIcon />}
|
||||
label="Platform"
|
||||
open={false}
|
||||
active={location.pathname.startsWith('/admin')}
|
||||
onToggle={() => navigate('/admin/tenants')}
|
||||
>
|
||||
{null}
|
||||
</Sidebar.Section>
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add breadcrumbs**
|
||||
|
||||
In Layout.tsx, compute breadcrumbs from the current route:
|
||||
|
||||
```typescript
|
||||
const breadcrumb = useMemo((): BreadcrumbItem[] => {
|
||||
const path = location.pathname;
|
||||
if (path.startsWith('/admin')) return [{ label: 'Admin' }, { label: 'Tenants' }];
|
||||
if (path.startsWith('/license')) return [{ label: 'License' }];
|
||||
return [{ label: 'Dashboard' }];
|
||||
}, [location.pathname]);
|
||||
```
|
||||
|
||||
Pass to TopBar:
|
||||
```tsx
|
||||
<TopBar breadcrumb={breadcrumb} ... />
|
||||
```
|
||||
|
||||
Import `BreadcrumbItem` type from `@cameleer/design-system` if needed.
|
||||
|
||||
- [ ] **Step 4: Fix sidebar collapse**
|
||||
|
||||
Replace the hardcoded collapse state:
|
||||
```typescript
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
```
|
||||
|
||||
```tsx
|
||||
<Sidebar collapsed={sidebarCollapsed} onCollapseToggle={() => setSidebarCollapsed(c => !c)}>
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Fix username null fallback**
|
||||
|
||||
Update the user prop (line ~125):
|
||||
```tsx
|
||||
const displayName = username || 'User';
|
||||
|
||||
<TopBar
|
||||
breadcrumb={breadcrumb}
|
||||
user={{ name: displayName }}
|
||||
onLogout={logout}
|
||||
/>
|
||||
```
|
||||
|
||||
This ensures the logout button is always visible.
|
||||
|
||||
- [ ] **Step 6: Replace custom SVG icons with lucide-react**
|
||||
|
||||
Replace the 4 custom SVG icon components (lines 25-62) with lucide-react icons:
|
||||
|
||||
```typescript
|
||||
import { LayoutDashboard, ShieldCheck, Building, Server } from 'lucide-react';
|
||||
```
|
||||
|
||||
Then update sidebar sections:
|
||||
```tsx
|
||||
icon={<LayoutDashboard size={18} />} // was <DashboardIcon />
|
||||
icon={<ShieldCheck size={18} />} // was <LicenseIcon />
|
||||
icon={<Building size={18} />} // was <PlatformIcon />
|
||||
```
|
||||
|
||||
Remove the 4 custom SVG component functions (DashboardIcon, LicenseIcon, ObsIcon, PlatformIcon).
|
||||
|
||||
- [ ] **Step 7: Verify**
|
||||
|
||||
1. Sidebar shows active highlight on current page
|
||||
2. Breadcrumbs show "Dashboard", "License", or "Admin > Tenants"
|
||||
3. Sidebar collapse works (click collapse button, sidebar minimizes)
|
||||
4. User avatar/logout always visible
|
||||
5. Icons render correctly from lucide-react
|
||||
6. Check if server controls are hidden (depending on step 1 result)
|
||||
|
||||
- [ ] **Step 8: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/components/Layout.tsx ui/src/main.tsx
|
||||
git commit -m "fix: sidebar active state, breadcrumbs, collapse, username fallback, lucide icons"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Error handling and OrgResolver fix
|
||||
|
||||
**Spec items:** 3.1, 3.2, 3.7
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/src/auth/OrgResolver.tsx`
|
||||
- Modify: `ui/src/pages/DashboardPage.tsx`
|
||||
- Modify: `ui/src/pages/AdminTenantsPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Fix OrgResolver error state**
|
||||
|
||||
In `ui/src/auth/OrgResolver.tsx`, find the error handling (lines 88-90):
|
||||
|
||||
```tsx
|
||||
// BEFORE:
|
||||
if (isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// AFTER:
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-64 gap-4">
|
||||
<EmptyState
|
||||
title="Unable to load account"
|
||||
description="Failed to retrieve your organization. Please try again or contact support."
|
||||
/>
|
||||
<Button variant="secondary" size="sm" onClick={() => refetch()}>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Add imports: `EmptyState`, `Button` from `@cameleer/design-system`. Ensure `refetch` is available from the query hook (check if `useQuery` returns it).
|
||||
|
||||
- [ ] **Step 2: Add error handling to DashboardPage**
|
||||
|
||||
In `ui/src/pages/DashboardPage.tsx`, after the loading check (line ~49) and tenant check (line ~57), add error handling:
|
||||
|
||||
```tsx
|
||||
const { data: tenant, isError: tenantError } = useTenant();
|
||||
const { data: license, isError: licenseError } = useLicense();
|
||||
|
||||
// After loading spinner check:
|
||||
if (tenantError || licenseError) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<EmptyState
|
||||
title="Unable to load dashboard"
|
||||
description="Failed to retrieve tenant information. Please try again later."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Check how `useTenant()` and `useLicense()` expose error state — they may use `isError` from React Query.
|
||||
|
||||
- [ ] **Step 3: Add empty state and date formatting to AdminTenantsPage**
|
||||
|
||||
In `ui/src/pages/AdminTenantsPage.tsx`:
|
||||
|
||||
1. Add error handling:
|
||||
```tsx
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<EmptyState
|
||||
title="Unable to load tenants"
|
||||
description="You may not have admin permissions, or the server is unavailable."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
2. Format the `createdAt` column (line 31):
|
||||
```tsx
|
||||
// BEFORE:
|
||||
{ key: 'createdAt', header: 'Created' },
|
||||
|
||||
// AFTER:
|
||||
{ key: 'createdAt', header: 'Created', render: (_, row) => new Date(row.createdAt).toLocaleDateString() },
|
||||
```
|
||||
|
||||
3. Add empty state to DataTable (if supported) or show EmptyState when tenants is empty:
|
||||
```tsx
|
||||
{(!tenants || tenants.length === 0) ? (
|
||||
<EmptyState title="No tenants" description="No tenants have been created yet." />
|
||||
) : (
|
||||
<DataTable columns={columns} data={tenants} onRowClick={handleRowClick} />
|
||||
)}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/auth/OrgResolver.tsx ui/src/pages/DashboardPage.tsx ui/src/pages/AdminTenantsPage.tsx
|
||||
git commit -m "fix: add error states to OrgResolver, DashboardPage, AdminTenantsPage"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: DS component adoption and license token copy
|
||||
|
||||
**Spec items:** 3.3, 3.4
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/src/pages/LicensePage.tsx`
|
||||
|
||||
- [ ] **Step 1: Replace raw button with DS Button**
|
||||
|
||||
In `ui/src/pages/LicensePage.tsx`, find the token toggle button (lines ~166-172):
|
||||
|
||||
```tsx
|
||||
// BEFORE:
|
||||
<button
|
||||
type="button"
|
||||
className="text-sm text-primary-400 hover:text-primary-300 underline underline-offset-2 focus:outline-none"
|
||||
onClick={() => setTokenExpanded((v) => !v)}
|
||||
>
|
||||
{tokenExpanded ? 'Hide token' : 'Show token'}
|
||||
</button>
|
||||
|
||||
// AFTER:
|
||||
<Button variant="ghost" size="sm" onClick={() => setTokenExpanded((v) => !v)}>
|
||||
{tokenExpanded ? 'Hide token' : 'Show token'}
|
||||
</Button>
|
||||
```
|
||||
|
||||
Ensure `Button` is imported from `@cameleer/design-system`.
|
||||
|
||||
- [ ] **Step 2: Add copy-to-clipboard button**
|
||||
|
||||
Add `useToast` import and `Copy` icon:
|
||||
```typescript
|
||||
import { useToast } from '@cameleer/design-system';
|
||||
import { Copy } from 'lucide-react';
|
||||
```
|
||||
|
||||
Add toast hook in component:
|
||||
```typescript
|
||||
const { toast } = useToast();
|
||||
```
|
||||
|
||||
Next to the show/hide button, add a copy button (only when expanded):
|
||||
```tsx
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<Button variant="ghost" size="sm" onClick={() => setTokenExpanded((v) => !v)}>
|
||||
{tokenExpanded ? 'Hide token' : 'Show token'}
|
||||
</Button>
|
||||
{tokenExpanded && (
|
||||
<Button variant="ghost" size="sm" onClick={() => {
|
||||
navigator.clipboard.writeText(license.token);
|
||||
toast({ title: 'Token copied to clipboard', variant: 'success' });
|
||||
}}>
|
||||
<Copy size={14} /> Copy
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/pages/LicensePage.tsx
|
||||
git commit -m "fix: replace raw button with DS Button, add token copy-to-clipboard"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Sign-in page improvements
|
||||
|
||||
**Spec items:** 3.6, 4.5
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/sign-in/src/SignInPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Add password visibility toggle**
|
||||
|
||||
In `ui/sign-in/src/SignInPage.tsx`, add state and imports:
|
||||
|
||||
```typescript
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
```
|
||||
|
||||
Update the password FormField (lines ~84-94):
|
||||
```tsx
|
||||
<FormField label="Password" htmlFor="login-password">
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Input
|
||||
id="login-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
autoComplete="current-password"
|
||||
disabled={loading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
style={{
|
||||
position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)',
|
||||
background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-muted)',
|
||||
padding: 4, display: 'flex', alignItems: 'center',
|
||||
}}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</button>
|
||||
</div>
|
||||
</FormField>
|
||||
```
|
||||
|
||||
Note: Using a raw `<button>` here because the sign-in page may not have the full DS Button available (it's a separate Vite build). Use inline styles for positioning since the sign-in page uses CSS modules.
|
||||
|
||||
- [ ] **Step 2: Fix branding text**
|
||||
|
||||
In `ui/sign-in/src/SignInPage.tsx`, find the logo text (line ~61):
|
||||
|
||||
```tsx
|
||||
// BEFORE:
|
||||
<div className={styles.logo}>
|
||||
<img src={cameleerLogo} alt="" className={styles.logoImg} />
|
||||
cameleer3
|
||||
</div>
|
||||
|
||||
// AFTER:
|
||||
<div className={styles.logo}>
|
||||
<img src={cameleerLogo} alt="" className={styles.logoImg} />
|
||||
Cameleer
|
||||
</div>
|
||||
```
|
||||
|
||||
Also update the page title if it's set anywhere (check `index.html` in `ui/sign-in/`):
|
||||
```html
|
||||
<title>Sign in — Cameleer</title>
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/sign-in/src/SignInPage.tsx ui/sign-in/index.html
|
||||
git commit -m "fix: add password visibility toggle and fix branding to 'Cameleer'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Unify tier colors and fix badges
|
||||
|
||||
**Spec items:** 4.1, 4.2
|
||||
|
||||
**Files:**
|
||||
- Create: `ui/src/utils/tier.ts`
|
||||
- Modify: `ui/src/pages/DashboardPage.tsx`
|
||||
- Modify: `ui/src/pages/LicensePage.tsx`
|
||||
|
||||
- [ ] **Step 1: Create shared tier utility**
|
||||
|
||||
Create `ui/src/utils/tier.ts`:
|
||||
|
||||
```typescript
|
||||
export type TierColor = 'primary' | 'success' | 'warning' | 'error' | 'auto';
|
||||
|
||||
export function tierColor(tier: string): TierColor {
|
||||
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';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Replace local tierColor in both pages**
|
||||
|
||||
In `DashboardPage.tsx`, remove the local `tierColor` function (lines 12-19) and add:
|
||||
```typescript
|
||||
import { tierColor } from '../utils/tier';
|
||||
```
|
||||
|
||||
In `LicensePage.tsx`, remove the local `tierColor` function (lines 25-33) and add:
|
||||
```typescript
|
||||
import { tierColor } from '../utils/tier';
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Fix feature badge color**
|
||||
|
||||
In `LicensePage.tsx`, find the feature badge (line ~131-132):
|
||||
|
||||
```tsx
|
||||
// BEFORE:
|
||||
color={enabled ? 'success' : 'auto'}
|
||||
|
||||
// Check what neutral badge colors the DS supports.
|
||||
// If 'auto' hashes to inconsistent colors, use a fixed muted option.
|
||||
// AFTER:
|
||||
color={enabled ? 'success' : 'warning'}
|
||||
```
|
||||
|
||||
Use `'warning'` (amber/muted) for "Not included" — it's neutral without implying error. If the DS has a better neutral option, use that.
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/utils/tier.ts ui/src/pages/DashboardPage.tsx ui/src/pages/LicensePage.tsx
|
||||
git commit -m "fix: unify tier color mapping, fix feature badge colors"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: AdminTenantsPage confirmation and polish
|
||||
|
||||
**Spec items:** 4.3
|
||||
|
||||
**Files:**
|
||||
- Modify: `ui/src/pages/AdminTenantsPage.tsx`
|
||||
|
||||
- [ ] **Step 1: Add confirmation before tenant context switch**
|
||||
|
||||
In `ui/src/pages/AdminTenantsPage.tsx`, add state and import:
|
||||
|
||||
```typescript
|
||||
import { AlertDialog } from '@cameleer/design-system';
|
||||
|
||||
const [switchTarget, setSwitchTarget] = useState<TenantResponse | null>(null);
|
||||
```
|
||||
|
||||
Update the row click handler:
|
||||
```tsx
|
||||
// BEFORE:
|
||||
const handleRowClick = (tenant: TenantResponse) => {
|
||||
const orgs = useOrgStore.getState().organizations;
|
||||
const match = orgs.find((o) => o.name === tenant.name || o.slug === tenant.slug);
|
||||
if (match) {
|
||||
setCurrentOrg(match.id);
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
// AFTER:
|
||||
const handleRowClick = (tenant: TenantResponse) => {
|
||||
setSwitchTarget(tenant);
|
||||
};
|
||||
|
||||
const confirmSwitch = () => {
|
||||
if (!switchTarget) return;
|
||||
const orgs = useOrgStore.getState().organizations;
|
||||
const match = orgs.find((o) => o.name === switchTarget.name || o.slug === switchTarget.slug);
|
||||
if (match) {
|
||||
setCurrentOrg(match.id);
|
||||
navigate('/');
|
||||
}
|
||||
setSwitchTarget(null);
|
||||
};
|
||||
```
|
||||
|
||||
Add the AlertDialog at the bottom of the component return:
|
||||
```tsx
|
||||
<AlertDialog
|
||||
open={!!switchTarget}
|
||||
onCancel={() => setSwitchTarget(null)}
|
||||
onConfirm={confirmSwitch}
|
||||
title="Switch tenant?"
|
||||
description={`Switch to tenant "${switchTarget?.name}"? Your dashboard context will change.`}
|
||||
confirmLabel="Switch"
|
||||
variant="warning"
|
||||
/>
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add ui/src/pages/AdminTenantsPage.tsx
|
||||
git commit -m "fix: add confirmation dialog before tenant context switch"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Batch | Key Changes | Commit |
|
||||
|------|-------|-------------|--------|
|
||||
| 1 | Layout | CSS module with DS variables, fix label/value, replace text-white | `fix: replace hardcoded text-white with DS variables, fix label/value layout` |
|
||||
| 2 | Layout | Remove redundant "Open Server Dashboard" button | `fix: remove redundant Open Server Dashboard button` |
|
||||
| 3 | Navigation | Sidebar active state, breadcrumbs, collapse, username fallback, lucide icons | `fix: sidebar active state, breadcrumbs, collapse, username fallback, lucide icons` |
|
||||
| 4 | Error Handling | OrgResolver error UI, DashboardPage error state, AdminTenantsPage error + date format | `fix: add error states to OrgResolver, DashboardPage, AdminTenantsPage` |
|
||||
| 5 | Components | DS Button for token toggle, copy-to-clipboard with toast | `fix: replace raw button with DS Button, add token copy-to-clipboard` |
|
||||
| 6 | Sign-in | Password visibility toggle, branding fix to "Cameleer" | `fix: add password visibility toggle and fix branding to 'Cameleer'` |
|
||||
| 7 | Polish | Shared tierColor(), fix feature badge colors | `fix: unify tier color mapping, fix feature badge colors` |
|
||||
| 8 | Polish | Confirmation dialog for admin tenant switch | `fix: add confirmation dialog before tenant context switch` |
|
||||
Reference in New Issue
Block a user