import { useState } from "react"; import { CreditCard, ShieldCheck, Users } from "lucide-react"; import { Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, DataTable, type ColumnDef, } from "@godxjp/ui/data-display"; import { Button, Text } from "@godxjp/ui/general"; import { Flex, MasterDetail, PageContainer } from "@godxjp/ui/layout"; /** * Pass mobilePane="master" or "detail" for one-pane navigation below the token-owned collapse threshold. * Supply detailBack with a native back link; own selection in URL/router history. * Omit mobilePane to retain the original stacked composition. * MasterDetail · チーム画面 (SCR-110) の正準構成 — 流動的な一覧 + 固定幅の詳細レール。 * * Shows: rail="detail" (既定 · 1fr / 320px) と rail="master" (先頭ナビゲーションレール)、 * railWidth compact/standard、collapseBelow の全段階、そして master 側の選択コントロールを * aria-controls / フォーカス移動で detail region に結び付ける方法。 * * 幾何はすべてトークン所有 (--master-detail-rail-*, --master-detail-gap, * --master-detail-collapse-below)。このページには grid-template-columns も * 画面幅ごとの余白指定も一切ありません — 1440 / 1024 / 390 のいずれでも同じ JSX です。 * * 選択とキーボード操作は呼び出し側の責務 (ここでは DataTable の行クリックと * aria-pressed のトグルボタン)。MasterDetail はレイアウトと 2 つの region だけを所有します。 */ type Team = { id: string; name: string; owner: string; members: number; updatedAt: string; status: "active" | "pending"; }; const TEAMS: Team[] = [ { id: "core", name: "コアプラットフォーム", owner: "佐藤 智", members: 24, updatedAt: "2026-07-28", status: "active", }, { id: "billing", name: "請求基盤", owner: "田中 花子", members: 12, updatedAt: "2026-07-26", status: "active", }, { id: "support", name: "カスタマーサポート", owner: "鈴木 一郎", members: 38, updatedAt: "2026-07-21", status: "pending", }, { id: "security", name: "セキュリティ", owner: "高橋 美咲", members: 6, updatedAt: "2026-07-19", status: "active", }, ]; const SERVICES = [ { id: "members", label: "メンバー管理", description: "招待、状態、所属チーム", icon: Users, count: 128, }, { id: "security", label: "セキュリティ", description: "MFAポリシーと監査ログ", icon: ShieldCheck, count: 6, }, { id: "billing", label: "請求", description: "プラン、請求書、支払い方法", icon: CreditCard, count: 12, }, ]; const RAIL_WIDTHS = ["compact", "standard"] as const; const COLLAPSE_STEPS = ["token", "sm", "md", "lg", "xl", "false"] as const; const MASTER_VIEWPORTS = ["auto", "compact", "standard"] as const; /** * 実サービスの「長い」コレクション。auto のままだと一覧が数千 px に伸び、 * 積み重なった 390px 画面では詳細がフォールドの遥か下に押し出される。 */ const MEMBERS = Array.from({ length: 60 }, (_, i) => ({ id: `member-${i + 1}`, name: `メンバー ${i + 1}`, role: i % 3 === 0 ? "管理者" : i % 3 === 1 ? "編集者" : "閲覧者", })); const numberFormat = new Intl.NumberFormat("ja-JP"); const dateFormat = new Intl.DateTimeFormat("ja-JP", { dateStyle: "medium" }); const formatDate = (iso: string) => dateFormat.format(new Date(`${iso}T00:00:00Z`)); const TEAM_DETAIL_ID = "master-detail-team-detail"; const SERVICE_DETAIL_ID = "master-detail-service-detail"; const MEMBER_DETAIL_ID = "master-detail-member-detail"; export default function Demo() { const [teamId, setTeamId] = useState("core"); const [railWidth, setRailWidth] = useState<(typeof RAIL_WIDTHS)[number]>("standard"); const [collapseStep, setCollapseStep] = useState<(typeof COLLAPSE_STEPS)[number]>("token"); const [serviceId, setServiceId] = useState("members"); const [masterViewport, setMasterViewport] = useState<(typeof MASTER_VIEWPORTS)[number]>("compact"); const [memberId, setMemberId] = useState(MEMBERS[0].id); const team = TEAMS.find((item) => item.id === teamId) ?? TEAMS[0]; const service = SERVICES.find((item) => item.id === serviceId) ?? SERVICES[0]; const ServiceIcon = service.icon; const member = MEMBERS.find((item) => item.id === memberId) ?? MEMBERS[0]; // "token" は prop を渡さない = テーマの --master-detail-collapse-below に委ねる状態。 const collapseBelow = collapseStep === "token" ? undefined : collapseStep === "false" ? false : collapseStep; const columns: ColumnDef[] = [ { key: "name", header: "チーム", sortable: true, render: (row) => ( {row.name} {row.id === teamId ? 選択中 : null} ), }, { key: "owner", header: "オーナー", hiddenOnMobile: true }, { key: "members", header: "人数", align: "right", sortable: true, render: (row) => {numberFormat.format(row.members)}, }, { key: "status", header: "状態", render: (row) => , }, { key: "updatedAt", header: "更新日", align: "right", hiddenOnMobile: true, render: (row) => {formatDate(row.updatedAt)}, }, ]; return ( railWidth {RAIL_WIDTHS.map((value) => ( ))} collapseBelow {COLLAPSE_STEPS.map((value) => ( ))} masterViewport {MASTER_VIEWPORTS.map((value) => ( ))} } > {/* ── 1. 正準構成: rail="detail" (既定) — 流動的な一覧 + 固定幅の詳細レール ── */} row.id} onRowClick={(row) => setTeamId(row.id)} /> } > {team.name} オーナー: {team.owner} メンバー {numberFormat.format(team.members)} 状態 最終更新 {formatDate(team.updatedAt)} {/* ── 2. rail="master" — 先頭ナビゲーションレール + 流動的な詳細面。 master 側のボタンが aria-controls で detail region を指し、 「詳細へ移動」は tabIndex={-1} の region にフォーカスを移す。 ── */} {SERVICES.map((item) => { const Icon = item.icon; const active = item.id === serviceId; return ( ); })} } > {service.description} 現在の設定件数: {numberFormat.format(service.count)} {/* ── 3. masterViewport — 長い実コレクションを境界付きビューポートに収める。 auto: 一覧が 60 行ぶん伸び、390px では詳細がフォールドの遥か下に落ちる。 compact / standard: --master-detail-master-viewport-* が block サイズを 上限とし、コレクションは region の中でスクロールする。 region 自体が tabIndex={0} のスクロールコンテナなので、キーボードだけでも スクロールできる (WCAG 2.1.1) — フォーカスは閉じ込められない。 ── */} {MEMBERS.map((item) => ( ))} } > {member.name} 権限: {member.role} masterViewport="{masterViewport}" · 一覧は{" "} {masterViewport === "auto" ? "コンテンツ全高" : "トークン上限つきスクロール"} ); }