import { useState } from "react"; import { AppShell, Flex, PageContainer, Sidebar, Topbar, TopbarItem } from "@godxjp/ui/layout"; import type { SidebarSectionProp } from "@godxjp/ui/layout"; import { Avatar, AvatarFallback, Badge, Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@godxjp/ui/data-display"; import { Button, Text } from "@godxjp/ui/general"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@godxjp/ui/navigation"; import { Bell, Building2, ChevronDown, FileText, LayoutDashboard, LogOut, PanelLeftClose, PanelLeftOpen, Search, Settings, UserRound, Users, } from "lucide-react"; /** * Topbar · a PURE SLOT bar. The shell only positions `start` / `center` / `end`; the CONSUMER * composes every control. */ const SECTIONS: SidebarSectionProp[] = [ { label: "会計", items: [ { id: "dashboard", label: "ダッシュボード", icon: LayoutDashboard }, { id: "journal", label: "仕訳", icon: FileText }, ], }, { label: "管理", items: [{ id: "users", label: "ユーザー", icon: Users }] }, ]; const ENTITIES = [ { id: "acme", name: "株式会社アクメ" }, { id: "globex", name: "グローバル商事株式会社" }, { id: "initech", name: "イニテック有限会社" }, ] as const; const TOPBAR_COPY = { ja: { screenTitle: "組織セキュリティと認証ポリシーの管理", search: "ユーザー、組織、監査イベントを検索…", }, en: { screenTitle: "Organization security and authentication policy administration", search: "Search users, organizations, and audit events…", }, vi: { screenTitle: "Quản trị chính sách bảo mật và xác thực của tổ chức", search: "Tìm người dùng, tổ chức và sự kiện kiểm toán…", }, } as const; type DemoLocale = keyof typeof TOPBAR_COPY; export default function Demo() { const [activeId, setActiveId] = useState("dashboard"); const [collapsed, setCollapsed] = useState(false); const [activeEntity, setActiveEntity] = useState<(typeof ENTITIES)[number]>(ENTITIES[0]); const [unread, setUnread] = useState(true); const [searchOpenCount, setSearchOpenCount] = useState(0); const [locale, setLocale] = useState("ja"); const topbarCopy = TOPBAR_COPY[locale]; // start cluster · sidebar toggle + brand mark (Avatar) + an entity switcher the consumer owns. const start = ( <> {/* Decorative mark — hidden below sm so the budget goes to the two real controls. That was only ever a COMMENT: nothing here hid anything, and at 320px the 32px mark was part of why the switcher beside it was crushed to 0 visible px (gh#639). `hideBelow` is the contract that makes the sentence true — a slot cannot shrink a control, so a bar that does not fit has to drop one. */} C {/* Button ships `shrink-0`, and only the LAST child of the start slot gets the built-in truncation contract — so a switcher sitting mid-slot has to be told it may shrink, or it keeps its full width and the slot clips it while it stays focusable. `flex-1 min-w-11` below sm lets it take just the leftover room without ever dropping under the 44px touch floor — plain `min-w-0` let flex hand it 20px, which is under the 24px SC 2.5.8 minimum, trading one failure for another. `truncate` puts the ellipsis on the label rather than letting it spill. */} エンティティ切替 {ENTITIES.map((e) => ( setActiveEntity(e)}> {e.name} {activeEntity.id === e.id ? ( 現在 ) : null} ))} {topbarCopy.screenTitle} ); // center · a search trigger (opens YOUR command palette). No baked search box. const center = ( ); // end · notifications + user menu, both consumer-composed. const end = ( <> {/* The environment marker is the end cluster's most optional item — 92px of a 320px bar, and the single biggest reason the start cluster had nothing left. It goes first. */} ステージング setUnread(false)} > 佐藤 花子 プロフィール アカウント設定 ログアウト ); const sidebar = ( undefined} /> ); return ( } sidebarCollapsed={collapsed} > 現在の状態 各コントロールは props ではなく slot に置いた実コンポーネント。操作すると更新されます。 {(["ja", "en", "vi"] as const).map((language) => ( ))} アクティブエンティティ {activeEntity.name} 通知バッジ {unread ? "未読あり" : "なし"} 検索を開いた回数 {searchOpenCount} Topbar は3つの slot だけ start / center / end。中身(ブランド・ナビ・検索・言語切替・ユーザーメニュー)は すべて consumer が決める。アイコンのみ/ラベル付き/枠線あり等は各コンポーネントの props であって、シェルが強制するものではない。 {[ { prop: "start", desc: "サイドバートグル + ブランドマーク(Avatar)+ エンティティ切替(DropdownMenu)", }, { prop: "center", desc: "検索トリガー(Button)。コマンドパレットを開く" }, { prop: "end", desc: "通知ボタン + ユーザーメニュー(DropdownMenu)" }, { prop: "overflow", desc: '"scroll"(既定)| "clip" · 収まらないときバーが横スクロールするか、切り落とすか', }, { prop: "children", desc: "3 slot を使わず完全カスタムにする場合の逃げ道" }, ].map(({ prop, desc }) => ( {prop} {desc} ))} 状態リセット 320px stress · 長いローカライズ内容 1100px 以下では package-owned contract が center slot を隠し、start の最後の ラベルを ellipsis で切り詰めて end utilities を保護する。 それでも足りなくなったら バーは切り落とさず横スクロールするoverflow="scroll" が既定 · gh#728): start はセル 1 つ分の下限を、end はセルの実寸を保つので、収まらない分は「半分だけ描かれる」のではなく 視界の外に出るだけになる。セルは DOM に残るため Tab で到達でき、フォーカスが当たるとブラウザが可視域へスクロールする。 overflow="clip" で以前の切り落としに戻せる。 株式会社とても長い組織名称} center={} end={} /> ); }