import { useState } from "react"; import { Flex, PageContainer, ResponsiveGrid, Separator, SplitPane } from "@godxjp/ui/layout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Badge, StatCard, } from "@godxjp/ui/data-display"; import { Button, Text } from "@godxjp/ui/general"; import { FileText, Building2, Calendar, CreditCard, ArrowRight } from "lucide-react"; /** * SplitPane · main + fixed aside panel. * Shows: main list + detail panel, asideWidth sm/md, nested inside PageContainer. * It OWNS its query container (container-type: inline-size), so the two-column split * is chosen from the PANE's own width (≥48rem), not the viewport — a narrow embedded * pane on a large screen correctly stays single-column. * Composed only from real @godxjp/ui components. * * NOTE: defaults to direction="row"; vertical stacks set direction="col" * explicitly. Spacing comes from the Flex `gap` prop or valid 4-point utilities * (gap-3, pt-2, ml-3) · never named classes like `gap-md` (no such utility exists). */ type Invoice = { id: string; partner: string; date: string; amount: string; status: "承認済" | "保留中" | "未承認"; }; const INVOICES: Invoice[] = [ { id: "INV-0241", partner: "株式会社アクメ", date: "2026-05-31", amount: "¥840,000", status: "承認済", }, { id: "INV-0240", partner: "グローバル商事", date: "2026-05-29", amount: "¥320,000", status: "保留中", }, { id: "INV-0239", partner: "イニテック有限会社", date: "2026-05-28", amount: "¥1,200,000", status: "承認済", }, { id: "INV-0238", partner: "フューチャー工業", date: "2026-05-26", amount: "¥460,000", status: "未承認", }, { id: "INV-0237", partner: "東京メディア", date: "2026-05-24", amount: "¥980,000", status: "承認済", }, ]; const STATUS_TONE = { 承認済: "success", 保留中: "warning", 未承認: "destructive", } as const; const CHANNEL_MESSAGES = [ { author: "田中", at: "09:12", body: "今月の請求書、承認お願いします。" }, { author: "佐藤", at: "09:20", body: "アクメ社の分だけ金額が前月と違うようです。" }, { author: "鈴木", at: "09:31", body: "内訳を確認してスレッドに貼りました。" }, ]; const THREAD_REPLIES = [ { author: "鈴木", body: "追加の保守費用が乗っています。" }, { author: "田中", body: "了解、契約書と突き合わせます。" }, { author: "佐藤", body: "ではこのまま承認で問題なさそうです。" }, ]; const yenFormat = new Intl.NumberFormat("ja-JP", { style: "currency", currency: "JPY", maximumFractionDigits: 0, }); const yen = (n: number) => yenFormat.format(Math.round(n)); const toNumber = (amount: string) => Number(amount.replace(/[^0-9]/g, "")); export default function Demo() { const [selectedId, setSelectedId] = useState("INV-0241"); const [asideWidth, setAsideWidth] = useState<"sm" | "md">("md"); const [threadOpen, setThreadOpen] = useState(true); const selected = INVOICES.find((inv) => inv.id === selectedId) ?? INVOICES[0]; const base = toNumber(selected.amount); return ( } > {/* ── Example 1: Invoice list + detail panel ── */} {selected.id} {selected.status} 請求書詳細 取引先 {selected.partner} 請求日 {selected.date} 金額(税抜) {selected.amount} 明細概要 小計 {selected.amount} 消費税 (10%) {yen(base * 0.1)}
合計 {yen(base * 1.1)}
} > {/* Main: invoice list — gap="sm" so the bordered cards breathe (xs let the borders nearly collide against the airy gap="md" detail aside). */} {INVOICES.map((inv) => { const active = selectedId === inv.id; return ( setSelectedId(inv.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setSelectedId(inv.id); } }} > {inv.id} {inv.status} {inv.partner} {inv.amount} {inv.date} ); })} {/* ── Example 2: Summary stats + aside narration ── */} 利用ガイド asideWidth="sm" {" "} (20rem) · フィルター、統計サマリー、クイック操作などのコンパクトパネル向け。 asideWidth="md" {" "} (22rem) · 詳細フォーム、タイムライン、長いメタデータリスト向け。 パネル自身の幅が 48rem 未満になると縦積みにフォールバックします (ビューポートではなくコンテナ基準)。 常にサイドバイサイドが必要な場合は CSS Grid を使用してください。 } > 月次サマリー {/* ── Example 3: Collapsible rail — aside={null} closes it WITHOUT remounting main ── */} 開閉できるパネル · {"aside={null}"} {"aside={null}"} を渡すと <aside> は描画されず、1 カラム(gap なし)に戻る。ラッパー(scope / pane / main)は残るので children の React ツリー上の位置は変わらず、 再マウントされない(=一覧のスクロール位置と状態が保たれる)。 閉じるときに{" "} SplitPane ごと外す書き方は、ツリーの深さが変わるため 主カラムが再マウントされる。 {threadOpen ? "aside={}" : "aside={null}"} スレッド 返信 3 件 {THREAD_REPLIES.map((reply) => ( {reply.author} {reply.body} ))} ) : null } > #general パネルを開閉しても、この一覧は再マウントされない {CHANNEL_MESSAGES.map((message) => ( {message.author} {message.at} {message.body} ))} Container stress · narrow vs wide embed 大きな viewport 内でも、パネル自身の幅で分割が決まる。狭い埋め込み(約 28rem)は 1 カラムに潰れ、広い埋め込み(約 56rem)は 2 カラムに分割する。ビューポート 幅は同一。 {/* Narrow embed → stays single column (the container is < 48rem). */} 約 28rem のコンテナ → 縦積み 補助パネル } > メインコンテンツ {/* Wide embed → splits into two columns (the container is ≥ 48rem). */} 約 56rem のコンテナ → 2 カラム 補助パネル } > メインコンテンツ ); }