Assist_Design/docs/plans/2026-03-06-portal-layout-redesign-plan.md
barsa be3388cf58 refactor: enhance AppShell layout and remove Header component
- Replaced the Header component with a mobile-only hamburger menu in the AppShell for improved navigation on smaller screens.
- Integrated user profile information directly into the Sidebar for better accessibility.
- Removed the Settings link from the navigation to streamline the user experience.
- Updated Sidebar and NotificationBell components to accommodate new user profile display logic.
2026-03-06 14:16:43 +09:00

9.8 KiB

Portal Layout Redesign Implementation Plan

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

Goal: Remove the top Header bar, move profile + notifications into the sidebar, and simplify the PageLayout header band.

Architecture: The AppShell currently renders a Header component (notifications, profile, help link) above the main content, and each page uses PageLayout which adds a second header band with title/actions. We remove the Header on desktop (keep mobile hamburger), add a profile row + notification bell to the Sidebar, and drop the border from PageLayout's title area.

Tech Stack: Next.js 15, React 19, Tailwind CSS v4, shadcn/ui, Heroicons


Task 1: Remove border from PageLayout header band

Files:

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

Step 1: Edit PageLayout

In PageLayout.tsx line 37, change:

<div className="bg-muted/40 border-b border-border">

to:

<div className="bg-muted/40">

Step 2: Verify visually

Run: pnpm --filter @customer-portal/portal dev (if dev server is running, just check the browser) Expected: Page title areas no longer have a bottom border, content flows smoothly below.

Step 3: Commit

git add apps/portal/src/components/templates/PageLayout/PageLayout.tsx
git commit -m "style: remove border from PageLayout header band"

Task 2: Add profile row to Sidebar

Files:

  • Modify: apps/portal/src/components/organisms/AppShell/Sidebar.tsx

Step 1: Add SidebarProfile component

Add a new component inside Sidebar.tsx, before the Sidebar export. This renders the compact profile row with avatar initials, display name, and the notification bell.

import { NotificationBell } from "@/features/notifications";

interface SidebarProfileProps {
  user: { firstName?: string | null; lastName?: string | null; email?: string | null } | null;
  profileReady: boolean;
}

function getDisplayName(user: SidebarProfileProps["user"], profileReady: boolean): string {
  const fullName = [user?.firstName, user?.lastName].filter(Boolean).join(" ");
  const emailPrefix = user?.email?.split("@")[0];
  if (profileReady) return fullName || emailPrefix || "Account";
  return emailPrefix || "Account";
}

function getInitials(
  user: SidebarProfileProps["user"],
  profileReady: boolean,
  displayName: string
): string {
  if (profileReady && user?.firstName && user?.lastName) {
    return `${user.firstName[0]}${user.lastName[0]}`.toUpperCase();
  }
  return displayName.slice(0, 2).toUpperCase();
}

function SidebarProfile({ user, profileReady }: SidebarProfileProps) {
  const displayName = getDisplayName(user, profileReady);
  const initials = getInitials(user, profileReady, displayName);

  return (
    <div className="px-3 pb-4">
      <div className="flex items-center gap-2 px-2 py-2 rounded-lg bg-white/10">
        <Link
          href="/account/settings"
          prefetch
          className="flex items-center gap-2 flex-1 min-w-0 group"
        >
          <div className="h-7 w-7 rounded-lg bg-white/20 flex items-center justify-center text-[11px] font-bold text-white flex-shrink-0">
            {initials}
          </div>
          <span className="text-sm font-medium text-white/90 truncate group-hover:text-white transition-colors">
            {displayName}
          </span>
        </Link>
        <NotificationBell className="flex-shrink-0 [&_button]:text-white/70 [&_button]:hover:text-white [&_button]:hover:bg-white/10 [&_button]:p-1.5 [&_button]:rounded-md [&_.absolute]:top-0.5 [&_.absolute]:right-0.5" />
      </div>
    </div>
  );
}

Note: The NotificationBell styling override uses Tailwind's child selector syntax to restyle the button for the dark sidebar context. The dropdown will still render with its normal popover styling since it uses absolute positioning with bg-popover.

Step 2: Update Sidebar props and render the profile row

Add user and profileReady to SidebarProps:

interface SidebarProps {
  navigation: NavigationItem[];
  pathname: string;
  expandedItems: string[];
  toggleExpanded: (name: string) => void;
  isMobile?: boolean;
  user?: { firstName?: string | null; lastName?: string | null; email?: string | null } | null;
  profileReady?: boolean;
}

In the Sidebar component, render SidebarProfile between the logo area and the nav:

export const Sidebar = memo(function Sidebar({
  navigation,
  pathname,
  expandedItems,
  toggleExpanded,
  user,
  profileReady = false,
}: SidebarProps) {
  return (
    <div className="flex flex-col h-0 flex-1 bg-sidebar">
      <div className="flex items-center flex-shrink-0 h-16 px-5 border-b border-sidebar-border">
        {/* ... existing logo ... */}
      </div>

      {/* Profile row */}
      <div className="pt-4">
        <SidebarProfile user={user} profileReady={profileReady} />
      </div>

      <div className="flex-1 flex flex-col pb-4 overflow-y-auto">
        <nav className="flex-1 px-3 space-y-1">{/* ... existing nav items ... */}</nav>
        {/* ... existing logout ... */}
      </div>
    </div>
  );
});

