import { useState } from "react"; import { AppShell, Breadcrumb, Flex, PageContainer, ResponsiveGrid, Sidebar, SplitPane, Topbar, } from "@godxjp/ui/layout"; import type { SidebarSectionProp } from "@godxjp/ui/layout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, StatCard, Badge, Avatar, AvatarFallback, } from "@godxjp/ui/data-display"; import { Button, Text } from "@godxjp/ui/general"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@godxjp/ui/navigation"; import { LayoutDashboard, FileText, Receipt, Users, BookOpen, Building2, ShieldCheck, Bell, Plus, ChevronDown, PanelLeftClose, PanelLeftOpen, Search, } from "lucide-react"; /** * AppShell · canonical admin frame. * Real composition: AppShell + Sidebar + Topbar + PageContainer body. * Japanese accounting shell (CoreBooks) with entity switcher, collapse toggle, * notifications badge, and a dashboard page body. * Composed only from real @godxjp/ui components. * Includes an executable switch between the composed topbar and legacy topbarLeft/topbarRight/logo. */ const SECTIONS: SidebarSectionProp[] = [ { label: "会計", items: [ { id: "dashboard", label: "ダッシュボード", icon: LayoutDashboard }, { id: "ledger", label: "元帳", icon: BookOpen, children: [ { id: "journal", label: "仕訳", icon: FileText }, { id: "recurring", label: "定期仕訳", icon: Receipt }, ], }, { id: "invoices", label: "請求書", icon: Receipt }, ], }, { label: "管理", items: [ { id: "partners", label: "取引先", icon: Building2 }, { id: "users", label: "ユーザー", icon: Users }, { id: "roles", label: "権限", icon: ShieldCheck, disabled: true }, ], }, ]; const ENTITIES = [ { id: "acme", name: "株式会社アクメ" }, { id: "globex", name: "グローバル商事株式会社" }, { id: "initech", name: "イニテック有限会社" }, ] as const; export default function Demo() { const [activeId, setActiveId] = useState("dashboard"); // Seeded collapsed at rest so the icon rail + collapsed reflow (data-collapsed) // is visible in a static screenshot; the topbar toggle expands the full sidebar. const [collapsed, setCollapsed] = useState(true); const [activeEntity, setActiveEntity] = useState<(typeof ENTITIES)[number]["name"]>( ENTITIES[0].name, ); const [unread, setUnread] = useState(true); const [legacyTopbar, setLegacyTopbar] = useState(false); // Bar-less shell: topbar / topbarLeft / topbarRight / logo をすべて渡さない状態。 // チャット・メール・IDE のようにページ側が最上段を所有する画面のための構成。 const [barless, setBarless] = useState(false); const sidebar = ( 山田 太郎 システム管理者 } /> ); const topbar = ( {/* Decorative brand mark — hidden below `sm` where the hamburger already anchors the start of a narrow mobile topbar, so functional controls (entity switcher, search) keep priority and the bar never overflows. */} C {/* Entity switcher collapses to an icon-only control below `sm` so the topbar never overflows a narrow (mobile) viewport; the long tenant name truncates from `sm` up. `aria-label` keeps the accessible name at every width even when the label is hidden. */} エンティティ切替 {ENTITIES.map((e) => ( setActiveEntity(e.name)}> {e.name} ))} } center={ // Search collapses to an icon-only trigger below `sm` (fits a narrow topbar) and expands to // the full-width search affordance from `sm` up. `aria-label` preserves the accessible name // when the visible label is hidden. } end={ } /> ); return ( は描画されず、 // .app-root に data-topbar="none" が付いてバーの行が 0 になる(900px 以下だけ、 // ハンバーガーのためにヘッダーが戻る)。 topbar={barless || legacyTopbar ? undefined : topbar} logo={ !barless && legacyTopbar ? ( L ) : undefined } topbarLeft={ !barless && legacyTopbar ? ( ) : undefined } topbarRight={!barless && legacyTopbar ? Legacy slots active : undefined} sidebarCollapsed={collapsed} // Shell-level breadcrumb slot (app-breadcrumb landmark) · distinct from // PageContainer's own breadcrumb; here the shell owns the trail. breadcrumb={ } // Shell-level footer slot (app-footer landmark) · distinct from the // Sidebar footer (the user identity block) shown on the left rail. footer={ 2026 CoreBooks 会計システム バージョン 7.2.0 } > } > Responsive frame coverage Frame の Dimensions で 320 / 375 / 390 / 768 / 1024 / 1280 / 1440 / 1920px を切り替え、sidebar・topbar・page actions・grid・split pane の変換を同じ実用構成で確認する。900px 以下ではドックされたサイドバーが隠れ、 AppShell 所有のモバイルドロワー(トップバー左のハンバーガー → フォーカストラップ付き Sheet · Esc で閉じてトリガーへフォーカス復帰)が navigation を提供する。 このデモは mobileNav を渡していないため、同じ Sidebar がドロワーに再利用される。 「トップバーなし(チャット型)」を押すと topbar / topbarLeft / topbarRight / logo の 4 スロットをすべて外した状態になる。バーに置くものが無い画面 (チャット・メール・IDE = ページ自身が最上段を所有する画面)で空のバーを残すと、 48px の固定行+境界線の上にページヘッダーがもう一段重なるため、AppShell は <header> を描画せず data-topbar="none" でその行を 0 にする。 900px 以下でだけヘッダーはハンバーガー専用として戻る(ドックされたレールが 隠れる幅で navigation を失わせないため)。空の {"topbar={<>}"} で代用しないこと ― それは 4 スロットが埋まった状態と同じで、バーの行は残る。 ブランドの席も幅で動く(gh#728)。.app-topbar-logosm{" "} の段より下で --app-shell-brand-compact-max-inline-size (=バーの高さ)まで切り詰められ、はみ出した分は inline-end 側から落ちる ― どのロックアップもマークが先頭にあるからで、これは package が「自分で描いていない絵」にできる唯一のこと。 何を出すかまで選びたいときは{" "} logoCompact に別ノードを渡す: viewBox は属性なので、どんな stylesheet でも書き換えられない。 実寸は{" "} layout/app-shell-narrow-bar のページで確認できる。 横方向のページ余白軸の持ち主は --space-page-x ひとつ。 トップバーは{" "} .app-main と同じグリッド列に載っている ― つまりバー先頭のコントロールとページタイトルは同じ縦線に並ぶべきもので、 --app-shell-bar-inset はその持ち主を読む ( --app-shell-bar-inset-compact も同様に --space-page-compact-x)。以前は両者が別々の値 (16px と 24px)を名乗り、しかも段が変わる幅まで違った(shell 900px · page 720px)ため、ズレは画面幅で変わった ― 1512px で 8px、720〜900px で 12px、 720px 未満で 4px。幅で変わる誤差はテーマ側で補正しようがない。 いまは狭幅の段も{" "} (max-width: 720px) でページと同時に動く。 Frame の Dimensions を切り替えて、バーとページ本文の左端が どの幅でも一致することを確認できる。 最近の仕訳 承認待ちの仕訳 {["JE-0041", "JE-0040", "JE-0039"].map((id) => ( {id} 保留中 ))} } > 売上推移 過去6ヶ月の月次売上 {[ { month: "12月", amount: "¥7,100,000" }, { month: "1月", amount: "¥6,800,000" }, { month: "2月", amount: "¥7,400,000" }, { month: "3月", amount: "¥8,000,000" }, { month: "4月", amount: "¥7,900,000" }, { month: "5月", amount: "¥8,200,000" }, ].map((row) => ( {row.month} {row.amount} ))} ); } /** A dashboard keeps the canonical topbar and main area without a sidebar track. */ export function WithoutSidebar() { return ( Workspace} />}> Project overview ); }