Core · アプリを開発する · クライアント

ページのレイアウト

業務のページは、既存のコンテナ、見出し、コンテンツ領域を組み合わせて作ります。アプリのシェルは、ブランド、ナビゲーション、グローバルな操作を担います。これらのコンポーネントはアプリが所有するソースで、操作できるコンポーネントには shadcn の慣習を使います。コンポーネントの追加については UI コンポーネントと操作 を参照してください。

製品カタログの例は、既定のテンプレート 1.0.0-beta.85、英語の画面、固定の製品のフィクスチャーを使います。製品の API の呼び出しや書き込みは行いません。検索と長い見出しの切り替えは親ページのメモリにあり、カタログから離れたり再読み込みしたりするとリセットされます。実行するには サンプルのソースとセットアップ を参照してください。

レイアウトを変える場所を選ぶ

変更内容場所
業務のページのフィルター、テーブル、フォームclient/pages/ 以下のページとそのローカルなコンポーネント
ページの余白、セクションの間隔、見出し、操作client/components/page-container.tsx と page-header.tsx
ブランド、サイドバーのフッター、ヘッダーのグローバルな操作client/layouts/components/
サイドバー、ヘッダー、コンテンツの関係client/layouts/app-layout.tsx
インライン、ダイアログ、ドロワー、覆う形の子ビュー親の Outlet と子のレイアウトコンポーネント
サインインと単独のページで共有するシェルclient/routing/standalone-page-layout.tsx

メニューの項目は、既存の選択と権限による絞り込みを保ったまま、ルートの navigation の宣言で追加します。ページ、ルート、ナビゲーション を参照してください。

業務のページを組み立てる

ページのコンテナ、見出し、操作

通常のページは 1 つの PageContainer を使い、見出し、説明、ページ全体の操作には PageHeader を使います。カタログはこの組み合わせを ProductPageFrame に切り出し、ページはフィルターとテーブルの内容を担います。

TSX
import { PageContainer } from '#components/page-container';
import { PageHeader } from '#components/page-header';

// Outer structure of ProductPageFrame; the next section adds its columns.
<PageContainer>
  <PageHeader title={title} description={description} actions={actions} />
  {children}
</PageContainer>

PageContainer は w-full space-y-6 p-6 md:p-8 を提供し、通常の section の属性を受け付けます。PageHeader は title、description、actions に React のノードを受け取り、タイトルを h1 としてレンダリングします。ProductPageFrame を使うページに、追加のページのコンテナは必要ありません。

サインインし、サイドバーから Product catalog を開きます。Long heading をクリックすると、操作の領域と並んだ見出しの折り返しを確認できます。狭い画面では、操作が見出しの下に移ります。

カタログの見出し、操作の領域、2 列のコンテンツ
カタログの見出し、操作の領域、2 列のコンテンツ

ページの組み立てを見る · レイアウトコンポーネントを見る

レスポンシブなフィルターと一覧

データとイベントの処理はページに残したまま、フィルターと一覧をレイアウトのスロットに渡します。

TSX
// Inside ProductPageFrame, after PageHeader.
<div className="grid min-w-0 gap-6 lg:grid-cols-[14rem_minmax(0,1fr)]">
  <aside aria-label={filterLabel} className="flex min-w-0 flex-col gap-4">
    {filters}
  </aside>
  <div className="min-w-0">{children}</div>
</div>

lg のブレークポイント以上では、フィルターの列の幅は 14rem で、一覧が残りの幅を使います。それより狭い場合は、フィルターが一覧の上に縦に並びます。minmax(0,1fr) と min-w-0 によって、幅の広いテーブルがアプリを押し広げることなく、コンテンツの列が縮められます。支援技術がフィルターの領域を識別できるよう、filterLabel は翻訳してください。

Search products に lamp と入力すると照明の製品が、存在しない名前を入力すると一致なしの状態が表示されます。ウィンドウを狭めると、フィルターがテーブルの上に来るはずです。

狭い画面では一覧の上にフィルターがあり、テーブルの中で横スクロールします
狭い画面では一覧の上にフィルターがあり、テーブルの中で横スクロールします

レイアウトコンポーネントを見る。ページ固有のフレームはそのページのディレクトリに置き、複数のページで実際に共有するようになったら client/components/ に移します。

長いコンテンツとスクロール

