Assist_Design/docs/plans/2026-03-05-portal-ui-cleanup-plan.md
barsa cab58d1c5b refactor: streamline component layouts and enhance navigation
- Updated the AppShell and Sidebar components for improved layout and spacing.
- Replaced font colors in the Logo component for better visibility.
- Adjusted the PageLayout component to utilize backLink props instead of breadcrumbs for navigation consistency.
- Removed unnecessary description props from multiple PageLayout instances across various views to simplify the codebase.
- Introduced SectionCard component in OrderDetail for better organization of billing information.
- Enhanced utility styles in CSS for improved typography and layout consistency.
2026-03-06 10:45:51 +09:00

22 KiB

Portal UI Cleanup Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Unify all portal account pages with consistent styling, fix sidebar brand alignment, simplify navigation, and standardize list/detail page patterns.

Architecture: Update PageLayout template to be the single source of truth for page headers (bg-muted/40 background, backLink support, no breadcrumbs). Create a reusable SectionCard molecule for detail pages. Update all views to use these patterns consistently.

Tech Stack: Next.js 15, React 19, Tailwind CSS v4, CSS custom properties (OKLCH), shadcn/ui patterns


Files:

  • Modify: apps/portal/src/app/globals.css (lines 83-88 for sidebar tokens, lines 197-199 for dark mode)
  • Modify: apps/portal/src/components/atoms/logo.tsx (lines 27-33 for SVG colors)
  • Modify: apps/portal/src/components/organisms/AppShell/Sidebar.tsx (lines 66-76 for logo container)

Step 1: Update sidebar CSS tokens

In globals.css, change the sidebar variables in :root:

/* Before */
--sidebar: oklch(0.18 0.03 250);
--sidebar-border: oklch(0.25 0.04 250);

/* After — deeper primary blue hue (234.4), more saturated */
--sidebar: oklch(0.2 0.06 234.4);
--sidebar-border: oklch(0.27 0.05 234.4);

In .dark, update sidebar:

/* Before */
--sidebar: oklch(0.13 0.025 250);
--sidebar-border: oklch(0.22 0.03 250);

/* After */
--sidebar: oklch(0.14 0.04 234.4);
--sidebar-border: oklch(0.22 0.04 234.4);

Step 2: Update logo SVG fallback to white

In logo.tsx, change the SVG path fills:

// Before
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#60A5FA" />
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#60A5FA" />
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="#1E40AF" />
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="#1E40AF" />

// After
<path d="M8 8 C12 4, 20 4, 24 8 L20 12 C18 10, 14 10, 12 12 Z" fill="#ffffff" />
<path d="M24 8 C28 12, 28 20, 24 24 L20 20 C22 18, 22 14, 20 12 Z" fill="#ffffff" />
<path d="M8 24 C12 28, 20 28, 24 24 L20 20 C18 22, 14 22, 12 20 Z" fill="rgba(255,255,255,0.6)" />
<path d="M8 24 C4 20, 4 12, 8 8 L12 12 C10 14, 10 18, 12 20 Z" fill="rgba(255,255,255,0.6)" />

Step 3: Simplify logo container in Sidebar

In Sidebar.tsx, remove the bg-white/10 container around the logo:

// Before (lines 66-76)
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
  <div className="flex items-center space-x-3">
    <div className="h-9 w-9 bg-white/10 backdrop-blur-sm rounded-lg border border-white/10 flex items-center justify-center">
      <Logo size={22} />
    </div>
    <div>
      <span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
      <p className="text-[11px] text-white/50 font-medium">Customer Portal</p>
    </div>
  </div>
</div>

// After — logo sits directly, no glass container
<div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border/50">
  <div className="flex items-center space-x-3">
    <Logo size={28} />
    <div>
      <span className="text-sm font-bold text-white tracking-tight">Assist Solutions</span>
      <p className="text-[11px] text-white/50 font-medium">Customer Portal</p>
    </div>
  </div>
</div>

Step 4: Verify visually

Run: pnpm --filter @customer-portal/portal dev (with user permission) Check: Sidebar color is deeper blue matching brand, logo is clearly visible white, text remains readable.

Step 5: Commit

style: align sidebar color with brand blue and use white logo

Task 2: Simplify Subscriptions Navigation

Files:

  • Modify: apps/portal/src/components/organisms/AppShell/navigation.ts (lines 29-94)
  • Modify: apps/portal/src/components/organisms/AppShell/AppShell.tsx (remove subscription data dependency)

