import { useState } from "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 } from "@godxjp/ui/navigation"; /** * CARD TABS — the strip and its panel as ONE object (gh#762). * * The screen this page is built from is the one the issue draws in ASCII: a saved-view strip over * a list, antd `type="editable-card"` shaped — closable views, a `+`, an action parked on the * trailing edge of the bar, and a body that the open tab opens INTO rather than sits above. * * Everything here is `Tabs` with documented props. There is no page-local CSS and no wrapper box * around the panel: the point of `bodied` is that the consumer no longer has to guess which box * to put underneath, because all three guesses were wrong (`` added a second 1px border at * a 9.708px radius over an 8px gap; `` and no box at all left the same * 8px gap). Measured after: 0px of gap, one continuous 1px line, and no line at all across the * active tab. */ type SavedView = { value: string; label: string; count: number; /** antd `Tab.closable` — 「すべての課題」 is the system view and cannot be deleted. */ closable?: boolean; rows: string[]; }; const INITIAL_VIEWS: SavedView[] = [ { value: "all", label: "すべての課題", count: 340, closable: false, rows: [ "TASK-1042 · 請求書の様式を更新", "TASK-1041 · 監査ログの保存期間", "TASK-1038 · 権限表の棚卸", ], }, { value: "mine", label: "自分の担当", count: 12, rows: ["TASK-1042 · 請求書の様式を更新", "TASK-1029 · 締め処理の手順書"], }, { value: "review", label: "レビュー待ち", count: 0, rows: [], }, ]; /** The list body a saved view opens into — a real panel, not a placeholder paragraph. */ function ViewBody({ view }: { view: SavedView }) { if (view.rows.length === 0) { return ( この条件に一致する課題はありません。 ); } return ( {view.rows.map((row) => ( {row} ))} ); } const PLACEMENT_ITEMS = [ { value: "summary", label: "サマリ", content: 当月の売上合計 ¥4,820,000 }, { value: "detail", label: "明細", content: 明細 24 件 · 未承認 2 件 }, { value: "audit", label: "監査", content: 監査ログ 96 件 }, ]; const COUNT_ITEMS = [ { value: "open", label: "未対応", count: 12, countLabel: "件の課題", content: 未対応 12 件, }, { value: "doing", label: "対応中", count: 340, overflowCount: 999, countLabel: "件の課題", content: 対応中 340 件, }, { value: "capped", label: "全期間", count: 1280, countLabel: "件の課題", content: 全期間 1,280 件 · 既定の overflowCount は 99, }, { value: "empty", label: "保留", count: 0, countLabel: "件の課題", content: 保留 0 件, }, { value: "hidden-zero", label: "却下", count: 0, showZero: false, countLabel: "件の課題", content: showZero={"{false}"} なので 0 のピルは描かれない, }, { value: "off", label: "アーカイブ", disabled: true, count: 7, countLabel: "件の課題", content: アーカイブ, }, ]; export default function Demo() { const [views, setViews] = useState(INITIAL_VIEWS); const [activeView, setActiveView] = useState("all"); const [nextView, setNextView] = useState(1); return ( {/* THE SCREEN THE ISSUE DRAWS — saved views over a list. */} 保存したビュー · editable-card + bodied antd の type="editable-card" と同じ形。 bodied がパネルの枠・角丸・背景を描くので、ストリップとパネルが ひとつの箱として読める。「すべての課題」は closable: false なので × が出ない。 フィルタを保存 } onEdit={(target, action) => { if (action === "add") { const value = `view-${nextView}`; setViews((prev) => [ ...prev, { value, label: `新しいビュー ${nextView}`, count: 0, rows: [] }, ]); setNextView((n) => n + 1); setActiveView(value); return; } const key = String(target); setViews((prev) => prev.filter((view) => view.value !== key)); setActiveView((current) => current === key ? (views.find((v) => v.value !== key)?.value ?? "all") : current, ); }} items={views.map((view) => ({ value: view.value, label: view.label, closable: view.closable, count: view.count, overflowCount: 999, countLabel: "件の課題", content: , }))} /> {/* THE DEFECT, SIDE BY SIDE. */} bodied なし / あり bodied なしの card ストリップは 8px 離れた透明なパネルの上に浮く —「card と tab が 1 つに融合していない」と報告された形。あり側は隙間 0px、 線は 1 本、アクティブなタブの下辺だけが開いている。 bodied なし(28.0.0 まで) bodied {/* EVERY PLACEMENT — the joined edge follows the strip. */} bodied × tabPlacement 角丸が落ちる辺・枠が重なる辺は、ストリップの位置に従う。 start / end は論理値なので RTL でも反転する (48rem 以下では top / bottom に折り返す)。 {(["top", "bottom", "start", "end"] as const).map((placement) => ( tabPlacement="{placement}" ))} {/* THE COUNT. */} タブの件数バッジ · count / countLabel count は Button・Toggle と同じカウンタ語彙で、 Intl.NumberFormat でロケール整形される。読み上げ名は 「未対応, 12 件の課題」 — 数字が直接ラベルに連結しないよう、ピルは aria-hidden、 読み上げ用の文だけが sr-only で隣に置かれる。 既定の上限は 99( overflowCount で変更)、 0 を隠すには showZero={"{false}"}。 variant="line" variant="card" + bodied size="sm" / "md" / "lg" {(["sm", "md", "lg"] as const).map((size) => ( ))} {/* The other joined pair the package already ships. */} もうひとつの「ひとつの箱」· Card tabList ストリップがカードの見出しの下に入る形は Card tabList が担当する (antd `Card` の tabList)。枠はカードのもので、 bodied は要らない。card 系のストリップを単体で置くときだけ bodied を使う。 カードの枠がそのままパネルの枠になる。 ); }