ページの縦スクロールはアプリのシェルが、横スクロールはテーブルが担います。この例では DataTable を再利用しており、その内部のテーブルのコンテナにはすでに overflow-x-auto があります。

TSX
import { DataTable } from '#components/data-table';

// The catalog page defines columns and the filter state.
<DataTable
  columns={columns}
  data={products.filter((product) =>
    product.name.toLowerCase().includes(query.toLowerCase()),
  )}
  pagination={false}
  emptyMessage={t('layouts.empty')}
/>

この例では、スクロールを観察しやすいよう 48 件のレコードをまとめて表示しています。狭い画面ではテーブルの中で横にスクロールし、アプリ自体に横スクロールバーが出てはいけません。一覧を下にスクロールしても、アプリのヘッダーはその場に残ります。フィルターと一覧に、追加の高さ固定の縦スクロール領域は必要ありません。

一覧のソースを見る。色と間隔の慣習については テーマとスタイル を参照してください。

アプリのシェルをカスタマイズする

ブランド、サイドバー、ヘッダーの操作

AppLayout はサイドバー、ヘッダー、コンテンツの領域を組み合わせます。局所的な変更には、対応するコンポーネントを編集します。この例の AppBrand は、展開時に Northstar Supply を表示し、折りたたみ時はアイコンを残します。HeaderActions は、既存のテーマとアカウントの操作と並べて Catalog guide を追加します。

TSX
import { BookOpen } from 'lucide-react';
import { Link } from 'react-router';
import { Button } from '#components/ui/button';

// Inside HeaderActions, before the existing ThemeSettings and UserMenu.
<Button
  variant="ghost"
  size="icon"
  aria-label={t('layouts.guide')}
  title={t('layouts.guide')}
  render={<Link to="/catalog-guide" />}
>
  <BookOpen />
</Button>

このテンプレートの Base UI のコンポーネントは render で組み合わせます。本のアイコンをクリックするとガイドが開きます。ブランドとヘッダーの項目は、カタログのスクリーンショット で確認できます。

ブランドを見る · ヘッダーの操作を見る

全体の領域を並べ替えるときは app-layout.tsx を変更します。RouteTreeProvider、PageBreadcrumbProvider、権限による絞り込み、useSyncServerLocale()、アカウントメニューのセッションの処理は残してください。これらは戻る操作、パンくずリスト、メニューの表示、ロケールの同期、サインアウトを支えています。

サイドバーの折りたたみとモバイルのナビゲーション

テンプレートのデスクトップとモバイルの動作には、AppSidebarProvider、AppSidebar、AppSidebarToggle を再利用します。

TSX
// AppLayout excerpt; the full source also provides route context and the header.
<AppSidebarProvider>
  <AppSidebar label={navigationLabel} footer={<AppSidebarFooter />}>
    <NavigationMenu
      items={menuItems}
      label={navigationLabel}
      selectedKey={selectedKey}
    />
  </AppSidebar>
  {/* Header and main content column */}
</AppSidebarProvider>

デスクトップでは、ヘッダーの切り替えでナビゲーションがアイコンだけに折りたたまれ、再読み込みしてもその設定が保たれます。モバイルでは、同じ入口でナビゲーションのパネルが開き、その開閉状態はデスクトップの折りたたみとは独立しています。ページで 2 つ目のサイドバーの状態を持たないでください。

モバイルでは、ヘッダーのボタンでアプリのナビゲーションが開きます
モバイルでは、ヘッダーのボタンでアプリのナビゲーションが開きます

アプリのシェルを見る · サイドバーのコンポーネントを見る

子ページを整理する

タブとインラインの子ルート

インラインの子は、親が選んだ位置にレンダリングされます。製品の詳細の Overview と Handling notes は、見出し、戻るリンク、ページのコンテナを共有します。親はタブのナビゲーションのあとに Outlet をレンダリングします。

TSX
import { Outlet } from 'react-router';

// Inside the detail PageContainer, after PageHeader and tab navigation.
<Outlet />

overview.tsx は製品の情報を、handling.tsx は取り扱いの注意事項をレンダリングします。どちらも別の PageContainer や主見出しは作りません。一覧から Details を開くと Overview に入り、続いて Handling notes を選びます。URL と内容の両方が変わります。直接のリンクや再読み込みでも、そのセクションにとどまります。