Step 1: Change Subscriptions to a direct link

In navigation.ts, change the Subscriptions entry from expandable to direct:

// Before (lines 41-44)
{
  name: "Subscriptions",
  icon: ServerIcon,
  children: [{ name: "All Subscriptions", href: "/account/subscriptions" }],
},

// After
{
  name: "Subscriptions",
  href: "/account/subscriptions",
  icon: ServerIcon,
},

Step 2: Remove computeNavigation function

Delete the computeNavigation function (lines 63-89) and the truncate helper (lines 91-94). Export only baseNavigation.

Step 3: Remove the Subscription import

Remove line 1: import type { Subscription } from "@customer-portal/domain/subscriptions";

This also fixes the TypeScript error Cannot find module '@customer-portal/domain/subscriptions'.

Step 4: Update AppShell to use baseNavigation directly

In AppShell.tsx, find where computeNavigation is called and replace with baseNavigation:

// Before
import { computeNavigation } from "./navigation";
// ... later
const navigation = computeNavigation(activeSubscriptions);

// After
import { baseNavigation } from "./navigation";
// ... later — use baseNavigation directly, remove activeSubscriptions fetch

Remove any hook/fetch for active subscriptions that was only used for sidebar navigation.

Step 5: Verify

Run: pnpm type-check Expected: No errors related to navigation.ts or AppShell.tsx

Step 6: Commit

refactor: simplify subscriptions to direct sidebar link

Task 3: Update PageLayout Template

Files:

  • Modify: apps/portal/src/components/templates/PageLayout/PageLayout.tsx

Step 1: Update PageLayout interface

// Remove BreadcrumbItem export and breadcrumbs prop
// Add backLink prop

interface PageLayoutProps {
  icon?: ReactNode | undefined;
  title: string;
  description?: string | undefined; // keep prop but deprecate usage
  actions?: ReactNode | undefined;
  backLink?: { label: string; href: string } | undefined;
  statusPill?: ReactNode | undefined; // new: renders next to title
  loading?: boolean | undefined;
  loadingFallback?: ReactNode | undefined;
  error?: Error | string | null | undefined;
  onRetry?: (() => void) | undefined;
  children: ReactNode;
}

Step 2: Rewrite the header section

Replace the entire header block (lines 40-99) with:

{
  /* Back link — detail pages only */
}
{
  backLink && (
    <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 pt-[var(--cp-space-md)]">
      <Link
        href={backLink.href}
        className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors duration-200"
      >
        <ArrowLeftIcon className="h-4 w-4" />
        {backLink.label}
      </Link>
    </div>
  );
}

{
  /* Header with muted background */
}
<div className="bg-muted/40 border-b border-border/40">
  <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-4">
    <div className="flex items-center justify-between gap-4">
      {/* Left: icon + title + status */}
      <div className="flex items-center gap-3 min-w-0">
        {icon && <div className="h-7 w-7 sm:h-8 sm:w-8 text-primary flex-shrink-0">{icon}</div>}
        <div className="min-w-0">
          <div className="flex items-center gap-3">
            <h1 className="text-xl sm:text-2xl md:text-3xl font-bold text-foreground leading-tight truncate">
              {title}
            </h1>
            {statusPill}
          </div>
          {description && (
            <p className="text-sm text-muted-foreground mt-0.5 leading-relaxed line-clamp-1">
              {description}
            </p>
          )}
        </div>
      </div>

      {/* Right: actions */}
      {actions && <div className="flex items-center gap-2 sm:gap-3 flex-shrink-0">{actions}</div>}
    </div>
  </div>
</div>;

Step 3: Update outer wrapper structure

The header now has its own bg, so it should sit outside the content container. Restructure:

export function PageLayout({ ... }: PageLayoutProps) {
  return (
    <div>
      {/* Back link */}
      {backLink && ( ... )}

      {/* Header with muted background */}
      <div className="bg-muted/40 border-b border-border/40">
        <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8 py-4">
          ...header content...
        </div>
      </div>

      {/* Content */}
      <div className="py-[var(--cp-space-lg)] sm:py-[var(--cp-space-xl)]">
        <div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-md)] sm:px-[var(--cp-space-lg)] md:px-8">
          <div className="space-y-6">
            {renderPageContent({ loading, error: error ?? undefined, children, onRetry, loadingFallback })}
          </div>
        </div>
      </div>
    </div>
  );
}

