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 カラム
補助パネル
}
>
メインコンテンツ
);
}