レイアウトアーキテクチャガイド
SPA→MPA移行に伴うレイアウト管理の中核設計原則
レイアウトアーキテクチャガイド
1. 概要
このドキュメントは、アプリケーション内のレイアウト管理のための中核となる原則とコンポーネントアーキテクチャを概説します。SPAからMPA(Multi-Page Application)モデルへの移行に伴い、一貫性があり、予測可能で、バグのないレイアウトシステムを確保することを目的としています。
レイアウトはTailwind CSSとそのFlexboxユーティリティに基づいて構築されています。基本的な戦略は、高さとスクロール管理に対するトップダウンの階層的アプローチです。
2. グローバルレイアウトの基盤
アプリケーションのレイアウトは、src/app/layout.tsx内のRootLayoutコンポーネントから始まります。ここがすべてのページの起点です。
RootLayout (src/app/layout.tsx)
└── <html>
└── <body>
└── AppShell (src/components/AppShell.tsx)
└── ...ページコンテンツ...2.1. AppShell.tsx の役割
AppShellは、ヘッダーとボトムナビゲーション(モバイル時)を除いた、アプリケーションの「シェル(骨格)」を定義します。
- 全体構造:
divルート要素はh-screenを持ち、画面全体の高さを確保します。flex flex-colにより、子は垂直方向に配置されます。 - ヘッダー:
<Header />は固定の高さを持つコンポーネントです。 - メインコンテンツ領域:
<main>要素はflex-1を持ち、ヘッダーとフッター(BottomNav)の間の残りのすべてのスペースを埋めます。min-h-0が指定されており、コンテンツが親の高さを超えてもレイアウトが崩れないようになっています。 - フッター:
<BottomNav />は固定の高さを持つコンポーネントです。
2.2. コア原則
レイアウトのバグ(要素のオーバーフローや画面外への押し出しなど)を防ぐため、すべての新規および既存のコンポーネントは以下の2つの原則に従う必要があります。
原則1:階層的な高さ管理(トップダウンの高さ制御)
- すべきこと: 最上位の
AppShellがh-screenで画面の高さを定義します。その中でスペースを埋める子は、親から高さを継承するためにh-fullを使用する必要があります。 - すべきでないこと:
AppShell内にネストされたコンポーネントでh-screenを絶対に使用しないでください。これを行うと、継承チェーンが壊れ、コンポーネントが親の境界を無視してオーバーフローし、バグにつながります。
原則2:スクロールの委譲(子によるスクロール管理)
- すべきこと: 親コンテナはスペースを分配する役割に徹します。スクロールが必要なコンテンツをラップする、最も内側の要素が
overflow-autoを持つべきです。 - すべきでないこと:
AppShellの<main>のような高レベルのコンテナにoverflow-autoを適用することは避けてください。
2.3. 動的コンテンツと幅の管理 (Flexboxの罠)
AI生成テキストや長いURLなど、予測不能な長さのコンテンツを表示する場合、以下の技術的プラクティスを遵守してください。
min-w-0の徹底: Flexアイテムは内容の最小幅を維持しようとするため、スクロール可能なコンテナや三点リーダー(truncate)を配置する親要素には必ずmin-w-0を付与してください。- スクロールコンテナの選択: コンポーネントが多重にネストされている場合、Radix UIの
ScrollAreaよりも、標準のdiv+overflow-autoの方がレイアウト計算が安定し、はみ出しバグを防げることがあります。 - Markdown・テキスト領域:
chat-message-contentなどのユーティリティクラス(overflow-wrap: break-word等を含む)を使用し、コンテンツが物理的な幅を超えないように制限してください。
3. メインUI: 3パネルデスクトップレイアウト
アプリケーションの主要なインターフェースは、src/components/HomePageClient.tsxによって提供される、リサイズ可能な3パネルレイアウトです。このコンポーネントはAppShellの<main>領域全体を占有します。
3.1. ResizablePanelGroupによる領域分割
HomePageClientは、shadcn/uiの<ResizablePanelGroup direction="horizontal">を使用して、画面を水平方向に3つの領域に分割します。- このパネルグループは
flex-1 min-h-0を持ち、親である<main>から高さを適切に継承し、コンテンツがはみ出すのを防ぎます。
3.2. 各パネルの内部構造
各<ResizablePanel>(LeftPanel, CenterPanel, RightPanel)は、flex flex-colコンテナとして機能し、パネル内でコンテンツが垂直方向にレイアウトされる基盤を提供します。
基本構造
ほとんどのパネルは、ヘッダーとスクロール可能なコンテンツ領域を持つ共通のパターンに従います。
<ResizablePanel className="flex flex-col">
{/* Optional Header */}
<div className="flex-shrink-0 border-b p-4">
<h2 className="font-semibold">Panel Title</h2>
</div>
{/* Scrollable Content Area */}
<div className="flex-1 overflow-auto">
{/* ... パネルのコンテンツ ... */}
</div>
</ResizablePanel><ResizablePanel>自身はflex flex-colコンテナです。- 内部のコンテンツ領域となる
divがflex-1で利用可能なすべてのスペースを埋め、overflow-autoでコンテンツのスクロールを管理します。 - この構造は、原則2:スクロールの委譲を完全に遵守しています。
RightPanelのタブ付きレイアウト
RightPanelはより複雑な例で、shadcn/uiのTabsコンポーネントを利用して複数のビューを切り替えます。
<ResizablePanel className="flex flex-col">
<Tabs defaultValue="prompts" className="flex-1 flex flex-col min-h-0">
{/* Tab Triggers in Header */}
<div className="flex-shrink-0 border-b p-4 flex justify-between items-center">
<h2 ...>AI Settings</h2>
<TabsList>
<TabsTrigger value="prompts">Writing Prompts</TabsTrigger>
<TabsTrigger value="settings">AI Settings</TabsTrigger>
</TabsList>
</div>
{/* Tab Content Panels */}
<TabsContent value="prompts" className="flex-1 flex flex-col min-h-0 data-[state=inactive]:hidden">
{/* ... Prompt Management Panel ... */}
</TabsContent>
<TabsContent value="settings" className="flex-1 flex flex-col min-h-0 data-[state=inactive]:hidden">
{/* ... AI Settings Panel ... */}
</TabsContent>
</Tabs>
</ResizablePanel>- この構造でも、ルートの
<ResizablePanel>がflexコンテナの役割を果たします。 - 内部の
<Tabs>コンポーネントがflex-1を引き継ぎ、利用可能なスペース全体を占有します。 - 各
<TabsContent>がさらにflex-1を持つことで、タブを切り替えてもコンテンツ領域の高さが維持され、レイアウトの一貫性が保たれます。
このアーキテクチャにより、各パネルのサイズはユーザーが自由に変更でき、かつ各パネルのコンテンツ(タブを含む)は独立してスクロールできる、柔軟で堅牢なUIが実現されています。
4. モバイルレイアウト
モバイルデバイス(画面幅1280px未満)では、3パネルレイアウトから単一パネル表示に切り替わります。
4.1. レイアウトの切り替え
use-mobileフック:src/hooks/use-mobile.tsxが現在の画面幅を監視し、モバイルサイズかどうかを判定します。- 条件付きレンダリング:
HomePageClient.tsxは、このフックからの戻り値に基づき、デスクトップ用の<ResizablePanelGroup>とモバイル用の<div>を条件付きでレンダリングします。
4.2. 単一パネル表示
- モバイルビューでは、一度に1つのパネル(
LeftPanel,CenterPanel,RightPanelのいずれか)のみが表示されます。 - どのパネルを表示するかは、Jotaiの
mobilePanelAtom('left' | 'center' | 'right') の状態によって管理されます。 HomePageClientやAIPageClient内の<main>要素がflex-1 min-h-0 overflow-y-autoを持ち、表示されている単一パネルのスクロールを担当します。
4.3. BottomNavによるパネルナビゲーション
src/components/BottomNav.tsxは、アプリケーション全体の主要ページ(本棚、執筆、AIなど)間を移動するためのグローバルなナビゲーションバーです。- パネルを持つページ(執筆
/project/...、AI/ai) では、BottomNavの挙動が変化します。 - ユーザーが現在アクティブなページ(例:「執筆」)のアイコンをタップすると、
shadcn/uiのPopoverを利用したドロップダウンメニューが表示されます。 - このメニューから、そのページが持つパネル(例:「プロット」「原稿」)を選択できます。選択に応じて
mobilePanelAtomの状態が更新され、表示されるパネルが切り替わります。 - この設計により、限られたスペースの中で、グローバルなページ移動と、ページ内のパネル移動という2つの異なるナビゲーションを直感的に両立させています。
5. ページ固有のレイアウト
ほとんどのページはグローバルなAppShellコンポーネント内でレンダリングされますが、一部のページは独自のレイアウト構造を持っています。
5.1. 標準ページ (/bookshelf, /explore, /ai)
- これらのページは、
AppShellの<main>コンテナの子としてレンダリングされます。 - ページコンポーネントのルート要素は
h-fullを持つべきであり、原則1に従って親から高さを継承します。 - 例えば
/bookshelfページは、独自のヘッダーとスクロール可能なメイン領域を持つことで、AppShellの枠内で自己完結したレイアウトを構築しています。
5.2. 例外的なレイアウト: 設定ページ (/settings)
- 設定ページ(
/settingsとそのサブページ)は、アプリケーションの他の部分から独立した、完全に独自のレイアウトを持っています。 AppShell.tsxは、現在のパスが/settingsで始まることを検知すると、自身のヘッダーやナビゲーションをレンダリングせず、ページコンポーネントを直接返します。- これにより、
SettingsPage.tsxはh-screenを使用して自身のレイアウトをゼロから構築し、グローバルなレイアウト原則に縛られないUIを実現しています。これは意図的な設計です。