Step 4: Add ArrowLeftIcon import

import { ArrowLeftIcon } from "@heroicons/react/24/outline";

Step 5: Remove old breadcrumb code and BreadcrumbItem export

Delete the BreadcrumbItem interface and all breadcrumb rendering code. Keep the ChevronRightIcon import removal too.

Step 6: Verify

Run: pnpm type-check Expected: May show errors in files still passing breadcrumbs prop — those get fixed in later tasks.

Step 7: Commit

refactor: update PageLayout with muted header bg and backLink support

Task 4: Create SectionCard Molecule

Files:

  • Create: apps/portal/src/components/molecules/SectionCard/SectionCard.tsx
  • Create: apps/portal/src/components/molecules/SectionCard/index.ts
  • Modify: apps/portal/src/components/molecules/index.ts (add export)

Step 1: Create SectionCard component

// apps/portal/src/components/molecules/SectionCard/SectionCard.tsx
import type { ReactNode } from "react";
import { cn } from "@/shared/utils";

type SectionTone = "primary" | "success" | "info" | "warning" | "danger" | "neutral";

const toneStyles: Record<SectionTone, string> = {
  primary: "bg-primary/10 text-primary",
  success: "bg-success/10 text-success",
  info: "bg-info/10 text-info",
  warning: "bg-warning/10 text-warning",
  danger: "bg-danger/10 text-danger",
  neutral: "bg-neutral/10 text-neutral",
};

interface SectionCardProps {
  icon: ReactNode;
  title: string;
  subtitle?: string | undefined;
  tone?: SectionTone;
  actions?: ReactNode | undefined;
  children: ReactNode;
  className?: string | undefined;
}

export function SectionCard({
  icon,
  title,
  subtitle,
  tone = "primary",
  actions,
  children,
  className,
}: SectionCardProps) {
  return (
    <div
      className={cn(
        "bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden",
        className
      )}
    >
      {/* Header */}
      <div className="bg-muted/40 px-6 py-4 border-b border-border/40">
        <div className="flex items-center justify-between gap-3">
          <div className="flex items-center gap-3 min-w-0">
            <div
              className={cn(
                "h-9 w-9 rounded-lg flex items-center justify-center flex-shrink-0",
                toneStyles[tone]
              )}
            >
              {icon}
            </div>
            <div className="min-w-0">
              <h3 className="text-sm font-semibold text-foreground">{title}</h3>
              {subtitle && <p className="text-xs text-muted-foreground mt-0.5">{subtitle}</p>}
            </div>
          </div>
          {actions && <div className="flex items-center gap-2 flex-shrink-0">{actions}</div>}
        </div>
      </div>

      {/* Content */}
      <div className="px-6 py-5">{children}</div>
    </div>
  );
}

Step 2: Create barrel export

// apps/portal/src/components/molecules/SectionCard/index.ts
export { SectionCard } from "./SectionCard";

Step 3: Add to molecules index

In apps/portal/src/components/molecules/index.ts, add:

export { SectionCard } from "./SectionCard";

Step 4: Verify

Run: pnpm type-check Expected: No errors.

Step 5: Commit

feat: add SectionCard molecule for unified detail page sections

Task 5: Update Subscriptions List View

Files:

  • Modify: apps/portal/src/features/subscriptions/views/SubscriptionsList.tsx
  • Modify: apps/portal/src/features/subscriptions/components/SubscriptionGridCard.tsx

Step 1: Remove description from PageLayout call

// Before
<PageLayout icon={<Server />} title="Subscriptions" description="Manage your active subscriptions">

// After
<PageLayout icon={<Server />} title="Subscriptions">

Do this for both the loading state and the main render.

Step 2: Standardize content card borders

// Before
<div className="bg-card rounded-xl border border-border/60 shadow-[var(--cp-shadow-1)] overflow-hidden">

// After
<div className="bg-card rounded-xl border border-border shadow-[var(--cp-shadow-1)] overflow-hidden">

Step 3: Add opacity dimming for inactive subscriptions in SubscriptionGridCard

In SubscriptionGridCard.tsx, wrap the card Link with conditional opacity:

// Add to the Link className, conditionally:
const isInactive = ["Completed", "Cancelled", "Terminated"].includes(subscription.status);

