@godxjp/ui 32.5.0 → 32.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +210 -0
- package/README.md +3 -2
- package/agent/DESIGN.md +210 -0
- package/agent/START-HERE.md +25 -29
- package/agent/anti-ai-tells.json +56 -2
- package/agent/components/Anchor.json +1 -1
- package/agent/components/ChatSuggestion.json +1 -1
- package/agent/components/LegalDocumentShell.json +7 -1
- package/agent/components/PageContainer.json +1 -2
- package/agent/components/Progress.json +1 -1
- package/agent/components-index.json +1 -1
- package/agent/components.json +11 -6
- package/agent/index.json +11 -5
- package/agent/llms.txt +7 -6
- package/agent/patterns/badge-coloring.json +2 -2
- package/agent/patterns/common-fixes.json +1 -1
- package/agent/patterns/confirm-destructive.json +2 -2
- package/agent/patterns/inertia-detail-page.json +2 -2
- package/agent/patterns/inertia-list-page.json +2 -2
- package/agent/patterns/page-sections.json +5 -3
- package/agent/patterns/settings-section-rows.json +2 -2
- package/agent/patterns-index.json +9 -7
- package/agent/patterns.json +16 -14
- package/agent/tokens.json +12 -0
- package/dist/components/data-entry/select.js +5 -4
- package/dist/components/layout/page-container.js +14 -8
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/lab/legal-document-shell.js +7 -3
- package/dist/props/components/layout.prop.d.ts +13 -1
- package/dist/props/registry.d.ts +4 -0
- package/dist/props/registry.js +5 -0
- package/dist/styles/layers.json +1 -1
- package/dist/styles/layout.css +26 -0
- package/dist/styles.css +1 -1
- package/dist/tokens/components/legal-document.css +4 -0
- package/docs/CONSUMER-RULES.md +6 -5
- package/docs/README.md +1 -1
- package/docs/migrations/v32.md +10 -5
- package/package.json +3 -2
- package/scripts/_agent-setup.mjs +149 -85
- package/scripts/cli.mjs +8 -0
- package/scripts/consumer-rule.md +41 -77
- package/scripts/ui-audit.mjs +189 -29
package/agent/patterns.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[
|
|
2
2
|
{
|
|
3
|
-
"code": "// ───────────────────────────────────────────────────────────────────────\n// 0) ★ MOST COMMON: <Card> body has NO padding (content is flush against the edges)\n// Cause: the bare <Card> has ZERO inner padding — it MUST contain <CardContent>.\n// Don't hand-roll padding with className=\"p-4\" on the Card either.\n// ❌ <Card><Flex direction=\"col\" gap=\"md\">…fields…</Flex></Card> // flush, no padding\n// ❌ <Card className=\"p-4\">…fields…</Card> // hand-rolled padding\n// ✅ <Card><CardContent><Flex direction=\"col\" gap=\"md\">…fields…</Flex></CardContent></Card>\n// Titles → <CardHeader><CardTitle>. Only go flush deliberately for a full-bleed table:\n// ✅ <Card><CardContent flush><DataTable/></CardContent></Card>\n// GENERAL RULE — compose godx-ui primitives FULLY; never hand-roll what one ships:\n// padding → CardContent (not p-4) · controls → Input/Select/Button (not raw <input>/<select>/<button>)\n// vertical spacing → Flex direction=\"col\" gap (not space-y-*) · empty rows → DataTable's built-in\n// empty / <EmptyState> (not a custom data.length===0 guard). If a primitive exists, USE it.\n\n// 1) StatCard shows a DOUBLE border (too thick)\n// Cause: StatCard IS already a bordered Card. Don't wrap it.\n// ❌ <Card><CardContent><StatCard label=\"x\" value=\"1\" /></CardContent></Card>\n// ✅ <ResponsiveGrid columns={4}><StatCard label=\"
|
|
3
|
+
"code": "// ───────────────────────────────────────────────────────────────────────\n// 0) ★ MOST COMMON: <Card> body has NO padding (content is flush against the edges)\n// Cause: the bare <Card> has ZERO inner padding — it MUST contain <CardContent>.\n// Don't hand-roll padding with className=\"p-4\" on the Card either.\n// ❌ <Card><Flex direction=\"col\" gap=\"md\">…fields…</Flex></Card> // flush, no padding\n// ❌ <Card className=\"p-4\">…fields…</Card> // hand-rolled padding\n// ✅ <Card><CardContent><Flex direction=\"col\" gap=\"md\">…fields…</Flex></CardContent></Card>\n// Titles → <CardHeader><CardTitle>. Only go flush deliberately for a full-bleed table:\n// ✅ <Card><CardContent flush><DataTable/></CardContent></Card>\n// GENERAL RULE — compose godx-ui primitives FULLY; never hand-roll what one ships:\n// padding → CardContent (not p-4) · controls → Input/Select/Button (not raw <input>/<select>/<button>)\n// vertical spacing → Flex direction=\"col\" gap (not space-y-*) · empty rows → DataTable's built-in\n// empty / <EmptyState> (not a custom data.length===0 guard). If a primitive exists, USE it.\n\n// 1) StatCard shows a DOUBLE border (too thick)\n// Cause: StatCard IS already a bordered Card. Don't wrap it.\n// ❌ <Card><CardContent><StatCard label=\"x\" value=\"1\" /></CardContent></Card>\n// ✅ dashboard only, with comparison/trend:\n// <ResponsiveGrid columns={4}><StatCard label=\"売上\" value=\"¥8.2M\" delta=\"+12%\" /></ResponsiveGrid>\n// A list or detail page has NO StatCard row — a row count is not a KPI (Pagination showTotal).\n\n// 2) Badge renders grey with a ○ (no colour) for localized/tier labels\n// Cause: it auto-maps only English lifecycle keys.\n// ✅ <Badge status=\"active\">公開中</Badge> // lifecycle → keep icon\n// ✅ <Badge status=\"プレミアム\" tone=\"neutral\" icon={null} /> // tier → ONE neutral tone\n// Colour is for status only; a tier/category/scope is plain text or a neutral pill.\n\n// 3) Table text collapses to one char per line, or a chip wraps\n// ✅ give long columns a width: { key: \"name\", header: \"氏名\", width: \"w-64\" }\n// (cells + chips are pinned white-space: nowrap by the library.)\n\n// 4) Empty (icon/action) column header shows a blank grey block\n// ✅ header: \"\" renders a transparent cell — leave it empty, don't inject a space.\n\n// 5) DataTable columns are crushed / squeezed\n// Cause: the table is nested in a narrow grid column.\n// ❌ <ResponsiveGrid columns={3}><div className=\"lg:col-span-2\"><Card><DataTable/></Card></div></ResponsiveGrid>\n// ✅ Table gets its OWN full-width row: <Card><CardContent flush><DataTable/></CardContent></Card>\n\n// 6) Toolbar (filter bar) has no padding (sticks to the edge)\n// Cause: it's inside CardContent flush (flush strips padding — that's for tables).\n// ❌ <Card><CardContent flush><Toolbar/><DataTable/></CardContent></Card>\n// ✅ <Toolbar/> then <Card><CardContent flush><DataTable/></CardContent></Card>\n\n// 7) Sidebar footer looks washed-out / off-design\n// Cause: raw opacity-*/text-[11px]. Use semantic tokens.\n// ✅ <div className=\"text-muted-foreground text-xs\">\n// <div className=\"text-foreground font-medium\">{name}</div><div>{role}</div></div>\n\n// 8) Inertia: \"Objects are not valid as a React child {errors, auth, …}\"\n// Cause: persistent layout passed as a render fn. Use the ARRAY form.\n// ❌ Page.layout = (page) => <Layout>{page}</Layout>\n// ✅ Page.layout = [Layout] // Layout is ({children}) => ...\n\n// 9) Inertia v3 hydration mismatch (\"server rendered text didn't match the client\")\n// Cause: Math.random()/argless new Date() during render (SSR ≠ client).\n// ✅ seed deterministically by index, or compute in an event handler.\n\n// 10) Hide a column on mobile / sign-aware KPI delta\n// ✅ columns: [{ key: \"email\", header: \"メール\", hiddenOnMobile: true }]\n// ✅ <StatCard label=\"売上\" value=\"¥8.2M\" delta=\"+12%\" /> // + green / - red; inverse flips",
|
|
4
4
|
"name": "common-fixes",
|
|
5
5
|
"tagline": "Fix the most common @godxjp/ui consumer mistakes & visual bugs (StatCard double-border, grey Badge, crushed/empty table headers, washed-out sidebar footer, Inertia layout crash, SSR hydration). Before → after.",
|
|
6
6
|
"tags": [
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
"preferences-rows",
|
|
56
56
|
"account-identity"
|
|
57
57
|
],
|
|
58
|
-
"code": "// Gate 0 (docs/COMPOSITION-VS-COMPONENT.md): a \"SettingsSection\"/\"SettingsRow\"/\
|
|
58
|
+
"code": "// Gate 0 (docs/COMPOSITION-VS-COMPONENT.md): a \"SettingsSection\"/\"SettingsRow\"/\"DangerZone\"\n// FAILS the Framework-Component Test, so compose it. Reference screen: get_exemplar(\"settings\")\n// (docs/showcase/settings-account-sections.tsx). Composition rules: DESIGN.md → Admin Screen Anatomy.\nimport { ExternalLink } from \"lucide-react\";\nimport { Button, Heading } from \"@godxjp/ui/general\";\nimport { Card, CardContent, Descriptions, ListRow, StatusBadge } from \"@godxjp/ui/data-display\";\nimport { Switch } from \"@godxjp/ui/data-entry\";\nimport { AlertDialog } from \"@godxjp/ui/feedback\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\n// Sections are a heading + spacing: the SECTION step (gap=\"xl\") is larger than the ITEM step\n// (gap=\"sm\"), which is what carries the hierarchy — not a box around every block.\n<Flex direction=\"col\" gap=\"xl\">\n\n {/* 1) IDENTITY — a section, not a Card. Metadata is Descriptions (a real <dl>). */}\n <Flex direction=\"col\" gap=\"sm\">\n <Flex justify=\"between\" align=\"center\">\n <Heading level={2}>アカウント情報</Heading>\n <Button variant=\"outline\" size=\"sm\">編集</Button>\n </Flex>\n <Descriptions columns={2}>\n <Descriptions.Item label=\"表示名\">藤原 慧</Descriptions.Item>\n <Descriptions.Item label=\"メールアドレス\">satoshi@example.co.jp</Descriptions.Item>\n <Descriptions.Item label=\"従業員番号\" mono>EMP-1042-JP</Descriptions.Item>\n <Descriptions.Item label=\"利用開始日\">\n <time dateTime=\"2023-04-01\">\n {new Intl.DateTimeFormat(locale, { dateStyle: \"long\", timeZone: \"UTC\" }).format(since)}\n </time>\n </Descriptions.Item>\n </Descriptions>\n </Flex>\n\n {/* 2) PREFERENCE ROWS — a list of setting rows IS a real boundary, so it gets one Card:\n CardContent flush + ListRow, whose trailing slot is the control. The visible title is the\n control's <label htmlFor>. NEVER a flex div with border-b rows. */}\n <Flex direction=\"col\" gap=\"sm\">\n <Heading level={2}>通知の環境設定</Heading>\n <Card>\n <CardContent flush>\n <ListRow\n align=\"start\"\n overflow=\"wrap\"\n title={<label htmlFor=\"pref-digest\">週次サマリーをメールで受け取る</label>}\n description=\"毎週月曜日の朝に未処理の承認をまとめて送信します。\"\n trailing={<Switch id=\"pref-digest\" checked={digest} onCheckedChange={setDigest} />}\n />\n </CardContent>\n </Card>\n </Flex>\n\n {/* 3) BILLING HANDOFF — a section. The state is a canonical StatusBadge (never a page-local\n status→colour map); money through Intl. */}\n <Flex direction=\"col\" gap=\"sm\">\n <Flex justify=\"between\" align=\"center\">\n <Heading level={2}>請求とサブスクリプション</Heading>\n <StatusBadge status=\"past_due\" variant=\"outline\" />\n </Flex>\n <Descriptions columns={2}>\n <Descriptions.Item label=\"契約プラン\">ビジネス(年額)</Descriptions.Item>\n <Descriptions.Item label=\"次回請求予定額\">\n {new Intl.NumberFormat(locale, { style: \"currency\", currency: \"JPY\" }).format(1536000)}\n </Descriptions.Item>\n </Descriptions>\n <div>\n <Button variant=\"outline\" asChild>\n <a href={portalUrl} target=\"_blank\" rel=\"noreferrer\">\n <ExternalLink aria-hidden=\"true\" />\n 請求ポータルを開く\n <span className=\"sr-only\">(新しいタブで開きます)</span>\n </a>\n </Button>\n </div>\n </Flex>\n\n {/* 4) DANGER ZONE — a real boundary: Card accent=\"destructive\" (the rail comes from\n --destructive). The heading and each row say what is lost; no decorative icon. */}\n <Flex direction=\"col\" gap=\"sm\">\n <Heading level={2}>取り消せない操作</Heading>\n <Card accent=\"destructive\">\n <CardContent flush>\n <ListRow\n align=\"start\"\n overflow=\"wrap\"\n title=\"ワークスペースと全データを完全に削除する\"\n description=\"勤怠記録・申請履歴・監査ログがすべて削除されます。\"\n trailing={<Button variant=\"destructive\" size=\"sm\" onClick={() => setConfirmOpen(true)}>削除</Button>}\n />\n </CardContent>\n </Card>\n </Flex>\n</Flex>\n\n<AlertDialog\n open={confirmOpen}\n onOpenChange={setConfirmOpen}\n title=\"ワークスペースを完全に削除しますか?\"\n description=\"この操作は取り消せません。確認のため識別子を入力してください。\"\n challenge={workspaceSlug}\n variant=\"destructive\"\n onConfirm={destroy}\n/>\n\n// ── DO / DON'T ────────────────────────────────────────────────────────────────────────────\n// ✅ Section step > item step (gap=\"xl\" between sections, gap=\"sm\" inside one).\n// ✅ A Card only where there is a real boundary: a list of setting rows, the danger zone.\n// ✅ Every setting row is a ListRow; its control lives in `trailing`, titled by <label htmlFor>.\n// ⛔ Do NOT add SettingsSection / SettingsRow / DangerZone to src/components — they fail Gate 0.\n// ⛔ Do NOT give every block Card + CardHeader + CardTitle + CardDescription (ui-audit card-per-section).\n// ⛔ Do NOT hand-roll padding (p-4), a status colour map, or a <dl> replacement.",
|
|
59
59
|
"name": "settings-section-rows",
|
|
60
|
-
"tagline": "The four canonical settings
|
|
60
|
+
"tagline": "The four canonical settings blocks — identity · preference rows · billing handoff · danger zone. Identity and billing are SECTIONS (Heading + Descriptions, no surface); preference rows and the danger zone are Cards because they are real boundaries. A COMPOSITION pattern: godx-ui ships no SettingsSection/SettingsRow/DangerZone component and never will.",
|
|
61
61
|
"tags": [
|
|
62
62
|
"settings",
|
|
63
63
|
"account",
|
|
@@ -87,9 +87,9 @@
|
|
|
87
87
|
]
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
|
-
"code": "import {
|
|
90
|
+
"code": "import { AlertDialog, toast } from \"@godxjp/ui/feedback\";\n\n// The preset covers a typed challenge: confirm stays disabled until the input equals `slug`.\nexport function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {\n return (\n <AlertDialog\n open={open}\n onOpenChange={onOpenChange}\n title=\"プロジェクトを削除\"\n description={`この操作は取り消せません。確認のためプロジェクト名 \"${slug}\" と入力してください。`}\n challenge={slug}\n confirmLabel=\"完全に削除\"\n onConfirm={() => {\n toast.success(\"削除しました\");\n onOpenChange(false);\n }}\n />\n );\n}",
|
|
91
91
|
"name": "confirm-destructive",
|
|
92
|
-
"tagline": "Type-to-confirm destructive dialog —
|
|
92
|
+
"tagline": "Type-to-confirm destructive dialog — the AlertDialog PRESET with `challenge` (the compound AlertDialog* parts are the older way in; a body the preset does not cover is Dialog variant=\"destructive\").",
|
|
93
93
|
"tags": [
|
|
94
94
|
"dialog",
|
|
95
95
|
"confirm",
|
|
@@ -151,9 +151,9 @@
|
|
|
151
151
|
]
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
|
-
"code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer,
|
|
154
|
+
"code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, DataTable, EmptyState, Badge, type ColumnDef } from \"@godxjp/ui/data-display\"\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\"\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\" // see \"inertia-persistent-layout\"\n\ntype Coupon = { id: string; name: string; status: string; scope: string; validFrom: string; validTo: string; usage: number }\nconst PAGE_SIZE = 10\n\nfunction Coupons({ coupons }: { coupons: Coupon[] }) {\n const [q, setQ] = useState(\"\")\n const [status, setStatus] = useState(\"all\")\n const [page, setPage] = useState(1)\n\n const filtered = useMemo(() => coupons.filter((c) => {\n if (q && !c.name.toLowerCase().includes(q.toLowerCase())) return false\n if (status !== \"all\" && c.status !== status) return false\n return true\n }), [coupons, q, status])\n const paged = filtered.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)\n\n // ColumnDef = { key, header, render?, align?: \"left\"|\"center\"|\"right\", sortable?, width? }\n const columns: ColumnDef<Coupon>[] = [\n { key: \"name\", header: \"クーポン名\", render: (c) => <span className=\"font-medium\">{c.name}</span> },\n { key: \"scope\", header: \"スコープ\" }, // a category is plain text; Badge is for the status column only\n { key: \"status\", header: \"ステータス\", render: (c) => <Badge status={c.status} /> },\n { key: \"valid\", header: \"有効期間\", render: (c) => `${formatDate(c.validFrom)} 〜 ${formatDate(c.validTo)}` },\n { key: \"usage\", header: \"利用数\", align: \"right\", render: (c) => c.usage.toLocaleString() },\n ]\n\n return (\n <>\n <Head title=\"クーポン管理\" />\n {/* RULE: every page wraps in PageContainer; vertical rhythm via Flex direction=\"col\"/ResponsiveGrid, never p-* or gap-* */}\n {/* No subtitle that restates the title, no StatCard row: the total is Pagination's showTotal. */}\n <PageContainer title=\"クーポン管理\">\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar is the filter-bar primitive (standalone, above the table surface). */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"} onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1) }}>\n {/* SearchInput is value + onSearch(v) — NOT onChange */}\n <SearchInput placeholder=\"クーポン名で検索\" value={q} onSearch={(v) => { setQ(v); setPage(1) }} />\n <ToolbarGroup label=\"ステータス\">\n <Select value={status} onValueChange={(v) => { setStatus(v); setPage(1) }}>\n <SelectTrigger><SelectValue /></SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">全ステータス</SelectItem>\n <SelectItem value=\"公開中\">公開中</SelectItem>\n <SelectItem value=\"下書き\">下書き</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n <Card>\n <CardContent flush>\n {filtered.length === 0\n ? <EmptyState title=\"該当するクーポンがありません\" description=\"検索条件を変更してください。\" /> /* one sentence, no icon */\n : <DataTable data={paged} columns={columns} getRowId={(c) => c.id} onRowClick={(c) => router.visit(`/coupons/${c.id}`)} />}\n </CardContent>\n </Card>\n\n {/* Pagination ONLY for real multi-page data — hide it while empty or single-page. */}\n {filtered.length > PAGE_SIZE && (\n <Pagination value={page} total={filtered.length} pageSize={PAGE_SIZE} showTotal onValueChange={(p) => setPage(p)} />\n )}\n </Flex>\n </PageContainer>\n </>\n )\n}\n\nCoupons.layout = withCrmLayout\nexport default Coupons",
|
|
155
155
|
"name": "inertia-list-page",
|
|
156
|
-
"tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar + DataTable + Badge + Pagination
|
|
156
|
+
"tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar (above the table) + DataTable + Badge + Pagination. No KPI row, no restating subtitle.",
|
|
157
157
|
"tags": [
|
|
158
158
|
"inertia",
|
|
159
159
|
"list",
|
|
@@ -166,9 +166,9 @@
|
|
|
166
166
|
]
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
|
-
"code": "import { Head, router } from \"@inertiajs/react\"\nimport { PageContainer
|
|
169
|
+
"code": "import { Head, router } from \"@inertiajs/react\"\nimport { PageContainer } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, EmptyState, Descriptions, Badge } from \"@godxjp/ui/data-display\"\nimport { Button } from \"@godxjp/ui/general\"\nimport { formatDate } from \"@godxjp/ui/datetime\"\nimport { ArrowLeft } from \"lucide-react\"\nimport { withCrmLayout } from \"@/layouts/crm-layout\"\n\n// Detail routes pass the param as an Inertia prop:\n// Route::get('/members/{id}', fn ($id) => Inertia::render('crm/members/show', ['id' => $id]))\nfunction MemberShow({ id }: { id: string }) {\n const member = MEMBERS.find((m) => m.id === id)\n\n if (!member) {\n return (\n <>\n <Head title=\"会員詳細\" />\n <PageContainer title=\"会員詳細\">\n <EmptyState title=\"会員が見つかりません\" description={`ID「${id}」は存在しません。`} />\n <Button variant=\"outline\" onClick={() => router.visit(\"/members\")}><ArrowLeft className=\"size-4\" />一覧へ戻る</Button>\n </PageContainer>\n </>\n )\n }\n\n return (\n <>\n <Head title={member.name} />\n <PageContainer title={member.name}>\n {/* Figures about one member are facts, not KPIs: Descriptions rows, not a StatCard row. */}\n <Card>\n <CardContent>\n {/* Descriptions is COMPOUND — value goes in children, not a prop */}\n <Descriptions columns={2}>\n <Descriptions.Item label=\"会員ID\">{member.id}</Descriptions.Item>\n <Descriptions.Item label=\"ランク\">{member.rank}</Descriptions.Item>\n <Descriptions.Item label=\"ステータス\"><Badge status={member.status} /></Descriptions.Item>\n <Descriptions.Item label=\"登録日\">{formatDate(member.registeredAt)}</Descriptions.Item>\n <Descriptions.Item label=\"累計購入額\">{`¥${member.total.toLocaleString()}`}</Descriptions.Item>\n <Descriptions.Item label=\"来店回数\">{member.visits}</Descriptions.Item>\n </Descriptions>\n </CardContent>\n </Card>\n </PageContainer>\n </>\n )\n}\n\nMemberShow.layout = withCrmLayout\nexport default MemberShow",
|
|
170
170
|
"name": "inertia-detail-page",
|
|
171
|
-
"tagline": "Inertia detail page — receives {id} prop, Descriptions (compound)
|
|
171
|
+
"tagline": "Inertia detail page — receives {id} prop, Descriptions (compound) in one Card + EmptyState fallback. No KPI tile row.",
|
|
172
172
|
"tags": [
|
|
173
173
|
"inertia",
|
|
174
174
|
"detail",
|
|
@@ -221,9 +221,9 @@
|
|
|
221
221
|
]
|
|
222
222
|
},
|
|
223
223
|
{
|
|
224
|
-
"code": "import { Badge } from \"@godxjp/ui/data-display\"\n\n// Badge auto-colours a fixed set of English LIFECYCLE keys:\n// active/completed (success ✓) · draft (neutral ○) · pending/temporary (warning ⏱)\n// scheduled/sending (info) · cancelled (neutral) · failed/deleted/bounced (destructive ✕)\n// Anything else (localized labels, tiers) falls back to neutral grey ○ unless you override.\n\n// 1) Lifecycle with localized text — map to the key, keep JP via children (icon stays):\n<Badge status=\"active\">公開中</Badge> // green ✓ 公開中\n\n// 2) Unknown label — set tone explicitly (no icon, since the key is unknown):\n<Badge status=\"公開中\" tone=\"success\" />\n\n// 3) Tier / category —
|
|
224
|
+
"code": "import { Badge } from \"@godxjp/ui/data-display\"\n\n// Badge auto-colours a fixed set of English LIFECYCLE keys:\n// active/completed (success ✓) · draft (neutral ○) · pending/temporary (warning ⏱)\n// scheduled/sending (info) · cancelled (neutral) · failed/deleted/bounced (destructive ✕)\n// Anything else (localized labels, tiers) falls back to neutral grey ○ unless you override.\n\n// 1) Lifecycle with localized text — map to the key, keep JP via children (icon stays):\n<Badge status=\"active\">公開中</Badge> // green ✓ 公開中\n\n// 2) Unknown label — set tone explicitly (no icon, since the key is unknown):\n<Badge status=\"公開中\" tone=\"success\" />\n\n// 3) Tier / category — ONE neutral tone for all of them, no glyph. Colour means status;\n// a rainbow of tiers reads as several statuses. Prefer plain text in a table column.\n<Badge status=\"プレミアム\" tone=\"neutral\" icon={null} />\n<Badge status=\"ゴールド\" tone=\"neutral\" icon={null} />\n\n// tone: \"success\" | \"warning\" | \"destructive\" | \"info\" | \"neutral\" (import type ToneProp)\n// RULE: a chip never wraps — it is pinned white-space: nowrap, so it stays one line in\n// narrow table cells. Centralize the domain→tone map in ONE small consumer wrapper and\n// import that instead of the raw Badge across pages.",
|
|
225
225
|
"name": "badge-coloring",
|
|
226
|
-
"tagline": "Colour a Badge for localized
|
|
226
|
+
"tagline": "Colour a Badge for a localized STATUS via tone + icon; tiers and categories take one neutral tone (colour means status).",
|
|
227
227
|
"tags": [
|
|
228
228
|
"statusbadge",
|
|
229
229
|
"badge",
|
|
@@ -367,16 +367,18 @@
|
|
|
367
367
|
"page-layout",
|
|
368
368
|
"stack-cards"
|
|
369
369
|
],
|
|
370
|
-
"code": "import { PageContainer, Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { Card,
|
|
370
|
+
"code": "import { PageContainer, Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { Card, CardContent, DataTable, Descriptions, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { Button, Heading } from \"@godxjp/ui/general\";\n\ntype Shot = { id: string; name: string };\nconst columns: ColumnDef<Shot>[] = [{ key: \"name\", header: \"File\" }];\n\n// Two steps, never one uniform gap:\n// section step — between direct children of PageContainer (--page-body-gap). Do NOT add gap-*/mt-*.\n// item step — inside a section, Flex gap=\"sm\" between its heading and its content.\n// A section is a Heading, not a Card. Card is for a real boundary: here, the table surface.\nexport function ReportPage({ shots }: { shots: Shot[] }) {\n return (\n <PageContainer title=\"Bug report\" extra={<Button>Send</Button>}>\n <Flex direction=\"col\" gap=\"sm\">\n <Heading level={2}>Details</Heading>\n <Descriptions columns={2}>\n <Descriptions.Item label=\"URL\">https://app.example.com/orders</Descriptions.Item>\n <Descriptions.Item label=\"Browser\">Safari 18</Descriptions.Item>\n </Descriptions>\n </Flex>\n <Flex direction=\"col\" gap=\"sm\">\n <Heading level={2}>Screenshots</Heading>\n <ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>…</ResponsiveGrid>\n </Flex>\n <Flex direction=\"col\" gap=\"sm\">\n <Heading level={2}>Attachments</Heading>\n <Card>\n <CardContent flush>\n <DataTable data={shots} columns={columns} getRowId={(r) => r.id} />\n </CardContent>\n </Card>\n </Flex>\n </PageContainer>\n );\n}\n",
|
|
371
371
|
"name": "page-sections",
|
|
372
|
-
"tagline": "A page
|
|
372
|
+
"tagline": "A page of several sections: a Heading + section spacing marks a section, not a Card. PageContainer spaces its direct children (section step); Flex gap spaces items inside one (item step). Card only for a real boundary.",
|
|
373
373
|
"tags": [
|
|
374
374
|
"layout",
|
|
375
375
|
"page",
|
|
376
376
|
"card",
|
|
377
377
|
"spacing",
|
|
378
378
|
"flex",
|
|
379
|
-
"grid"
|
|
379
|
+
"grid",
|
|
380
|
+
"section",
|
|
381
|
+
"heading"
|
|
380
382
|
]
|
|
381
383
|
},
|
|
382
384
|
{
|
package/agent/tokens.json
CHANGED
|
@@ -7877,6 +7877,18 @@
|
|
|
7877
7877
|
"tier": "component",
|
|
7878
7878
|
"value": "15rem"
|
|
7879
7879
|
},
|
|
7880
|
+
{
|
|
7881
|
+
"description": "Page inline margin of a shell used BARE, the way a public reader page uses it (gh#1296): default = --space-page-x, --space-page-compact-x at ≤720px, resolved at the call site because both re-derive under a density scope. Inside PageContainer / CenteredShell, which already pad, it resolves to 0 so the gutter is never doubled.",
|
|
7882
|
+
"name": "--legal-document-page-padding-inline",
|
|
7883
|
+
"tier": "component",
|
|
7884
|
+
"value": "initial"
|
|
7885
|
+
},
|
|
7886
|
+
{
|
|
7887
|
+
"description": "Document title size — two type-scale steps above a section heading, so the h1 reads as the page title and not as one more section (gh#1296). Default = the --font-size-2xl step.",
|
|
7888
|
+
"name": "--legal-document-title-font-size",
|
|
7889
|
+
"tier": "component",
|
|
7890
|
+
"value": "initial"
|
|
7891
|
+
},
|
|
7880
7892
|
{
|
|
7881
7893
|
"description": "Distance the pinned rail keeps from the top of the scrollport.",
|
|
7882
7894
|
"name": "--legal-document-toc-inset-block-start",
|
|
@@ -632,10 +632,11 @@ function DataSelect(props) {
|
|
|
632
632
|
"aria-label": props["aria-label"],
|
|
633
633
|
"aria-labelledby": props["aria-labelledby"]
|
|
634
634
|
});
|
|
635
|
-
const [
|
|
635
|
+
const [nativeLabel, setNativeLabel] = React.useState(null);
|
|
636
|
+
const hasNativeLabel = nativeLabel !== null;
|
|
636
637
|
useLayoutEffect(() => {
|
|
637
|
-
const
|
|
638
|
-
|
|
638
|
+
const label = typeof document !== "undefined" && props.id !== void 0 ? document.querySelector(`label[for="${CSS.escape(props.id)}"]`) : null;
|
|
639
|
+
setNativeLabel(label ? (label.textContent ?? "").trim() || null : null);
|
|
639
640
|
}, [props.id]);
|
|
640
641
|
if (props.mode !== void 0 || searchable) {
|
|
641
642
|
const antdFilter = showSearchConfig?.filterOption;
|
|
@@ -742,7 +743,7 @@ function DataSelect(props) {
|
|
|
742
743
|
},
|
|
743
744
|
disabled: disabled || !hasOptions && !showEmptyPopup,
|
|
744
745
|
allowsEmptyCollection: showEmptyPopup,
|
|
745
|
-
"aria-label": ariaProps["aria-label"] ?? nameFromPlaceholder,
|
|
746
|
+
"aria-label": ariaProps["aria-label"] ?? nameFromPlaceholder ?? nativeLabel ?? void 0,
|
|
746
747
|
"aria-labelledby": ariaProps["aria-labelledby"] ?? inheritedName["aria-labelledby"],
|
|
747
748
|
name: resolvedName,
|
|
748
749
|
open,
|
|
@@ -61,12 +61,18 @@ function PageContainerRoot({
|
|
|
61
61
|
children,
|
|
62
62
|
className
|
|
63
63
|
}) {
|
|
64
|
-
const
|
|
64
|
+
const hasTitle = title != null && title !== false && title !== "";
|
|
65
|
+
const hasIcon = icon != null && icon !== false;
|
|
66
|
+
const { start: extraStart, end: extraEnd } = resolvePageExtra(extra);
|
|
67
|
+
const hasHeading = headerLoading || hasTitle || hasIcon || status != null || !!subtitle;
|
|
68
|
+
const hasBreadcrumb = !!breadcrumb && breadcrumb.length > 0;
|
|
69
|
+
const hasEyecatch = isValidElement(cover) && cover.props.variant === "eyecatch";
|
|
70
|
+
const hasHeader = hasHeading || hasBreadcrumb || hasEyecatch || extraStart != null || extraEnd != null;
|
|
71
|
+
const reveal = stickyFooter && footer != null && footerReveal === "onScroll" && hasHeader;
|
|
65
72
|
const { headerRef, revealed } = useFooterReveal(reveal);
|
|
66
73
|
const footerRef = useRef(null);
|
|
67
74
|
useToastStickyBarClearance([footerRef], stickyFooter && !!footer && (!reveal || revealed));
|
|
68
75
|
const { t } = useTranslation();
|
|
69
|
-
const { start: extraStart, end: extraEnd } = resolvePageExtra(extra);
|
|
70
76
|
const titleFocus = titleRef === void 0 ? void 0 : { ref: titleRef, tabIndex: -1 };
|
|
71
77
|
const coverVariant = cover == null || cover === false ? void 0 : isValidElement(cover) && cover.props.variant === "eyecatch" ? "eyecatch" : "banner";
|
|
72
78
|
return /* @__PURE__ */ jsxs(
|
|
@@ -90,7 +96,7 @@ function PageContainerRoot({
|
|
|
90
96
|
),
|
|
91
97
|
children: [
|
|
92
98
|
coverVariant === "banner" ? /* @__PURE__ */ jsx("div", { className: "ui-page-cover-slot", children: cover }) : null,
|
|
93
|
-
/* @__PURE__ */ jsxs(
|
|
99
|
+
hasHeader && /* @__PURE__ */ jsxs(
|
|
94
100
|
"header",
|
|
95
101
|
{
|
|
96
102
|
ref: headerRef,
|
|
@@ -98,7 +104,7 @@ function PageContainerRoot({
|
|
|
98
104
|
"data-layout": headerLayout,
|
|
99
105
|
"aria-busy": headerLoading ? "true" : void 0,
|
|
100
106
|
children: [
|
|
101
|
-
|
|
107
|
+
hasBreadcrumb && /* @__PURE__ */ jsx(
|
|
102
108
|
"nav",
|
|
103
109
|
{
|
|
104
110
|
"aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
|
|
@@ -144,8 +150,8 @@ function PageContainerRoot({
|
|
|
144
150
|
),
|
|
145
151
|
coverVariant === "eyecatch" ? /* @__PURE__ */ jsx("div", { className: "ui-page-cover-slot", children: cover }) : null,
|
|
146
152
|
/* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
|
|
147
|
-
/* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading", children: [
|
|
148
|
-
|
|
153
|
+
hasHeading && /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading", children: [
|
|
154
|
+
hasIcon ? /* @__PURE__ */ jsx("div", { className: "ui-page-header-icon", children: icon }) : null,
|
|
149
155
|
headerLoading ? /* @__PURE__ */ jsx(
|
|
150
156
|
"h1",
|
|
151
157
|
{
|
|
@@ -154,9 +160,9 @@ function PageContainerRoot({
|
|
|
154
160
|
children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("layout.pageHeader.loading") })
|
|
155
161
|
}
|
|
156
162
|
) : status != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-header-title-row", children: [
|
|
157
|
-
/* @__PURE__ */ jsx("h1", { ...titleFocus, className: "ui-page-title", children: title }),
|
|
163
|
+
hasTitle && /* @__PURE__ */ jsx("h1", { ...titleFocus, className: "ui-page-title", children: title }),
|
|
158
164
|
/* @__PURE__ */ jsx("div", { className: "ui-page-header-status", children: status })
|
|
159
|
-
] }) : /* @__PURE__ */ jsx("h1", { ...titleFocus, className: "ui-page-title", children: title }),
|
|
165
|
+
] }) : hasTitle ? /* @__PURE__ */ jsx("h1", { ...titleFocus, className: "ui-page-title", children: title }) : null,
|
|
160
166
|
headerLoading ? (
|
|
161
167
|
// Decorative only — the pending state is already announced once by the heading above,
|
|
162
168
|
// so a second live placeholder here would double-announce it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.7.0",
|
|
4
4
|
"targetSize": {
|
|
5
5
|
"standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
|
|
6
6
|
"min": 24,
|
|
@@ -324,7 +324,8 @@
|
|
|
324
324
|
"legalDocumentShell": {
|
|
325
325
|
"contents": "Contents",
|
|
326
326
|
"version": "Version {version}",
|
|
327
|
-
"effectiveDate": "Effective {date}"
|
|
327
|
+
"effectiveDate": "Effective {date}",
|
|
328
|
+
"updatedDate": "Updated {date}"
|
|
328
329
|
},
|
|
329
330
|
"errorSurface": {
|
|
330
331
|
"statusLabel": "HTTP status {status}",
|
|
@@ -319,7 +319,8 @@
|
|
|
319
319
|
"legalDocumentShell": {
|
|
320
320
|
"contents": "目次",
|
|
321
321
|
"version": "バージョン {version}",
|
|
322
|
-
"effectiveDate": "施行日 {date}"
|
|
322
|
+
"effectiveDate": "施行日 {date}",
|
|
323
|
+
"updatedDate": "更新日 {date}"
|
|
323
324
|
},
|
|
324
325
|
"errorSurface": {
|
|
325
326
|
"statusLabel": "HTTP ステータス {status}",
|
|
@@ -320,7 +320,8 @@
|
|
|
320
320
|
"legalDocumentShell": {
|
|
321
321
|
"contents": "Mục lục",
|
|
322
322
|
"version": "Phiên bản {version}",
|
|
323
|
-
"effectiveDate": "Hiệu lực từ {date}"
|
|
323
|
+
"effectiveDate": "Hiệu lực từ {date}",
|
|
324
|
+
"updatedDate": "Cập nhật {date}"
|
|
324
325
|
},
|
|
325
326
|
"errorSurface": {
|
|
326
327
|
"statusLabel": "Trạng thái HTTP {status}",
|
|
@@ -33,6 +33,7 @@ const LegalDocumentShell = React.forwardRef(
|
|
|
33
33
|
title,
|
|
34
34
|
version,
|
|
35
35
|
effectiveDate,
|
|
36
|
+
dateKind = "effective",
|
|
36
37
|
summary,
|
|
37
38
|
contentsLabel,
|
|
38
39
|
sections,
|
|
@@ -135,9 +136,12 @@ const LegalDocumentShell = React.forwardRef(
|
|
|
135
136
|
version !== void 0 && /* @__PURE__ */ jsx("span", { children: t("layout.legalDocumentShell.version", { version }) }),
|
|
136
137
|
effectiveDate !== void 0 && // <time dateTime> keeps the MACHINE-readable value as the ISO 8601 input while
|
|
137
138
|
// the visible text is the Intl-formatted, locale-correct rendering.
|
|
138
|
-
/* @__PURE__ */ jsx("time", { dateTime: effectiveDate, children: t(
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
/* @__PURE__ */ jsx("time", { dateTime: effectiveDate, children: t(
|
|
140
|
+
dateKind === "updated" ? "layout.legalDocumentShell.updatedDate" : "layout.legalDocumentShell.effectiveDate",
|
|
141
|
+
{
|
|
142
|
+
date: formatEffectiveDate(effectiveDate, locale)
|
|
143
|
+
}
|
|
144
|
+
) })
|
|
141
145
|
] }),
|
|
142
146
|
summary !== void 0 && // A <div> (not <p>): the summary slot may carry block content, and a <p> inside a <p>
|
|
143
147
|
// is invalid HTML the browser silently un-nests.
|
|
@@ -44,7 +44,13 @@ export type PageContainerExtraProp = ExtraProp | {
|
|
|
44
44
|
export type PageContainerProp = {
|
|
45
45
|
toolbarPad?: PadProp;
|
|
46
46
|
footerPad?: PadProp;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The page's `<h1>`. Optional for a page whose CONTENT draws its own heading (a document reader,
|
|
49
|
+
* a LegalDocumentShell): omitted, PageContainer renders no `<h1>` at all, and a header with no
|
|
50
|
+
* other slot is not rendered. The page must then still have exactly one `<h1>` — from the
|
|
51
|
+
* content (WCAG 1.3.1 / 2.4.6); `titleRef` has nothing to attach to.
|
|
52
|
+
*/
|
|
53
|
+
title?: TitleProp;
|
|
48
54
|
subtitle?: SubtitleProp;
|
|
49
55
|
/**
|
|
50
56
|
* Status/meta band rendered beside the title inside the heading — StatusBadge, environment tag,
|
|
@@ -1676,6 +1682,12 @@ export type LegalDocumentShellProp = {
|
|
|
1676
1682
|
* `<time dateTime={effectiveDate}>`, so the machine-readable value is always the ISO input.
|
|
1677
1683
|
*/
|
|
1678
1684
|
effectiveDate?: string;
|
|
1685
|
+
/**
|
|
1686
|
+
* What the date in `effectiveDate` means, which picks its localized caption: `"effective"`
|
|
1687
|
+
* (default — 「施行日」 / "Effective", a policy in force from that date) or `"updated"`
|
|
1688
|
+
* (「更新日」 / "Updated", a wiki page or help article last revised on that date).
|
|
1689
|
+
*/
|
|
1690
|
+
dateKind?: "effective" | "updated";
|
|
1679
1691
|
/** Short plain-language summary rendered under the metadata, above the contents. */
|
|
1680
1692
|
summary?: ReactNode;
|
|
1681
1693
|
/** Accessible name + visible caption of the contents `<nav>` (e.g. "Contents"). */
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -1309,6 +1309,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
1309
1309
|
readonly field: "effectiveDate";
|
|
1310
1310
|
readonly local: true;
|
|
1311
1311
|
readonly reason: "ISO 8601 date input formatted via Intl.DateTimeFormat in the active locale.";
|
|
1312
|
+
}, {
|
|
1313
|
+
readonly field: "dateKind";
|
|
1314
|
+
readonly local: true;
|
|
1315
|
+
readonly reason: "Meaning of the date (effective | updated) — selects the localized caption, so the consumer never concatenates a label with a formatted date.";
|
|
1312
1316
|
}, {
|
|
1313
1317
|
readonly field: "summary";
|
|
1314
1318
|
readonly local: true;
|
package/dist/props/registry.js
CHANGED
|
@@ -1402,6 +1402,11 @@ const COMPONENT_PROP_REGISTRY = {
|
|
|
1402
1402
|
local: true,
|
|
1403
1403
|
reason: "ISO 8601 date input formatted via Intl.DateTimeFormat in the active locale."
|
|
1404
1404
|
},
|
|
1405
|
+
{
|
|
1406
|
+
field: "dateKind",
|
|
1407
|
+
local: true,
|
|
1408
|
+
reason: "Meaning of the date (effective | updated) \u2014 selects the localized caption, so the consumer never concatenates a label with a formatted date."
|
|
1409
|
+
},
|
|
1405
1410
|
{
|
|
1406
1411
|
field: "summary",
|
|
1407
1412
|
local: true,
|
package/dist/styles/layers.json
CHANGED
package/dist/styles/layout.css
CHANGED
|
@@ -1107,6 +1107,10 @@
|
|
|
1107
1107
|
justify-content: flex-end;
|
|
1108
1108
|
}
|
|
1109
1109
|
|
|
1110
|
+
.ui-page-header-extra:only-child {
|
|
1111
|
+
margin-inline-start: auto;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1110
1114
|
.ui-page-container[data-header-scale="chrome"] .ui-page-header-extra {
|
|
1111
1115
|
align-self: center;
|
|
1112
1116
|
}
|
|
@@ -1595,6 +1599,28 @@
|
|
|
1595
1599
|
|
|
1596
1600
|
.ui-legal-document-scope {
|
|
1597
1601
|
container: legal-document / inline-size;
|
|
1602
|
+
padding-inline: var(--legal-document-page-padding-inline, var(--space-page-x));
|
|
1603
|
+
|
|
1604
|
+
--heading-h1: var(
|
|
1605
|
+
--legal-document-title-font-size,
|
|
1606
|
+
var(
|
|
1607
|
+
--font-size-2xl,
|
|
1608
|
+
calc(
|
|
1609
|
+
var(--font-size-base) * var(--font-size-ratio) * var(--font-size-ratio) *
|
|
1610
|
+
var(--font-size-ratio) * var(--font-size-ratio)
|
|
1611
|
+
)
|
|
1612
|
+
)
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
@media (max-width: 720px) {
|
|
1617
|
+
.ui-legal-document-scope {
|
|
1618
|
+
padding-inline: var(--legal-document-page-padding-inline, var(--space-page-compact-x));
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
:where(.ui-page-body, .ui-centered-shell-main) .ui-legal-document-scope {
|
|
1623
|
+
--legal-document-page-padding-inline: 0px;
|
|
1598
1624
|
}
|
|
1599
1625
|
|
|
1600
1626
|
.ui-legal-document {
|