import { useState } from "react"; import { CircleX } from "lucide-react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { Button, Text } from "@godxjp/ui/general"; import { Flex, PageContainer } from "@godxjp/ui/layout"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@godxjp/ui/navigation"; /** * Tabs — Radix tab container with items API and three variants (default / line / card). * Use items for the common full set; compose TabsList/TabsTrigger/TabsContent manually * when you need per-panel control (forceMount, custom props). * Composed only from real @godxjp/ui components. */ const journalItems = [ { value: "pending", label: "未承認", content: ( JE-0042 · 売上計上 株式会社山田商事 ¥480,000 JE-0043 · 仕入計上 有限会社田中工業 ¥120,000 ), }, { value: "posted", label: "承認済", content: ( JE-0040 · 給与支払 2024年5月分 ¥2,800,000 JE-0041 · 経費精算 交通費 ¥38,500 ), }, { value: "voided", label: "取消済", content: ( 取消済の仕訳はありません。 ), disabled: false, }, ]; /** * A 設定 screen shaped like the one gh#502 was filed from: a vertical strip beside a panel whose * content does NOT collapse below about 200px (an action row of two buttons). That min-content is * what starved the strip — measured at 393px before the fold, the strip was 8px wide with 0px of * tab in it. `pnpm check:frame-geometry` sweeps this frame at 320/375/390 and fails on the inline * overflow it produced; a demo whose panel holds one short paragraph proves nothing, because a * paragraph's min-content is one character wide. */ const settingItems = [ { value: "general", label: "基本設定", content: ( 表示名・タイムゾーン・言語。 ), }, { value: "members", label: "メンバー", content: ( 招待済 8 名 · 管理者 2 名。 ), }, { value: "billing", label: "請求", content: ( 次回請求日 2026年10月1日。 ), }, ]; /** * 20 saved views with real Japanese labels — a strip that is wider than 1920px, so it overflows at * EVERY width the browser gates sweep. `scripts/check-tabs-overflow-menu.mjs` measures this frame; * it fails if the strip ever stops overflowing, because a gate over a bar that fits proves nothing. */ const savedViewItems = [ { value: "all", label: "すべての仕訳", content: 全 1,284 件 }, { value: "pending", label: "未承認の仕訳", content: 未承認 2 件 }, { value: "posted", label: "承認済の仕訳", content: 承認済 48 件 }, { value: "voided", label: "取消済の仕訳", content: 取消済 3 件 }, { value: "returned", label: "差戻しの仕訳", content: 差戻し 5 件 }, { value: "draft", label: "下書きの仕訳", content: 下書き 12 件 }, { value: "recurring", label: "定期仕訳のひな形", content: ひな形 7 件 }, { value: "accrual", label: "未払費用の計上", content: 未払費用 9 件 }, { value: "prepaid", label: "前払費用の振替", content: 前払費用 4 件 }, { value: "payroll", label: "給与支払の仕訳", content: 給与 3 件 }, { value: "tax", label: "消費税の集計", content: 消費税 6 件 }, { value: "closing", label: "決算整理の仕訳", content: 決算整理 11 件 }, { value: "audit", label: "監査対象の仕訳", content: 監査対象 8 件 }, { value: "archived", label: "アーカイブ済の仕訳", content: アーカイブ 214 件, }, { value: "fx", label: "外貨建の換算差額", content: 換算差額 2 件 }, { value: "intercompany", label: "関係会社間の取引", content: 関係会社 15 件 }, { value: "fixed-asset", label: "固定資産の減価償却", content: 減価償却 22 件, }, { value: "inventory", label: "棚卸資産の評価替", content: 評価替 6 件 }, { value: "bank", label: "銀行勘定の照合", content: 未照合 4 件 }, { value: "reversal", label: "翌期首の振戻し", content: 振戻し 9 件 }, ]; export default function Demo() { const [activeTab, setActiveTab] = useState("pending"); const [reopened, setReopened] = useState(0); const [editableTabs, setEditableTabs] = useState([ { value: "je-0042", label: "JE-0042", content: 売上計上 ¥480,000 }, { value: "je-0043", label: "JE-0043", content: 仕入計上 ¥120,000 }, { value: "je-0040", label: "JE-0040 (固定)", content: 給与支払 ¥2,800,000, closable: false, }, ]); const [editableTab, setEditableTab] = useState("je-0042"); return ( {/* items API — default variant (pill) */} Default variant · items API Pass an items array; Tabs renders all triggers and content panels automatically. variant="default" is the pill style. {/* Disabled first item — fallback selection must skip it */} 先頭タブが disabled · フォールバック選択 defaultValue/value を渡さない場合、Tabs は先頭の ENABLED タブを自動選択する(disabled の先頭タブは選ばない)。全タブが disabled の場合は何も選択しない。 アーカイブ済の仕訳は表示できません。, disabled: true, }, { value: "pending", label: "未承認", content: 未承認の仕訳が 2 件あります。, }, { value: "posted", label: "承認済", content: 当期承認済: 48 件, }, ]} /> 320px stress · 長いローカライズラベル 狭いコンテナでも長いラベルはクリップされず、水平タブリストが自身でスクロールする 。compact navigation への変換は Tabs API に存在しないため擬似実装しない · スクロールが意図した縮退動作。 方法, }, { value: "two-factor", label: "二要素認証の設定", content: 二要素認証, }, { value: "recovery", label: "アカウントの復旧方法", content: 復旧, }, ]} /> {/* items API — line variant */} Line variant · controlled variant="line" renders an underline indicator. Pair value + onValueChange when the active tab is driven by parent state (e.g. URL param). {/* items API — card variant */} Card variant variant="card" gives each trigger a card-like surface. Good for settings or admin profile pages. 未承認の仕訳が 2 件あります。, }, { value: "posted", label: "承認済", content: 当期承認済: 48 件, }, { value: "voided", label: "取消済", content: 取消済: 3 件, }, ]} /> {/* Manual compound — orientation vertical */} Manual compound · vertical orientation Compose TabsList / TabsTrigger / TabsContent when per-panel control is needed. orientation="vertical" goes on the root Tabs element. 概要 元帳 添付 取引先: 株式会社山田商事 · 売掛金残高 ¥480,000 元帳エントリ: 売掛金 Dr / 売上 Cr ¥480,000 添付ファイル: invoice_0042.pdf {/* Controlled with external Button */} 外部制御 · ボタンでタブを切り替え value + onValueChange で親から active tab を制御できる。 {/* TabsList union — both list variants, hand-composed and named explicitly */} TabsList variant · default / line 手動合成では TabsList に variant を直接指定する。variant="default" はピル型のリスト、variant="line" は下線インジケータのリスト。items API を使う場合は Tabs の variant がそのまま TabsList に渡る。 variant="default" サマリ 明細 当月の売上合計 ¥4,820,000 明細 24 件 · 未承認 2 件 variant="line" サマリ 明細 当月の売上合計 ¥4,820,000 明細 24 件 · 未承認 2 件 {/* Ant Design parity surface — editable-card + extra + centered + placement + size */} Editable card · onEdit / extra / hideAdd variant="editable-card" は Ant Design の editable-card。タブ内の × はポインタ用ショートカット(aria-hidden)で、キーボードでは Delete / Backspace(aria-keyshortcuts)。追加ボタンは tablist の外にある本物の button。extra は antd の tabBarExtraContent を論理軸(start / end)にしたもの。 { if (action === "add") { const next = `tab-${editableTabs.length + 1}`; setEditableTabs([ ...editableTabs, { value: next, label: `新規 ${editableTabs.length + 1}`, content: 新規タブ {next}, }, ]); setEditableTab(next); return; } const rest = editableTabs.filter((item) => item.value !== target); setEditableTabs(rest); if (target === editableTab && rest[0]) setEditableTab(rest[0].value); }} extra={{ end: ( ), }} items={editableTabs} /> {/* antd `more` — overflow="menu" beside the default overflow="scroll" */} overflow · scroll(既定) / menu antd の more をこのライブラリの overflow 語彙に写したもの。既定の scroll は今までどおり、帯が自分の水平オーバーフローをスクロールする。menu はそれに加えて、帯の外に「他のタブ」ボタンを出し、いま見えていないタブだけを並べる · antd と違いタブは帯から取り除かれない(tablist はタブ以外を持てず、display:none のタブはロービングフォーカスを受け取れないため · WAI-ARIA APG)。 overflow="scroll"(既定) overflow="menu" {/* antd onTabClick / removeIcon / Tab.forceRender */} onTabClick · closeIcon · forceRender onTabClick は antd の onTabClick で、ポインタで押されたときだけ発火し DOM の MouseEvent を渡す(キーボードは activationMode="manual" でフォーカス移動と選択が分かれるため発火しない · 選択は onValueChange が担当)。closeIcon は antd の removeIcon で、タブ全体の既定グリフを差し替える(item 側の closeIcon が優先)。item の forceRender は antd の Tab.forceRender で、そのパネルだけを先にマウントし、他のタブに 切り替えても保持する。 同じタブを押し直した回数: {reopened} { if (value === activeTab) setReopened((count) => count + 1); setActiveTab(value); }} items={[ { value: "pending", label: "未承認", content: 未承認の仕訳が 2 件あります。, }, { value: "chart", label: "推移グラフ", // The one panel that must survive a tab switch — antd Tab.forceRender. forceRender: true, content: 月次推移: 4 月 ¥3,120,000 · 5 月 ¥4,820,000, }, { value: "posted", label: "承認済", content: 当期承認済: 48 件, }, ]} /> tabPlacement · size · centered tabPlacement は antd 6.6.2 の名前で、値は論理軸(top / bottom / start / end)。start / end は tablist を縦方向のロービングフォーカスに切り替える。size はライブラリの control band(sm / md / lg)に対応。 {/* CARD on the vertical axis. `card` + `bottom` above had shipped upside down, and `start`/`end` had no card example at all — so the rail, which is drawn with a box-shadow and therefore cannot be logical, had nothing to be measured against in either direction. */} tabPlacement="start" · 狭い画面での折り返し 縦のタブ列はパネルと同じインライン軸を共有するので、パネルの min-content が画面の大半を占めると列が 0px まで潰れ、開いているタブ以外へ行く手段が消える(gh#502)。 --tabs-placement-responsive-breakpoint-width(48rem)以下では start / end を top / bottom へ折り返す。ロービングフォーカスの軸も一緒に切り替わる。 indicator · animated Ant Design の indicator は下線バーの長さと寄せ方を決める。size="full"(既定)はタブ全幅、size="label" はタブ自身の左右パディングを引いた分、つまりラベルの幅。align はバーが短いときだけ効く。animated は inkBar(下線のクロスフェード・既定 ON)と tabPane(パネルのフェードイン・既定 OFF)の二つのスイッチで、prefers-reduced-motion では両方とも止まる。 ); }