SCALE — Build Lab
UI部品 · REACT COMPONENT

アプリヘッダー

CATEGORYUI部品 TYPEReact Component EFFORT60〜180分 DIFFICULTY
PRIMARY CODE
tsx · components/layout/Header.tsx
'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Bell, Search, Menu } from 'lucide-react';
import { getSystemByPath } from '@/lib/systems';
import { useAuth } from '@/lib/auth-context';

interface Props {
  onMenuClick?: () => void;
}

export default function Header({ onMenuClick }: Props) {
  const pathname = usePathname();
  const { user } = useAuth();
  const system = getSystemByPath(pathname);

  const currentSection = system?.sections.find(
    s => pathname === s.href || (s.href !== `/${system.id}` && pathname.startsWith(s.href + '/'))
  );

  return (
    <>
      <header className="h-14 bg-bg2 border-b border-border flex items-center justify-between px-4 md:px-6 shrink-0">
        <div className="flex items-center gap-2 md:gap-3 min-w-0">
          <button
            onClick={onMenuClick}
            className="md:hidden w-9 h-9 rounded-lg flex items-center justify-center text-text2 hover:text-text hover:bg-bg3 transition-colors shrink-0"
            aria-label="メニュー"
          >
            <Menu size={18} />
          </button>
          {system && (
            <div className="flex items-center gap-2 min-w-0">
              <span className="text-text2 text-sm truncate hidden sm:inline">{system.name}</span>
              <span className="text-text2 text-sm truncate sm:hidden">{system.shortName}</span>
              {currentSection && (
                <>
                  <span className="text-text3 shrink-0">/</span>
                  <span className="text-text text-sm font-medium truncate">{currentSection.label}</span>
                </>
              )}
            </div>
          )}
          {!system && (
            <span className="text-text text-sm font-medium">SCALE Base</span>
          )}
        </div>

        <div className="flex items-center gap-2 md:gap-3 shrink-0">
          {/* Search — desktop only */}
          <div className="relative hidden lg:block">
            <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text3" />
            <input
              type="text"
              placeholder="検索..."
              className="h-8 w-48 rounded-lg bg-bg3 border border-border pl-8 pr-3 text-xs text-text placeholder:text-text3 outline-none focus:border-accent/50 transition-colors"
            />
          </div>

          <button className="relative w-9 h-9 rounded-lg flex items-center justify-center text-text3 hover:text-text hover:bg-bg3 transition-colors">
            <Bell size={16} />
            <span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-red" />
          </button>

          <div className="flex items-center gap-2 pl-2 border-l border-border">
            <div className="w-7 h-7 rounded-full bg-accent/20 text-accent flex items-center justify-center text-xs font-bold">
              {user?.avatar || '?'}
            </div>
            <div className="hidden lg:block">
              <p className="text-xs text-text leading-tight">{user?.name}</p>
              <p className="text-[10px] text-text3 leading-tight">{user?.departmentName}</p>
            </div>
          </div>
        </div>
      </header>

      {/* Mobile section tabs (horizontal scroll) */}
      {system && system.sections.length > 0 && (
        <nav className="md:hidden bg-bg2 border-b border-border overflow-x-auto scrollbar-hide">
          <div className="flex items-center gap-1 px-2 py-2 min-w-max">
            {system.sections.map((item) => {
              const isActive = pathname === item.href ||
                (item.href !== `/${system.id}` && pathname.startsWith(item.href + '/'));
              const Icon = item.icon;
              return (
                <Link
                  key={item.href}
                  href={item.href}
                  className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs whitespace-nowrap transition-colors ${
                    isActive ? 'bg-bg4 text-text font-medium' : 'text-text2 hover:bg-bg3'
                  }`}
                  style={isActive ? { color: system.color } : undefined}
                >
                  <Icon size={13} />
                  <span>{item.label}</span>
                </Link>
              );
            })}
          </div>
        </nav>
      )}
    </>
  );
}

前提条件
Tailwind CSS v4TypeScript 5
USE CASES
  • 全ダッシュボード共通

アプリヘッダー

:LiTarget: 用途

ロゴ・検索・通知・ユーザーメニューを含むダッシュボード共通ヘッダー。

:LiSparkle: 特徴

  • ロゴ表示
  • グローバル検索
  • 通知バッジ
  • ユーザーメニュー

:LiCode: 実コード(SCALE Base より自動抽出)

:LiInfo: components/layout/Header.tsx の中身そのもの。コピペ即可。

'use client';

import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Bell, Search, Menu } from 'lucide-react';
import { getSystemByPath } from '@/lib/systems';
import { useAuth } from '@/lib/auth-context';

interface Props {
  onMenuClick?: () => void;
}

export default function Header({ onMenuClick }: Props) {
  const pathname = usePathname();
  const { user } = useAuth();
  const system = getSystemByPath(pathname);

  const currentSection = system?.sections.find(
    s => pathname === s.href || (s.href !== `/${system.id}` && pathname.startsWith(s.href + '/'))
  );

  return (
    <>
      <header className="h-14 bg-bg2 border-b border-border flex items-center justify-between px-4 md:px-6 shrink-0">
        <div className="flex items-center gap-2 md:gap-3 min-w-0">
          <button
            onClick={onMenuClick}
            className="md:hidden w-9 h-9 rounded-lg flex items-center justify-center text-text2 hover:text-text hover:bg-bg3 transition-colors shrink-0"
            aria-label="メニュー"
          >
            <Menu size={18} />
          </button>
          {system && (
            <div className="flex items-center gap-2 min-w-0">
              <span className="text-text2 text-sm truncate hidden sm:inline">{system.name}</span>
              <span className="text-text2 text-sm truncate sm:hidden">{system.shortName}</span>
              {currentSection && (
                <>
                  <span className="text-text3 shrink-0">/</span>
                  <span className="text-text text-sm font-medium truncate">{currentSection.label}</span>
                </>
              )}
            </div>
          )}
          {!system && (
            <span className="text-text text-sm font-medium">SCALE Base</span>
          )}
        </div>

        <div className="flex items-center gap-2 md:gap-3 shrink-0">
          {/* Search — desktop only */}
          <div className="relative hidden lg:block">
            <Search size={14} className="absolute left-2.5 top-1/2 -translate-y-1/2 text-text3" />
            <input
              type="text"
              placeholder="検索..."
              className="h-8 w-48 rounded-lg bg-bg3 border border-border pl-8 pr-3 text-xs text-text placeholder:text-text3 outline-none focus:border-accent/50 transition-colors"
            />
          </div>

          <button className="relative w-9 h-9 rounded-lg flex items-center justify-center text-text3 hover:text-text hover:bg-bg3 transition-colors">
            <Bell size={16} />
            <span className="absolute top-1.5 right-1.5 w-2 h-2 rounded-full bg-red" />
          </button>

          <div className="flex items-center gap-2 pl-2 border-l border-border">
            <div className="w-7 h-7 rounded-full bg-accent/20 text-accent flex items-center justify-center text-xs font-bold">
              {user?.avatar || '?'}
            </div>
            <div className="hidden lg:block">
              <p className="text-xs text-text leading-tight">{user?.name}</p>
              <p className="text-[10px] text-text3 leading-tight">{user?.departmentName}</p>
            </div>
          </div>
        </div>
      </header>

      {/* Mobile section tabs (horizontal scroll) */}
      {system && system.sections.length > 0 && (
        <nav className="md:hidden bg-bg2 border-b border-border overflow-x-auto scrollbar-hide">
          <div className="flex items-center gap-1 px-2 py-2 min-w-max">
            {system.sections.map((item) => {
              const isActive = pathname === item.href ||
                (item.href !== `/${system.id}` && pathname.startsWith(item.href + '/'));
              const Icon = item.icon;
              return (
                <Link
                  key={item.href}
                  href={item.href}
                  className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs whitespace-nowrap transition-colors ${
                    isActive ? 'bg-bg4 text-text font-medium' : 'text-text2 hover:bg-bg3'
                  }`}
                  style={isActive ? { color: system.color } : undefined}
                >
                  <Icon size={13} />
                  <span>{item.label}</span>
                </Link>
              );
            })}
          </div>
        </nav>
      )}
    </>
  );
}

:LiFolder: ソースファイルのパス

/Users/oogushiyuuki/Library/CloudStorage/GoogleDrive-y-ogushi@scale-group.co.jp/マイドライブ/AI/scale-base/components/layout/Header.tsx

:LiHandPointer: 使い方

対象プロジェクトに該当ファイルをコピーして、props を流し込むだけ。

:LiAlertCircle: 注意事項

  • 依存パッケージを忘れず追加