インラインの内容は、詳細ページの見出し、タブ、コンテナを共有します
インラインの内容は、詳細ページの見出し、タブ、コンテナを共有します

親の詳細ページを見る · インラインのセクションを見る。既定のセクションへのリダイレクトと子ルートの登録については ページ、ルート、ナビゲーション で扱います。

ダイアログ、ドロワー、覆う形の子ページ

これらのコンポーネントが、子ページの表示の仕方を決めます。子は routes.ts で宣言し、親の Outlet はページのコンテナのあとに兄弟として置きます。

TSX
<>
  <ProductPageFrame {...frameProps}>{/* Product list */}</ProductPageFrame>
  <Outlet />
</>

これにより、RouteChildPage は一覧の DOM を残したまま親を覆い、その操作を無効にできます。子の中で別の AppLayout や BrowserRouter をレンダリングしないでください。

Summary は、ダイアログに製品の簡潔な情報を表示します。

TSX
import { RouteDialog } from '#components/route-dialog';

<RouteDialog
  title={t('layouts.summaryTitle')}
  description={t('layouts.readOnly')}
  className="sm:max-w-md"
  footer={<CloseProduct />}
>
  <ProductInfo />
</RouteDialog>
ルートのダイアログに表示される製品の簡潔な情報
ルートのダイアログに表示される製品の簡潔な情報

製品名をクリックすると、長い内容のドロワーが開きます。

TSX
import { RouteDrawer } from '#components/route-drawer';

<RouteDrawer
  title={t('layouts.drawerTitle')}
  description={t('layouts.readOnly')}
  footer={<CloseProduct />}
>
  <ProductInfo extended />
</RouteDrawer>
ドロワーの内容は、固定の見出しとフッターの間でスクロールします
ドロワーの内容は、固定の見出しとフッターの間でスクロールします

ダイアログとドロワーは、独自の余白、スクロールする本体、固定のフッターを提供します。その中に PageContainer をネストしないでください。ボタンは直接 footer に渡します。閉じるコンポーネントは、オーバーレイの中から useRouteOverlay() を呼び出します。

TSX
import { useRouteOverlay } from '#components/use-route-overlay';
import { Button } from '#components/ui/button';
import { useTranslation } from '@nocobase/i18n/client';

function CloseProduct() {
  const { close } = useRouteOverlay();
  const { t } = useTranslation();
  return <Button onClick={() => void close()}>{t('layouts.done')}</Button>;
}

Details は RouteChildPage を使ってアプリのコンテンツ領域を覆い、サイドバーとヘッダーは使えるままにします。こちらはページのコンテナを持ちます。

TSX
import { RouteChildPage } from '#components/route-child-page';

<RouteChildPage>
  <PageContainer>
    <BackButton>{t('layouts.back')}</BackButton>
    <PageHeader title={product?.name ?? t('layouts.missing')} />
    {/* Tab navigation and inline Outlet */}
  </PageContainer>
</RouteChildPage>
覆う形の子ページはコンテンツ領域を占め、ナビゲーションは使えるままです
覆う形の子ページはコンテンツ領域を占め、ナビゲーションは使えるままです

覆う形のページはモーダルではなく、Esc では閉じません。この例では Back to catalog を使います。十分なパンくずリストの戻り先があるページでは、重複した戻るリンクを追加せず、それを使ってもかまいません。

ダイアログを見る · ドロワーを見る · 覆う形のページを見る · ルートの宣言を見る

親の状態とスクロール位置を保つ

既定のシェルは、配置とスクロールを分けています。

TSX
// Main content structure from AppLayout.
<main className="app-brand-surface relative isolate min-w-0 flex-1 overflow-hidden">
  {/* Brand watermark */}
  <div className="h-full overflow-y-auto">
    <Outlet />
  </div>
</main>

main は覆う形のページの配置の境界で、内側の div がページを縦にスクロールします。RouteChildPage も同様に外側の要素を配置し、内側でスクロールするため、その面が親の一覧と一緒に動いたり、一覧の高さまで伸びたりすることはありません。

lamp を検索し、下にスクロールして Summary または Details を開き、閉じるか戻ります。親はマウントされたままで、フィルターとスクロール位置が保たれます。ダイアログとドロワーは、フォーカスもそのトリガーに戻します。完全な再読み込みでは、メモリ上の状態は作り直されます。URL に基づくフィルターと編集については、ページ、ルート、ナビゲーション と データのリクエストと状態管理 を参照してください。

