import { useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, ScrollArea, StatCard, } from "@godxjp/ui/data-display"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@godxjp/ui/data-entry"; import { Text } from "@godxjp/ui/general"; import { Flex, PageContainer, ResponsiveGrid, SplitPane } from "@godxjp/ui/layout"; /** * ResponsiveGrid · equal-width multi-column tile grid with automatic responsive * collapse (CSS container queries, not viewport). It OWNS its query container * (container-type: inline-size), so it responds to the width available to the grid * with no external container-type declaration. Direct children are typically * StatCard (self-contained bordered card · never wrap in Card/CardContent) or * Card+CardContent for richer tile bodies. columns accepts a number OR breakpoint * object { base?, sm?, md?, lg? } — or use the named `preset` prop (e.g. "pricing-plans") for a * recognised collection shape instead of hand-rolling the breakpoint map. Composed only from * real @godxjp/ui components. */ const KANBAN_LANES = [ { key: "todo", title: "未着手" }, { key: "doing", title: "進行中" }, { key: "done", title: "完了" }, ] as const; type KanbanLane = (typeof KANBAN_LANES)[number]["key"]; const KANBAN_TASKS: { id: string; title: string; lane: KanbanLane; priority: string }[] = [ { id: "T-101", title: "請求書テンプレートの改訂", lane: "todo", priority: "high" }, { id: "T-102", title: "取引先マスタの重複整理", lane: "todo", priority: "medium" }, { id: "T-103", title: "月次締めチェックリスト", lane: "doing", priority: "low" }, ]; export default function Demo() { // Kanban: which lane each card is in, moved by native drag and drop. const [tasks, setTasks] = useState(KANBAN_TASKS); const moveTask = (id: string, lane: KanbanLane) => setTasks((current) => current.map((task) => (task.id === id ? { ...task, lane } : task))); return ( Compact mobile metrics columns={{ base: 2, sm: 4 }} keeps two columns below the first container breakpoint and four above it. コンテナクエリで折り返し(自己所有のクエリコンテナ) 同じ columns={4} でも、コンテナ幅で列数が変わる。左の広い領域は 4 列、右の狭いサイドバー(約 20rem)は 1 列に折り返す。ビューポート幅は同一なので、 折り返しの基準がコンテナ幅であることが分かる。ResponsiveGrid は自身のクエリコンテナ (container-type: inline-size)を持つため、外側に @container を用意しなくても正しく 折り返す。閾値はコンテナ幅 40 / 48 / 64rem。 列間の gap は var(--space-stack-md) が既定で、gap prop で変更できます。 狭いコンテナ(約 20rem)→ sm=1 列 {/* No wrapping `@container` needed — ResponsiveGrid OWNS its query container. */} } > 広いコンテナ → lg=4 列 Container stress · 320 / 768px 相当(外側コンテナ不要) 固定幅の親の中でも、同じ columns={4} が親幅で列数を変える。親には container-type を付けていない。ResponsiveGrid が自身のスコープでクエリするため、 外側にコンテナ宣言がなくても 1 列に潰れない。 {["max-w-80", "max-w-3xl"].map((width) => (
{Array.from({ length: 4 }, (_, index) => (
{index + 1}
))}
))}
columns={4} · KPI 行(StatCard 直置き) StatCard は自身がボーダー付き Card なので Card/CardContent でラップしない。 columns 数値指定時は sm=min(n,2) / md=min(n,3) / lg=n に自動変換。 columns={3} · 法人比較(Card+CardContent) StatCard 以外のリッチなタイルは Card+CardContent で構成。 columns={3} → sm=2 / md=3 / lg=3 に自動変換。 東京本社 JP-001 売上: ¥8,200,000 取引先: 124社 大阪支社 JP-002 売上: ¥3,100,000 取引先: 57社 福岡拠点 JP-003 売上: ¥1,060,000 取引先: 31社 columns={{ sm: 1, md: 2, lg: 4 }} · ブレークポイントオブジェクト(3 段制御) ブレークポイント指定オブジェクト: sm / md / lg キーでコンテナ幅ごとの列数を個別に制御。 未指定キーは下位にフォールバック(lg→md→sm、md→sm、sm 既定 1)。 数値指定(sm=min(n,2) / md=min(n,3) / lg=n の自動変換)では作れない 「sm=1 / md=2 / lg=4」のような任意の組み合わせを表現できる。 preset="pricing-plans" · 課金プラン一覧(3/3/1) 名前付きコレクション geometry: コンテナ幅が lg ステップ(64rem)に達するまでは 1 列、そこから 3 列に切り替わる。ResponsiveGrid には lg より上のステップが無いため、1024px 相当でも 1440px 相当でも同じ 3 列になる · 課金プランカタログの「3/3/1」契約 (dxs-platform/platform#333)。`columns` の代わりに `preset` を渡すだけで、呼び出し側がブレークポイント値を自作する必要がない。 スタンダード ¥9,800 / 月 小規模チーム向けの基本プラン。 プロフェッショナル ¥29,800 / 月 成長企業向けの推奨プラン。 エンタープライズ お問い合わせ 大規模組織向けのカスタムプラン。 flow="columns" align="stretch" · カンバンのレーン align="stretch"(antd Row align)で全レーンが最も高いレーンと同じ高さになり、空の 「完了」レーンも全高のドロップ先になる。省略時は columns フローが start、rows フローが stretch のまま。カードはドラッグでき、中の Select はドラッグ画像をカードの外へ広げない。 {KANBAN_LANES.map((lane) => ( event.preventDefault()} onDrop={(event) => { event.preventDefault(); moveTask(event.dataTransfer.getData("text/plain"), lane.key); }} > {lane.title} {tasks .filter((task) => task.lane === lane.key) .map((task) => ( event.dataTransfer.setData("text/plain", task.id) } > {task.id} {task.title} ))} ))} 一覧と補助情報 2:1 メインの一覧 補助情報
); }