// In the className of the Link:
className={cn(
  "group flex flex-col p-4 rounded-xl bg-card border border-border transition-all duration-200 hover:shadow-[var(--cp-shadow-2)] hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
  isInactive && "opacity-60"
)}

Also standardize card border from border-border/60 to border-border.

Step 4: Verify

Run: pnpm type-check

Step 5: Commit

style: clean up subscriptions list with unified patterns

Task 6: Update Subscription Detail View

Files:

  • Modify: apps/portal/src/features/subscriptions/views/SubscriptionDetail.tsx

Step 1: Replace breadcrumbs with backLink

// Before
breadcrumbs={[
  { label: "Subscriptions", href: "/account/subscriptions" },
  { label: subscription.productName },
]}

// After
backLink={{ label: "Back to Subscriptions", href: "/account/subscriptions" }}

Step 2: Move cancel button to PageLayout actions

Replace any inline-styled cancel button with:

actions={
  subscription.status === "Active" && isInternetService ? (
    <Button variant="destructive" size="sm" as="a" href={`/account/subscriptions/${subscription.id}/cancel`}>
      Cancel Service
    </Button>
  ) : undefined
}

Remove the old inline cancel button from the body.

Step 3: Use SectionCard for billing section

import { SectionCard } from "@/components/molecules/SectionCard";

// Replace the billing card with:
<SectionCard
  icon={<CreditCardIcon className="h-5 w-5" />}
  title="Billing Information"
  subtitle="Payment and invoices"
  tone="primary"
  actions={
    <Link
      href="/account/billing/invoices"
      className="text-sm font-medium text-primary hover:text-primary/80 transition-colors"
    >
      View Invoices
    </Link>
  }
>
  {/* billing content */}
</SectionCard>;

Step 4: Remove description from PageLayout

Drop description prop from PageLayout usage.

Step 5: Verify

Run: pnpm type-check

Step 6: Commit

style: update subscription detail with backLink and SectionCard

Task 7: Update Orders Views

Files:

  • Modify: apps/portal/src/features/orders/views/OrdersList.tsx
  • Modify: apps/portal/src/features/orders/views/OrderDetail.tsx

Step 1: OrdersList — remove description, standardize card borders

Remove description prop from PageLayout. Change any border-border/60 to border-border. Replace inline status badge classes with StatusPill component.

Step 2: OrderDetail — replace breadcrumbs with backLink

// Before
breadcrumbs={[{ label: "Orders", href: "/account/orders" }, { label: `Order #${id}` }]}

// After
backLink={{ label: "Back to Orders", href: "/account/orders" }}

Remove description prop. Use SectionCard for order item sections.

Step 3: Remove any custom title sizes

If OrderDetail has its own text-2xl title styling, remove it — let PageLayout handle the title.

Step 4: Verify

Run: pnpm type-check

Step 5: Commit

style: update order views with unified patterns

Task 8: Update Billing Views

Files:

  • Modify: apps/portal/src/features/billing/views/InvoicesList.tsx
  • Modify: apps/portal/src/features/billing/views/InvoiceDetail.tsx
  • Modify: apps/portal/src/features/billing/views/PaymentMethods.tsx

Step 1: InvoicesList — remove description

Drop description prop from PageLayout.

Step 2: InvoiceDetail — replace breadcrumbs with backLink

backLink={{ label: "Back to Invoices", href: "/account/billing/invoices" }}

Remove description. Use SectionCard for invoice detail sections where appropriate.

Step 3: PaymentMethods — remove description

Drop description prop.

Step 4: Verify

Run: pnpm type-check

Step 5: Commit

style: update billing views with unified patterns

Task 9: Update Support Views

Files:

  • Modify: apps/portal/src/features/support/views/SupportCasesView.tsx
  • Modify: apps/portal/src/features/support/views/SupportCaseDetailView.tsx
  • Modify: apps/portal/src/features/support/views/NewSupportCaseView.tsx

Step 1: SupportCasesView — remove description, fix inline badges

Remove description and breadcrumbs props from PageLayout. Replace inline badge classes (inline-flex text-xs px-2 py-0.5 rounded font-medium) with <StatusPill> component.

Step 2: SupportCaseDetailView — replace breadcrumbs with backLink

backLink={{ label: "Back to Cases", href: "/account/support" }}

Remove description. Use SectionCard for conversation and meta sections.