Step 3: Add Link import

Ensure Link from next/link is imported (it already is in the existing file).

Step 4: Verify build

Run: pnpm type-check Expected: No type errors.

Step 5: Commit

git add apps/portal/src/components/organisms/AppShell/Sidebar.tsx
git commit -m "feat: add profile row with notification bell to sidebar"

Task 3: Pass user data to Sidebar from AppShell

Files:

  • Modify: apps/portal/src/components/organisms/AppShell/AppShell.tsx

Step 1: Pass user and profileReady props to both Sidebar instances

In AppShell.tsx, the user object from useAppShellAuth has firstName, lastName, email fields. Pass them to both the mobile and desktop <Sidebar> instances:

For the mobile sidebar (around line 177):

<Sidebar
  navigation={navigation}
  pathname={pathname}
  expandedItems={expandedItems}
  toggleExpanded={toggleExpanded}
  isMobile
  user={user}
  profileReady={!!(user?.firstname || user?.lastname)}
/>

For the desktop sidebar (around line 191):

<Sidebar
  navigation={navigation}
  pathname={pathname}
  expandedItems={expandedItems}
  toggleExpanded={toggleExpanded}
  user={user}
  profileReady={!!(user?.firstname || user?.lastname)}
/>

Note: The auth store uses firstname/lastname (lowercase). The Sidebar's SidebarProfile expects firstName/lastName (camelCase) matching the Header convention. Map in the prop:

user={user ? { firstName: user.firstname, lastName: user.lastname, email: user.email } : null}

Step 2: Verify build

Run: pnpm type-check Expected: No type errors.

Step 3: Commit

git add apps/portal/src/components/organisms/AppShell/AppShell.tsx
git commit -m "feat: pass user data to sidebar for profile row"

Task 4: Remove Header from desktop, keep mobile hamburger

Files:

  • Modify: apps/portal/src/components/organisms/AppShell/AppShell.tsx

Step 1: Replace the Header component with a mobile-only hamburger bar

In AppShell.tsx, replace the <Header> render (around line 203) with:

{
  /* Mobile-only hamburger bar */
}
<div className="md:hidden flex items-center h-12 px-3 border-b border-border/40 bg-background">
  <button
    type="button"
    className="flex items-center justify-center w-10 h-10 -ml-1 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/60 active:bg-muted transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-primary/20"
    onClick={() => setSidebarOpen(true)}
    aria-label="Open navigation"
  >
    <Bars3Icon className="h-5 w-5" />
  </button>
  <div className="ml-2">
    <Logo size={20} />
  </div>
</div>;

Step 2: Add necessary imports

Add imports at the top of AppShell.tsx:

import { Bars3Icon } from "@heroicons/react/24/outline";
import { Logo } from "@/components/atoms/logo";

Step 3: Remove the Header import

Remove this line from AppShell.tsx:

import { Header } from "./Header";

Step 4: Verify build

Run: pnpm type-check Expected: No type errors.

Step 5: Verify visually

Check the portal in a browser:

  • Desktop: no top bar, content starts immediately. Sidebar shows profile + bell.
  • Mobile (resize narrow): thin bar with hamburger + logo. Sidebar overlay has profile + bell.

Step 6: Commit

git add apps/portal/src/components/organisms/AppShell/AppShell.tsx
git commit -m "refactor: remove desktop header, add mobile-only hamburger bar"

Task 5: Clean up Header.tsx

Files:

  • Modify or delete: apps/portal/src/components/organisms/AppShell/Header.tsx

Step 1: Check for other imports of Header

Run: pnpm exec grep -r "from.*Header" apps/portal/src/components/organisms/AppShell/ --include="*.tsx" --include="*.ts"

If only AppShell.tsx imported it (and we removed that import in Task 4), the file is unused.

Step 2: Delete Header.tsx

If no other consumers exist, delete the file.

Step 3: Check for barrel exports

Check if Header is exported from any index file:

  • Check apps/portal/src/components/organisms/AppShell/index.ts (if it exists)
  • Check apps/portal/src/components/organisms/index.ts
  • Remove any Header re-exports.

Step 4: Verify build

Run: pnpm type-check Expected: No type errors.

Step 5: Commit

git add -A apps/portal/src/components/organisms/AppShell/Header.tsx
git commit -m "chore: remove unused Header component"

Task 6: Final type-check and lint

Files: None (verification only)

Step 1: Run type-check

Run: pnpm type-check Expected: Clean pass.

Step 2: Run lint

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

Step 3: Commit any lint fixes if needed

git add .
git commit -m "fix: lint fixes from layout redesign"