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 件,
},
]}
/>
}
onEdit={() => undefined}
items={[
{
value: "je-0042",
label: "JE-0042",
content: 売上計上 ¥480,000,
},
{
value: "je-0043",
label: "JE-0043",
content: 仕入計上 ¥120,000,
},
]}
/>
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
では両方とも止まる。
);
}