Step 3: NewSupportCaseView — replace breadcrumbs with backLink

backLink={{ label: "Back to Cases", href: "/account/support" }}

Remove description.

Step 4: Verify

Run: pnpm type-check

Step 5: Commit

style: update support views with unified patterns

Task 10: Update Remaining Views

Files:

  • Modify: apps/portal/src/features/dashboard/views/DashboardView.tsx
  • Modify: apps/portal/src/features/services/views/AccountServicesOverview.tsx
  • Modify: apps/portal/src/features/account/views/ProfileContainer.tsx
  • Modify: apps/portal/src/features/verification/views/ResidenceCardVerificationSettingsView.tsx
  • Modify: apps/portal/src/features/subscriptions/views/SimReissue.tsx
  • Modify: apps/portal/src/features/subscriptions/views/SimTopUp.tsx
  • Modify: apps/portal/src/features/subscriptions/views/SimChangePlan.tsx
  • Modify: apps/portal/src/features/subscriptions/views/SimCallHistory.tsx
  • Modify: apps/portal/src/features/subscriptions/views/CancelSubscription.tsx
  • Modify: apps/portal/src/features/subscriptions/components/CancellationFlow/CancellationFlow.tsx
  • Modify: apps/portal/src/features/checkout/components/AccountCheckoutContainer.tsx
  • Modify: apps/portal/src/features/services/components/sim/SimConfigureView.tsx
  • Modify: apps/portal/src/features/services/components/internet/configure/InternetConfigureContainer.tsx

Step 1: Remove description from all remaining PageLayout usages

For every file listed above, remove the description prop from <PageLayout>.

Step 2: Replace breadcrumbs with backLink where applicable

  • ResidenceCardVerificationSettingsView.tsx: backLink={{ label: "Back to Settings", href: "/account/settings" }}
  • CancelSubscription.tsx / CancellationFlow.tsx: backLink={{ label: "Back to Subscription", href: "/account/subscriptions/{id}" }}
  • SIM views (Reissue, TopUp, ChangePlan, CallHistory): backLink={{ label: "Back to Subscription", href: "/account/subscriptions/{id}" }}

Step 3: Verify

Run: pnpm type-check Expected: No errors. All breadcrumbs prop usages should be gone.

Step 4: Commit

style: remove descriptions and breadcrumbs from all remaining views

Task 11: Fix Domain Build & Clean Up

Files:

  • Modify: apps/portal/src/features/subscriptions/components/SubscriptionGridCard.tsx (if import still broken)
  • Modify: apps/portal/src/components/templates/PageLayout/PageLayout.tsx (remove dead BreadcrumbItem type if not already)

Step 1: Build domain package

Run: pnpm domain:build Expected: Successful build, resolves @customer-portal/domain/subscriptions import errors.

Step 2: Remove BreadcrumbItem from PageLayout exports

If BreadcrumbItem is still exported, remove it. Check if any file imports it:

Run: grep -r "BreadcrumbItem" apps/portal/src/

Remove any remaining imports.

Step 3: Full type check

Run: pnpm type-check Expected: Clean — no errors.

Step 4: Lint check

Run: pnpm lint Expected: Clean or only pre-existing warnings.

Step 5: Commit

chore: fix domain build and remove dead breadcrumb types

Task 12: Final Visual Verification

Step 1: Start dev server (with user permission)

Run: pnpm --filter @customer-portal/portal dev

Step 2: Verify each page visually

Check these pages match the design:

  • Sidebar: deeper blue, white logo, Subscriptions as direct link
  • Dashboard (/account): muted header bg, no description
  • Subscriptions list (/account/subscriptions): muted header, metrics row, unified content card, inactive subs dimmed
  • Subscription detail (/account/subscriptions/{id}): back link, header with actions, SectionCard for billing
  • Orders list (/account/orders): consistent with subscriptions list pattern
  • Order detail (/account/orders/{id}): back link, SectionCards
  • Invoices (/account/billing/invoices): consistent list pattern
  • Invoice detail: back link, consistent
  • Support cases (/account/support): StatusPill badges, no inline badges
  • Support detail: back link, consistent
  • Settings (/account/settings): muted header

Step 3: Fix any visual issues found

Address spacing, alignment, or color issues discovered during review.

Step 4: Final commit

style: portal UI cleanup — unified page patterns and brand alignment