シェルを見る · 親と子の組み立てを見る

サインインと単独のページのレイアウト

テンプレートの AppRouter は、auth でレイアウトを選びます。required のページは、認証とセッションの準備のあとに AppLayout を使います。guest と optional のページは、それぞれの認証ルールで StandalonePageLayout を使います。

Catalog guide は公開の静的な説明で、任意の認証で宣言します。

TSX
{
  name: 'catalogGuide',
  path: '/catalog-guide',
  auth: 'optional',
  authz: 'skip',
  componentLoader: () => import('./guide.js'),
}

その PageContainer は className="mx-auto max-w-3xl pt-24" を使い、コンテンツの幅を制限して上部の操作のための余白を確保します。サインインしたユーザーもゲストもガイドを開けます。Open product catalog は、未認証の訪問者をサインインに送ります。

単独のガイドは、アプリのサイドバーなしでページのコンテナを再利用します
単独のガイドは、アプリのサイドバーなしでページのコンテナを再利用します

auth はアクセスの条件を記述するものです。サイドバーを消すために保護されたページを公開にすると、だれが入れるかが変わってしまいます。別の認証済みのシェルが必要な場合は、認証、セッションの準備、認可を保ったまま、アプリのルートの組み立てをカスタマイズします。任意の layout フィールドで、その組み込みが成り立つわけではありません。

ガイドを見る · レイアウトの選択を見る · 単独のシェルを見る

レイアウトを確認する

動作中のサンプルで、次の操作を試します。

  • デスクトップの幅と 375px で長い見出しを切り替えます。見出し、操作、フィルター、テーブルを確認し、横スクロールがテーブルの中にとどまることを確かめます。
  • ナビゲーションを折りたたんで再読み込みします。モバイルのナビゲーションを開き、その項目が使えることを確かめます。
  • 絞り込んでスクロールした一覧から、それぞれの子の表示方法を開きます。戻って、状態とキーボードのフォーカスが保たれていることを確かめます。
  • 見出しと Done に手が届くまま、ドロワーの長い内容をスクロールします。アプリのナビゲーションが使えるまま、覆う形のページをスクロールします。
  • 子の URL を直接開き、再読み込みし、ブラウザーの戻る・進むを使います。表示方法と戻り先を確認します。
  • 存在しない名前を検索するか、詳細の URL の製品 ID を missing に置き換え、状態のメッセージを確認します。
  • 英語と中国語、ライトモードとダークモードを切り替えます。折り返し、枠線、操作を確認します。サインアウト後にカタログにアクセスすると、サインインに移るはずです。

サンプルのソースとセットアップ

サンプルのアプリの全体 は Node.js 24 と pnpm 11 を使います。製品はフィクスチャーですが、テンプレートのアカウント、セッション、設定にはデータベースが必要です。次の SQLite の設定で検証しています。ほかのデータベースでは、アプリを作成する の説明に従ってドライバーをインストールし、接続を設定します。

Bash
git clone https://github.com/nocobase/docs-examples-v3.git
cd docs-examples-v3
git checkout 929240c07561b2fa02afbe82a3a104a6f0a908b0
cd apps/page-layouts
pnpm install --frozen-lockfile
pnpm nocobase config init --dialect sqlite
pnpm nocobase config check
pnpm dev

起動時に表示される、ベースパスを含むアプリのアドレスを開きます。サインインページからデモ用のアカウントを登録してサインインし、Product catalog を選びます。既定の /main/ の接頭辞では、カタログは /main/layouts、公開のガイドは /main/catalog-guide です。フィクスチャーのカタログはサインインしたすべてのユーザーが使え、管理機能ではテンプレートの実際の権限チェックが残っています。

README には、使い方とコードやスクリーンショットの対応と、コンポーネントとブラウザーのテストのコマンドが記載されています。ブラウザーのテストは、ローカルのサンプルのデータベースに独立したアカウントを作成します。製品の保存、在庫の操作、業務ロールによる認可は、この例の範囲外です。

NocoBase ドキュメントアシスタント

NocoBase について何を知りたいですか?

質問は個別に回答され、過去の質問や回答は参照されません。必要な背景をすべて含めてください。