@godxjp/ui 28.9.0 → 28.10.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.
Files changed (232) hide show
  1. package/agent/START-HERE.md +193 -0
  2. package/agent/anti-ai-tells.json +158 -0
  3. package/agent/components/Accordion.json +60 -0
  4. package/agent/components/AccountChip.json +59 -0
  5. package/agent/components/Actions.json +78 -0
  6. package/agent/components/Activity.json +80 -0
  7. package/agent/components/Affix.json +78 -0
  8. package/agent/components/Alert.json +65 -0
  9. package/agent/components/AlertDialog.json +109 -0
  10. package/agent/components/AlertDialogRoot.json +52 -0
  11. package/agent/components/Anchor.json +119 -0
  12. package/agent/components/AppLauncher.json +95 -0
  13. package/agent/components/AppProvider.json +105 -0
  14. package/agent/components/AppSettingPicker.json +88 -0
  15. package/agent/components/AppSettingToggle.json +70 -0
  16. package/agent/components/AppShell.json +159 -0
  17. package/agent/components/AreaChart.json +105 -0
  18. package/agent/components/AspectRatio.json +38 -0
  19. package/agent/components/Attachments.json +76 -0
  20. package/agent/components/AuthAccountSummary.json +64 -0
  21. package/agent/components/AuthDivider.json +35 -0
  22. package/agent/components/AuthFooter.json +48 -0
  23. package/agent/components/AuthIdentity.json +42 -0
  24. package/agent/components/AuthShell.json +108 -0
  25. package/agent/components/AuthStack.json +21 -0
  26. package/agent/components/Avatar.json +89 -0
  27. package/agent/components/Badge.json +96 -0
  28. package/agent/components/Banner.json +48 -0
  29. package/agent/components/BarChart.json +108 -0
  30. package/agent/components/BranchScopePicker.json +89 -0
  31. package/agent/components/Breadcrumb.json +54 -0
  32. package/agent/components/Button.json +133 -0
  33. package/agent/components/Calendar.json +259 -0
  34. package/agent/components/Callout.json +46 -0
  35. package/agent/components/Card.json +112 -0
  36. package/agent/components/CardBar.json +49 -0
  37. package/agent/components/CardContent.json +51 -0
  38. package/agent/components/Carousel.json +50 -0
  39. package/agent/components/Cascader.json +209 -0
  40. package/agent/components/CenteredShell.json +66 -0
  41. package/agent/components/ChatBubble.json +100 -0
  42. package/agent/components/ChatBubbleList.json +64 -0
  43. package/agent/components/ChatComposer.json +160 -0
  44. package/agent/components/ChatSuggestion.json +86 -0
  45. package/agent/components/Checkbox.json +68 -0
  46. package/agent/components/CheckboxGroup.json +96 -0
  47. package/agent/components/CodeBlock.json +64 -0
  48. package/agent/components/Collapsible.json +74 -0
  49. package/agent/components/ColorPicker.json +87 -0
  50. package/agent/components/Command.json +168 -0
  51. package/agent/components/CommandPalette.json +84 -0
  52. package/agent/components/CompactBarTrend.json +101 -0
  53. package/agent/components/Conversations.json +82 -0
  54. package/agent/components/CredentialReveal.json +93 -0
  55. package/agent/components/DataState.json +79 -0
  56. package/agent/components/DataTable.json +268 -0
  57. package/agent/components/DatePicker.json +275 -0
  58. package/agent/components/Descriptions.json +67 -0
  59. package/agent/components/Dialog.json +78 -0
  60. package/agent/components/DraggablePanel.json +106 -0
  61. package/agent/components/DropdownMenu.json +102 -0
  62. package/agent/components/EmptyState.json +83 -0
  63. package/agent/components/ErrorSurface.json +128 -0
  64. package/agent/components/FeatureList.json +43 -0
  65. package/agent/components/Field.json +64 -0
  66. package/agent/components/FilterBar.json +99 -0
  67. package/agent/components/Flex.json +153 -0
  68. package/agent/components/FloatButton.json +91 -0
  69. package/agent/components/Form.json +87 -0
  70. package/agent/components/FormErrors.json +51 -0
  71. package/agent/components/FormField.json +137 -0
  72. package/agent/components/FormFieldArray.json +39 -0
  73. package/agent/components/FormFieldControl.json +129 -0
  74. package/agent/components/FormRoot.json +122 -0
  75. package/agent/components/Heading.json +61 -0
  76. package/agent/components/HoverCard.json +55 -0
  77. package/agent/components/Icon.json +60 -0
  78. package/agent/components/InfiniteQueryState.json +58 -0
  79. package/agent/components/Input.json +122 -0
  80. package/agent/components/InputOTP.json +106 -0
  81. package/agent/components/Label.json +43 -0
  82. package/agent/components/LegalDocumentShell.json +102 -0
  83. package/agent/components/Legend.json +42 -0
  84. package/agent/components/LineChart.json +103 -0
  85. package/agent/components/Link.json +41 -0
  86. package/agent/components/ListRow.json +92 -0
  87. package/agent/components/Logo.json +85 -0
  88. package/agent/components/Marquee.json +91 -0
  89. package/agent/components/Masonry.json +82 -0
  90. package/agent/components/MasterDetail.json +95 -0
  91. package/agent/components/MegaMenu.json +120 -0
  92. package/agent/components/MobileShell.json +73 -0
  93. package/agent/components/NavList.json +63 -0
  94. package/agent/components/NumberInput.json +158 -0
  95. package/agent/components/OrgSwitcher.json +89 -0
  96. package/agent/components/OverlayPortalProvider.json +42 -0
  97. package/agent/components/PageContainer.json +181 -0
  98. package/agent/components/Pagination.json +132 -0
  99. package/agent/components/Paragraph.json +40 -0
  100. package/agent/components/PasswordInput.json +79 -0
  101. package/agent/components/PasswordStrength.json +51 -0
  102. package/agent/components/PermissionMatrix.json +81 -0
  103. package/agent/components/PieChart.json +99 -0
  104. package/agent/components/Popover.json +110 -0
  105. package/agent/components/PrefetchLink.json +65 -0
  106. package/agent/components/Progress.json +79 -0
  107. package/agent/components/Prose.json +57 -0
  108. package/agent/components/QrCode.json +62 -0
  109. package/agent/components/Radio.json +98 -0
  110. package/agent/components/RadioGroup.json +91 -0
  111. package/agent/components/RangeTimeline.json +80 -0
  112. package/agent/components/Rating.json +92 -0
  113. package/agent/components/ResizablePanel.json +69 -0
  114. package/agent/components/ResponsiveGrid.json +77 -0
  115. package/agent/components/Reveal.json +70 -0
  116. package/agent/components/ScrollArea.json +104 -0
  117. package/agent/components/SearchInput.json +98 -0
  118. package/agent/components/Segmented.json +96 -0
  119. package/agent/components/Select.json +397 -0
  120. package/agent/components/Separator.json +86 -0
  121. package/agent/components/ServiceCatalogCta.json +46 -0
  122. package/agent/components/ServiceLauncherCard.json +86 -0
  123. package/agent/components/ServiceRolePanel.json +83 -0
  124. package/agent/components/Sheet.json +85 -0
  125. package/agent/components/Sidebar.json +118 -0
  126. package/agent/components/Skeleton.json +57 -0
  127. package/agent/components/SkeletonArticle.json +71 -0
  128. package/agent/components/SkeletonAvatar.json +50 -0
  129. package/agent/components/SkeletonButton.json +57 -0
  130. package/agent/components/SkeletonForm.json +52 -0
  131. package/agent/components/SkeletonImage.json +37 -0
  132. package/agent/components/SkeletonInput.json +51 -0
  133. package/agent/components/SkeletonNode.json +42 -0
  134. package/agent/components/SkeletonRows.json +49 -0
  135. package/agent/components/SkeletonTable.json +45 -0
  136. package/agent/components/Slider.json +160 -0
  137. package/agent/components/SplitPane.json +66 -0
  138. package/agent/components/StatCard.json +83 -0
  139. package/agent/components/Steps.json +95 -0
  140. package/agent/components/Swatch.json +41 -0
  141. package/agent/components/Switch.json +81 -0
  142. package/agent/components/Table.json +112 -0
  143. package/agent/components/Tabs.json +158 -0
  144. package/agent/components/TagInput.json +105 -0
  145. package/agent/components/Text.json +201 -0
  146. package/agent/components/Textarea.json +126 -0
  147. package/agent/components/ThoughtChain.json +76 -0
  148. package/agent/components/Thumbnail.json +70 -0
  149. package/agent/components/TimePicker.json +200 -0
  150. package/agent/components/TimeRangePicker.json +90 -0
  151. package/agent/components/Timeline.json +47 -0
  152. package/agent/components/TimelineGrid.json +92 -0
  153. package/agent/components/Title.json +67 -0
  154. package/agent/components/Toaster.json +42 -0
  155. package/agent/components/Toggle.json +90 -0
  156. package/agent/components/ToggleGroup.json +102 -0
  157. package/agent/components/Toolbar.json +120 -0
  158. package/agent/components/Tooltip.json +110 -0
  159. package/agent/components/Topbar.json +83 -0
  160. package/agent/components/TopbarItem.json +79 -0
  161. package/agent/components/Transfer.json +141 -0
  162. package/agent/components/Tree.json +185 -0
  163. package/agent/components/TreeSelect.json +232 -0
  164. package/agent/components/TwoFactorSetup.json +79 -0
  165. package/agent/components/Typography.json +42 -0
  166. package/agent/components/Upload.json +221 -0
  167. package/agent/components/UploadCropDialog.json +60 -0
  168. package/agent/components/VisuallyHidden.json +20 -0
  169. package/agent/components/Welcome.json +65 -0
  170. package/agent/components/formatDate.json +46 -0
  171. package/agent/components/inertiaUpload.json +32 -0
  172. package/agent/components/useZodForm.json +39 -0
  173. package/agent/components-index.json +884 -0
  174. package/agent/components.json +15507 -0
  175. package/agent/index.json +56 -0
  176. package/agent/llms.txt +32 -0
  177. package/agent/patterns/account-recovery-settings.json +19 -0
  178. package/agent/patterns/async-data-state.json +20 -0
  179. package/agent/patterns/auth-recovery-panels.json +29 -0
  180. package/agent/patterns/badge-coloring.json +14 -0
  181. package/agent/patterns/common-fixes.json +16 -0
  182. package/agent/patterns/confirm-destructive.json +11 -0
  183. package/agent/patterns/data-table-page.json +18 -0
  184. package/agent/patterns/deferred-loading.json +12 -0
  185. package/agent/patterns/error-pages.json +28 -0
  186. package/agent/patterns/inertia-detail-page.json +13 -0
  187. package/agent/patterns/inertia-list-page.json +15 -0
  188. package/agent/patterns/inertia-persistent-layout.json +14 -0
  189. package/agent/patterns/organization-memberships.json +19 -0
  190. package/agent/patterns/page-sections.json +18 -0
  191. package/agent/patterns/settings-page-responsive.json +18 -0
  192. package/agent/patterns/settings-section-rows.json +23 -0
  193. package/agent/patterns/signup-form.json +13 -0
  194. package/agent/patterns/topbar-account-chip.json +18 -0
  195. package/agent/patterns/transactional-email.json +22 -0
  196. package/agent/patterns-index.json +323 -0
  197. package/agent/patterns.json +342 -0
  198. package/agent/rules.json +237 -0
  199. package/agent/tokens.json +8422 -0
  200. package/agent/vocabulary.json +198 -0
  201. package/dist/components/data-entry/input.js +8 -1
  202. package/dist/components/layout/flex.d.ts +2 -2
  203. package/dist/components/layout/flex.js +2 -0
  204. package/dist/components/ui/tag-input.d.ts +10 -0
  205. package/dist/components/ui/tag-input.js +35 -2
  206. package/dist/contracts/measurement.json +1 -1
  207. package/dist/i18n/messages/en.json +23 -1
  208. package/dist/i18n/messages/ja.json +21 -1
  209. package/dist/i18n/messages/vi.json +21 -1
  210. package/dist/props/components/data-entry.prop.d.ts +21 -2
  211. package/dist/props/components/layout.prop.d.ts +42 -0
  212. package/dist/props/registry.d.ts +9 -0
  213. package/dist/props/registry.js +6 -0
  214. package/dist/styles/card-layout.css +4 -4
  215. package/dist/styles/control.css +31 -2
  216. package/dist/styles/data-display-layout.css +1 -1
  217. package/dist/styles/layout.css +17 -0
  218. package/dist/styles/navigation-layout.css +3 -1
  219. package/dist/styles/shell-layout.css +2 -0
  220. package/dist/styles/table-layout.css +50 -9
  221. package/dist/tokens/components/segmented.css +5 -1
  222. package/dist/tokens/components/table.css +2 -1
  223. package/docs/COMPOSITION-VS-COMPONENT.md +41 -0
  224. package/docs/DESIGN-AUTHORITY.md +14 -0
  225. package/docs/DEVELOPMENT.md +81 -6
  226. package/docs/data-entry/tag-input.tsx +37 -0
  227. package/docs/layout/flex.tsx +40 -0
  228. package/docs/roadmap/website-components.md +34 -0
  229. package/docs/showcase/marketing-page.tsx +54 -45
  230. package/docs/showcase/table-pagination.tsx +88 -18
  231. package/docs/showcase/theme-customization.tsx +25 -2
  232. package/package.json +6 -4
@@ -0,0 +1,56 @@
1
+ {
2
+ "counts": {
3
+ "anti-ai-tells": 26,
4
+ "components": 170,
5
+ "patterns": 19,
6
+ "rules": 47,
7
+ "tokens": 1684,
8
+ "vocabulary": 14
9
+ },
10
+ "files": [
11
+ {
12
+ "file": "components-index.json",
13
+ "note": "45 KB — name + group + tagline for all 170. FETCH THIS FIRST, then fetch only the components you chose.",
14
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json"
15
+ },
16
+ {
17
+ "file": "components/<Name>.json",
18
+ "note": "One file per component (1 KB–33 KB, median 6 KB), each carrying its importPath. This is the selective route: read the index, then fetch only what you need instead of the 1.1 MB blob.",
19
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/<Name>.json"
20
+ },
21
+ {
22
+ "file": "components.json",
23
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json"
24
+ },
25
+ {
26
+ "file": "tokens.json",
27
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json"
28
+ },
29
+ {
30
+ "file": "vocabulary.json",
31
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json"
32
+ },
33
+ {
34
+ "file": "rules.json",
35
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json"
36
+ },
37
+ {
38
+ "file": "patterns.json",
39
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns.json"
40
+ },
41
+ {
42
+ "file": "anti-ai-tells.json",
43
+ "url": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json"
44
+ }
45
+ ],
46
+ "generated": "scripts/gen-agent-catalog.mjs",
47
+ "name": "@godxjp/ui",
48
+ "note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
49
+ "read": {
50
+ "live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
51
+ "pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v28.10.0/agent/index.json"
52
+ },
53
+ "source": "mcp/src/data — the same data @godxjp/ui-mcp serves",
54
+ "start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
55
+ "version": "28.10.0"
56
+ }
package/agent/llms.txt ADDED
@@ -0,0 +1,32 @@
1
+ # @godxjp/ui
2
+
3
+ > A Japanese-enterprise React design system: 170 components, 1684 design tokens,
4
+ > 47 cardinal rules. This file is the entry point for AI agents. Catalog version 28.10.0.
5
+
6
+ If your client can run a process, do not read these files — run the MCP server instead
7
+ (`npx @godxjp/ui-mcp@28.10.0`). It is searchable and version-locked. These files exist for agents
8
+ that can only fetch URLs.
9
+
10
+ ## Start
11
+
12
+ - [START-HERE.md](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md): the whole contract — the four rules, the token override model, a page that passes review. Read this first; it is self-contained.
13
+ - [index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json): version, counts, and the URL of every file.
14
+
15
+ ## Catalog
16
+
17
+ - [patterns-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/patterns-index.json): 19 whole-task patterns (name, tagline, tags). Start here when the task is a TASK — "build a settings page" — then fetch `patterns/<name>.json` for complete code.
18
+ - [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 45 KB — all 170 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
19
+ - [components/&lt;Name&gt;.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–33 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
20
+ - [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.1 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
21
+ - [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value and the reason it exists.
22
+ - [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
23
+ - [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 47 cardinal rules.
24
+ - [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
25
+
26
+ ## Pinning
27
+
28
+ Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
29
+ the tag: `.../godx-jp/godxjp-ui/v28.10.0/agent/...`. A catalog that does not match the installed
30
+ package describes props that are absent, or hides props that are present, and says nothing either way.
31
+
32
+ Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
@@ -0,0 +1,19 @@
1
+ {
2
+ "aliases": [
3
+ "recovery-email",
4
+ "backup-codes",
5
+ "security-settings",
6
+ "recovery-settings"
7
+ ],
8
+ "code": "// Research basis: Google Account recovery (email/phone + backup codes), Microsoft Security info,\n// GitHub recovery codes. Signed-IN recovery SETTINGS (manage your methods) is a DIFFERENT surface\n// from the signed-OUT forgot-password JOURNEY — do not mix them on one page.\nimport { Card, CardContent, CardHeader, CardTitle, ListRow, Badge } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { Mail, Smartphone, KeyRound } from \"lucide-react\";\n\ntype MethodStatus = \"verified\" | \"unverified\" | \"unavailable\";\n\nconst STATUS: Record<MethodStatus, { tone: \"success\" | \"warning\" | \"neutral\"; label: string }> = {\n verified: { tone: \"success\", label: \"確認済み\" },\n unverified: { tone: \"warning\", label: \"未確認\" },\n unavailable: { tone: \"neutral\", label: \"未設定\" },\n};\n\nfunction MethodRow({ icon: Icon, name, value, status, action }: {\n icon: React.ComponentType<{ className?: string }>; name: string; value?: string;\n status: MethodStatus; action: React.ReactNode;\n}) {\n const s = STATUS[status];\n return (\n <ListRow\n leading={<Icon className=\"text-muted-foreground size-5\" />}\n title={<span className=\"font-medium\">{name}</span>}\n description={value ?? \"未設定\"}\n trailing={<Flex gap=\"sm\" align=\"center\"><Badge tone={s.tone} icon={null}>{s.label}</Badge>{action}</Flex>}\n />\n );\n}\n\n// Compact status/action rows — one row per method, capability-aware. Do NOT stack full-width\n// Info Alerts for ordinary status, and do NOT show unavailable features as permanent page content\n// beyond a single row that offers to set them up.\nexport function AccountRecoverySettings({ email, phone, backupCodesSupported, backupCodesRemaining, onChangeEmail, onChangePhone, onRegenerateCodes, onChangePassword }: {\n email?: string; phone?: string; backupCodesSupported: boolean; backupCodesRemaining: number;\n onChangeEmail: () => void; onChangePhone: () => void; onRegenerateCodes: () => void; onChangePassword: () => void;\n}) {\n return (\n <Flex direction=\"col\" gap=\"lg\">\n <Card>\n <CardHeader><CardTitle>アカウント復旧</CardTitle></CardHeader>\n <CardContent flush>\n <MethodRow icon={Mail} name=\"復旧用メール\" value={email} status={email ? \"verified\" : \"unavailable\"}\n action={<Button variant=\"outline\" size=\"sm\" onClick={onChangeEmail}>{email ? \"変更\" : \"追加\"}</Button>} />\n <MethodRow icon={Smartphone} name=\"復旧用電話番号\" value={phone} status={phone ? \"verified\" : \"unavailable\"}\n action={<Button variant=\"outline\" size=\"sm\" onClick={onChangePhone}>{phone ? \"変更\" : \"追加\"}</Button>} />\n {/* Capability-aware: only render backup codes when the account type supports them. */}\n {backupCodesSupported && (\n <MethodRow icon={KeyRound} name=\"バックアップコード\"\n value={`残り ${backupCodesRemaining} 個`} status={backupCodesRemaining > 0 ? \"verified\" : \"unverified\"}\n action={<Button variant=\"outline\" size=\"sm\" onClick={onRegenerateCodes}>再生成</Button>} />\n )}\n </CardContent>\n </Card>\n\n {/* Password change is its OWN destination (risk-appropriate confirmation lives there). */}\n <Card>\n <CardHeader><CardTitle>パスワード</CardTitle></CardHeader>\n <CardContent>\n <Flex justify=\"between\" align=\"center\">\n <span className=\"text-muted-foreground text-sm\">最終更新: 90日前</span>\n <Button variant=\"outline\" onClick={onChangePassword}>パスワードを変更</Button>\n </Flex>\n </CardContent>\n </Card>\n </Flex>\n );\n}\n// The signed-OUT recovery journey (forgot password → email link → reset) is a separate flow on the\n// auth screens (AuthShell), NOT part of these signed-in settings. Do not surface it here.",
9
+ "name": "account-recovery-settings",
10
+ "tagline": "Signed-in recovery: compact method status/action rows (email · phone · backup codes) + a separate password-change destination — capability-aware, risk-appropriate, NO stacked Info Alerts.",
11
+ "tags": [
12
+ "account",
13
+ "recovery",
14
+ "security",
15
+ "backup-codes",
16
+ "password",
17
+ "settings"
18
+ ]
19
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "aliases": [
3
+ "loading-states",
4
+ "data-state",
5
+ "query-states"
6
+ ],
7
+ "code": "import { DataState, classifyQueryError } from \"@godxjp/ui/query\";\nimport { EmptyState } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { Building2, Inbox } from \"lucide-react\";\n\n// ONE primary state renders at a time; DataState makes them mutually exclusive:\n// prerequisite → skeleton(loading) → data → empty → error (never two at once).\n//\n// enabled:false is PREREQUISITE/idle, NOT loading. TanStack reports isPending while a query is\n// disabled, but fetchStatus stays \"idle\" (no request in flight) — DataState renders the prerequisite\n// slot, so a disabled query shows an instruction, NOT an endless skeleton.\n// const query = useQuery({ queryKey: [\"members\", orgId], queryFn, enabled: Boolean(orgId) });\n//\n// A background refetch over EXISTING data keeps the content on screen (no skeleton flash) and\n// announces the busy state politely — stale/placeholder refresh is handled for you.\n//\n// Errors are classified by CAUSE, not blanket-retried:\n// auth (401) → onAuthError: renew session / sign in again (NOT a retry)\n// forbidden (403) → permission message + access path (no retry)\n// notFound (404) → contextual not-found (no retry)\n// validation (400/422)→ corrective guidance (no retry)\n// transient (408/429/5xx/network) → Retry offered automatically\n// unknown → neutral; opt into Retry via showRetry/onRetry only if it can help\nexport function MembersPanel({ query, orgId, onSignIn }: {\n query: any; orgId?: string; onSignIn: () => void;\n}) {\n return (\n <DataState\n query={query}\n prerequisite={<EmptyState icon={Building2} variant=\"section\" title=\"組織を選択してください\"\n description=\"メンバーを表示するには、上のセレクタで組織を選びます。\" />}\n skeleton={<SkeletonTable rows={8} columns={4} />}\n empty={<EmptyState icon={Inbox} variant=\"section\" title=\"メンバーがいません\"\n description=\"この組織にはまだメンバーが登録されていません。\" />}\n isEmpty={(data) => data.items.length === 0}\n onAuthError={onSignIn}\n >\n {(data) => <MemberTable items={data.items} />}\n </DataState>\n );\n}\n\n// Need a bespoke error surface? Pass errorRenderer and branch on the classified category — the\n// default detail is always a localized message (never raw token / endpoint / stack text):\n// errorRenderer={(error, retry) => {\n// const { category } = classifyQueryError(error);\n// if (category === \"auth\") return <SessionExpired onRenew={onSignIn} />;\n// if (category === \"forbidden\") return <NoAccess />;\n// if (category === \"transient\") return <Retryable onRetry={retry} />;\n// return <GenericError />;\n// }}\n// RULE: pagination/footer chrome NEVER renders outside the populated-data branch (see data-table-page).",
8
+ "name": "async-data-state",
9
+ "tagline": "The full async state machine — ONE primary state at a time: prerequisite, disabled-vs-loading, stale refresh, populated, real-empty, and cause-aware error (401/403/404/422/transient) with correct recovery.",
10
+ "tags": [
11
+ "async",
12
+ "loading",
13
+ "query",
14
+ "error",
15
+ "empty",
16
+ "prerequisite",
17
+ "retry",
18
+ "react-query"
19
+ ]
20
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "aliases": [
3
+ "password-recovery",
4
+ "forgot-password",
5
+ "reset-password",
6
+ "mfa-challenge",
7
+ "two-factor-challenge",
8
+ "2fa-challenge",
9
+ "otp-challenge",
10
+ "passkey-failure",
11
+ "scr-008"
12
+ ],
13
+ "code": "// ⚠️ There is NO <PasswordRecoveryPanel state=…/> and NO <MfaChallengePanel state=…/> in\n// @godxjp/ui. What the package owns is the MEASURE:\n// <AuthShell variant=\"canonical\" preset=\"account-recovery\"> ← 432px panel, 15px gutter at 390\n// Docs page: docs/layout/auth-recovery/\n//\n// PRESENTATION ONLY. No route, no reset semantics, no OTP verification, no recovery-code\n// consumption, no passkey authentication, no permissions. The consumer owns all logic and copy.\n\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from \"@godxjp/ui/data-display\";\nimport { FormField, Input, InputOTP, InputOTPGroup, InputOTPSlot, PasswordInput, PasswordStrength } from \"@godxjp/ui/data-entry\";\nimport { Alert, AlertDescription, AlertTitle } from \"@godxjp/ui/feedback\";\nimport { Button, Reveal } from \"@godxjp/ui/general\";\nimport { AuthFooter, AuthShell, AuthStack, Flex } from \"@godxjp/ui/layout\";\n\n// ── THE CANONICAL PANEL ANATOMY — identical for ALL SEVEN states ───────────────────────\n// Card > CardHeader(CardTitle + CardDescription) ← title/description INSIDE the surface\n// > CardContent > AuthStack\n// [notice] Alert tone=\"success|destructive\" (only when the state has one)\n// [fields] FormField(s): email · password pair · ONE 6-slot InputOTP · one Input\n// [primary] Button fullWidth (spans the panel; exactly one)\n// [fallback] Flex justify=\"between\" gap=\"sm\" wrap (two ghost actions share ONE row)\n//\n// DON'T put <AuthIdentity> above the panel here — it always renders the hosted mark, and the\n// SCR-008 hierarchy puts the heading INSIDE the bordered surface.\n// DON'T reuse <TwoFactorSetup> — that is the ENROLLMENT Dialog (QR / manual key / recovery\n// output). A sign-in challenge has no secret to reveal and no dialog to dismiss: it IS the page.\n\n// ── SIGN-IN MFA CHALLENGE (otp) ────────────────────────────────────────────────────────\n// Focus order IS DOM order: code field → verify → fallback A → fallback B. No tabindex.\n// The 6-slot row is ONE field: InputOTP owns paste/arrows/backspace/caret and is a single tab\n// stop. Six <Input>s would put six stops in the ring and break paste of a 6-digit code.\nexport function MfaChallengePage({ code, onCodeChange, error, pending, onVerify }) {\n return (\n <AuthShell\n variant=\"canonical\"\n preset=\"account-recovery\"\n brand={brand}\n footer={<AuthFooter product=\"GoDX ID\" terms={termsLink} privacy={privacyLink} locale={localePicker} />}\n >\n <Reveal>\n <Card>\n <CardHeader>\n <CardTitle level={1}>{t(\"mfa.title\")}</CardTitle>\n <CardDescription>{t(\"mfa.description\")}</CardDescription>\n </CardHeader>\n <CardContent>\n <AuthStack>\n <FormField id=\"mfa-code\" label={t(\"mfa.codeLabel\")} error={error} required>\n <InputOTP maxLength={6} pattern=\"^[0-9]+$\" value={code} onChange={onCodeChange}>\n <InputOTPGroup>\n {/* six <InputOTPSlot index={0..5} /> */}\n </InputOTPGroup>\n </InputOTP>\n </FormField>\n <Button fullWidth loading={pending} loadingText={t(\"common.verifying\")} onClick={onVerify}>\n {t(\"mfa.verify\")}\n </Button>\n <Flex justify=\"between\" gap=\"sm\" wrap>\n <Button variant=\"ghost\" size=\"sm\">{t(\"mfa.useRecoveryCode\")}</Button>\n <Button variant=\"ghost\" size=\"sm\">{t(\"mfa.retryPasskey\")}</Button>\n </Flex>\n </AuthStack>\n </CardContent>\n </Card>\n </Reveal>\n </AuthShell>\n );\n}\n\n// ── THE OTHER SIX STATES — same anatomy, only the notice/fields/copy change ─────────────\n// The STATE IS NOT A PROP: the server answers, and the route renders the matching body in the\n// SAME 432px panel. Nothing is reconstructed, because nothing was torn down.\n//\n// password recovery · request — fields: email (autoComplete=\"username\")\n// password recovery · sent — <Alert tone=\"success\"> + <Button fullWidth disabled> (cooldown)\n// password recovery · new-password — two <PasswordInput autoComplete=\"new-password\"> + <PasswordStrength>\n// password recovery · expired — <Alert tone=\"destructive\">, primary = request a new link\n// MFA · recovery-code — ONE <Input autoComplete=\"one-time-code\">\n// MFA · passkey-failure — <Alert tone=\"destructive\">, primary = retry passkey\n//\n// The masked address in `sent` (\"h*****@example.co.jp\") is produced by the SERVER — the panel\n// never reconstructs an identifier, it renders the string it is given.\n\n// ── STATES ─────────────────────────────────────────────────────────────────────────────\n// error FormField error ⇒ aria-invalid + aria-errormessage + role=\"alert\". Never colour-only.\n// loading Button loading + loadingText ⇒ aria-busy, the box does not resize, fallbacks disabled.\n// disabled a resend cooldown is <Button disabled> — keep it VISIBLE so the affordance is known.\n\n// ── RESPONSIVE CONTRACT (1440 / 1024 / 390) ────────────────────────────────────────────\n// 1440 · 1024 panel 432px, centred, 16px page gutter; OTP one row (6 × 36px = 216px)\n// 390 panel 360px at x=15 (15px inline gutter, --auth-shell-recovery-main-padding-mobile),\n// 310px content column, OTP still ONE row, primary still full-width, the fallback\n// row wraps to a stack only when the localized labels exceed the column.\n// ⚠️ The 390 canonical reference is a desktop 2×2 COMPOSITE that overflows\n// and crops horizontally. It is NOT a valid mobile source and was not traced; the row above is a\n// DECIDED contract, documented in docs/layout/auth-recovery/ and pinned by tests.\n\n// ANTI-PATTERNS: a page-local --auth-shell-card-max-width override or a .recovery-panel{width:432px}\n// class · AuthIdentity above the panel · TwoFactorSetup as a challenge · six Inputs for the OTP row ·\n// re-scoping --control-height to widen the slots (use --otp-slot-size) · two primary actions.",
14
+ "name": "auth-recovery-panels",
15
+ "tagline": "Signed-OUT password recovery + sign-in MFA challenge panels (SCR-008). There is NO PasswordRecoveryPanel and NO MfaChallengePanel — compose Card + AuthStack + FormField/InputOTP + Button inside AuthShell preset=\"account-recovery\" (the 432px token-owned measure).",
16
+ "tags": [
17
+ "auth",
18
+ "authshell",
19
+ "recovery",
20
+ "password",
21
+ "mfa",
22
+ "2fa",
23
+ "otp",
24
+ "inputotp",
25
+ "passkey",
26
+ "layout",
27
+ "presentation"
28
+ ]
29
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "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 — coloured pill, drop the misleading glyph with icon={null}:\n<Badge status=\"プレミアム\" tone=\"success\" icon={null} />\n<Badge status=\"ゴールド\" tone=\"warning\" icon={null} />\n<Badge status=\"法人共通\" tone=\"info\" 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.",
3
+ "name": "badge-coloring",
4
+ "tagline": "Colour a Badge for localized labels and tiers via tone + icon (escape-hatch props).",
5
+ "tags": [
6
+ "statusbadge",
7
+ "badge",
8
+ "tone",
9
+ "color",
10
+ "status",
11
+ "tier",
12
+ "table"
13
+ ]
14
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "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=\"x\" value=\"1\" /></ResponsiveGrid>\n// Need a section title? Use a heading, NOT a Card:\n// ✅ <Flex direction=\"col\" gap=\"sm\"><div className=\"text-sm font-medium\">KPI</div>\n// <ResponsiveGrid columns={4}><StatCard .../></ResponsiveGrid></Flex>\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=\"プレミアム\" />\n// ✅ <Badge status=\"プレミアム\" tone=\"success\" icon={null} /> // tier → pill, no icon\n// ✅ <Badge status=\"active\">公開中</Badge> // lifecycle → keep icon\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",
3
+ "name": "common-fixes",
4
+ "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.",
5
+ "tags": [
6
+ "fixes",
7
+ "migration",
8
+ "bug",
9
+ "cardstat",
10
+ "statusbadge",
11
+ "datatable",
12
+ "sidebar",
13
+ "gotcha",
14
+ "review"
15
+ ]
16
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "code": "import { useState } from \"react\";\nimport { AlertDialogRoot, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from \"@godxjp/ui/feedback\";\nimport { Input } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { toast } from \"sonner\";\n\nexport function DeleteProjectDialog({ open, onOpenChange, slug }: { open: boolean; onOpenChange: (v: boolean) => void; slug: string }) {\n const [confirm, setConfirm] = useState(\"\");\n return (\n <AlertDialogRoot open={open} onOpenChange={onOpenChange}>\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>プロジェクトを削除</AlertDialogTitle>\n <AlertDialogDescription>この操作は取り消せません。確認のためプロジェクト名 \"{slug}\" と入力してください。</AlertDialogDescription>\n </AlertDialogHeader>\n <Flex direction=\"col\" gap=\"md\">\n <Input value={confirm} onChange={(e) => setConfirm(e.target.value)} placeholder={slug} />\n </Flex>\n <AlertDialogFooter>\n <AlertDialogCancel asChild><Button variant=\"outline\">キャンセル</Button></AlertDialogCancel>\n <AlertDialogAction asChild>\n <Button variant=\"destructive\" disabled={confirm !== slug} onClick={() => { toast.success(\"削除しました\"); onOpenChange(false); }}>完全に削除</Button>\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialogRoot>\n );\n}",
3
+ "name": "confirm-destructive",
4
+ "tagline": "Type-to-confirm destructive dialog — AlertDialogRoot + Input gate + toast (real @godxjp/ui API).",
5
+ "tags": [
6
+ "dialog",
7
+ "confirm",
8
+ "destructive",
9
+ "delete"
10
+ ]
11
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "aliases": [
3
+ "filter-bar",
4
+ "table-state",
5
+ "list-page"
6
+ ],
7
+ "code": "import { useState } from \"react\";\nimport { DataState } from \"@godxjp/ui/query\";\nimport { Card, CardContent, DataTable, EmptyState, type ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\nimport { SearchInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from \"@godxjp/ui/data-entry\";\nimport { Toolbar, ToolbarGroup, Pagination } from \"@godxjp/ui/navigation\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { useQuery, keepPreviousData } from \"@tanstack/react-query\";\n\ntype Row = { id: string; name: string; status: string };\n\nexport function MembersTablePage({ orgId }: { orgId?: string }) {\n const [q, setQ] = useState(\"\");\n const [status, setStatus] = useState(\"all\");\n const [page, setPage] = useState(1);\n const pageSize = 20;\n\n // placeholderData: keepPreviousData → on page/filter change the PREVIOUS page stays visible\n // (a stale refresh) instead of flashing a skeleton. enabled gates on the prerequisite (orgId).\n const query = useQuery({\n queryKey: [\"members\", orgId, q, status, page],\n queryFn: () => fetchMembers({ orgId: orgId!, q, status, page, pageSize }),\n enabled: Boolean(orgId),\n placeholderData: keepPreviousData,\n });\n\n const columns: ColumnDef<Row>[] = [\n { key: \"name\", header: \"氏名\", render: (r) => <span className=\"font-medium\">{r.name}</span> },\n { key: \"status\", header: \"ステータス\" },\n ];\n\n return (\n <Flex direction=\"col\" gap=\"lg\">\n {/* Toolbar (filter bar) is STANDALONE and always visible so the user can change filters even\n while a query is idle/loading/empty. Changing a filter resets to page 1. */}\n <Toolbar hasActiveFilters={q !== \"\" || status !== \"all\"}\n onClear={() => { setQ(\"\"); setStatus(\"all\"); setPage(1); }}>\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=\"active\">有効</SelectItem>\n <SelectItem value=\"invited\">招待中</SelectItem>\n </SelectContent>\n </Select>\n </ToolbarGroup>\n </Toolbar>\n\n {/* The table surface owns exactly ONE card. Pagination lives INSIDE the success branch, so it\n is hidden during prerequisite / loading / empty / error and for single-page results. */}\n <DataState\n query={query}\n prerequisite={<EmptyState variant=\"section\" title=\"組織を選択してください\" />}\n skeleton={<Card><CardContent flush><SkeletonTable rows={8} columns={2} /></CardContent></Card>}\n empty={<EmptyState variant=\"section\" title=\"該当するメンバーがいません\" description=\"検索条件を変更してください。\" />}\n isEmpty={(d) => d.items.length === 0}\n >\n {(d) => {\n const totalPages = Math.ceil(d.total / pageSize);\n return (\n <>\n <Card><CardContent flush><DataTable data={d.items} columns={columns} getRowId={(r) => r.id} /></CardContent></Card>\n {totalPages > 1 && (\n <Pagination value={page} total={d.total} pageSize={pageSize} showTotal onValueChange={setPage} />\n )}\n </>\n );\n }}\n </DataState>\n </Flex>\n );\n}\n// Responsive: Pagination's showTotal range + controls stack at narrow widths automatically.\n// A transient retry keeps q/status/page (they live in component state, not the query) — the\n// same page reloads in place; the user never loses their filters.",
8
+ "name": "data-table-page",
9
+ "tagline": "Filter (Toolbar) + table surface + bottom pagination driven by a query state machine — pagination shows ONLY for successful multi-page data; filters/page survive a transient retry.",
10
+ "tags": [
11
+ "table",
12
+ "pagination",
13
+ "filter",
14
+ "async",
15
+ "toolbar",
16
+ "datatable"
17
+ ]
18
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "code": "// Server (Laravel): defer the heavy prop\n// Inertia::render('crm/coupons/index', [\n// 'coupons' => Inertia::defer(fn () => Coupon::all()),\n// ]);\nimport { Card, CardContent, DataTable } from \"@godxjp/ui/data-display\";\nimport type { ColumnDef } from \"@godxjp/ui/data-display\";\nimport { SkeletonTable } from \"@godxjp/ui/feedback\";\n\ntype Coupon = { id: string; name: string };\nconst columns: ColumnDef<Coupon>[] = [{ key: \"name\", header: \"クーポン名\" }];\n\n// coupons is undefined until the deferred request resolves\nexport default function Coupons({ coupons }: { coupons?: Coupon[] }) {\n return (\n <Card>\n <CardContent flush>\n {!coupons\n ? <SkeletonTable rows={10} columns={6} />\n : <DataTable data={coupons} columns={columns} getRowId={(c) => c.id} />}\n </CardContent>\n </Card>\n );\n}",
3
+ "name": "deferred-loading",
4
+ "tagline": "Inertia deferred props with a Skeleton fallback — SkeletonTable while data loads, then DataTable (real @godxjp/ui API).",
5
+ "tags": [
6
+ "loading",
7
+ "skeleton",
8
+ "deferred",
9
+ "inertia",
10
+ "table"
11
+ ]
12
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "aliases": [
3
+ "error-surface",
4
+ "errorsurface",
5
+ "403",
6
+ "404",
7
+ "500",
8
+ "503",
9
+ "exception-page"
10
+ ],
11
+ "code": "// @godxjp/ui SHIPS <ErrorSurface>. Do NOT hand-compose an error page from\n// AuthShell + a generic Card + a local \".canonical-auth-card\" — that workaround IS the regression.\n//\n// 403 / 404 → mode=\"application\": the surface is the BODY of the AppShell you ALREADY render\n// (sidebar + topbar + breadcrumb are PRESERVED, never reconstructed)\n// 500 / 503 → mode=\"system\": the surface OWNS the page (CenteredShell align=\"center\",\n// package-owned geometry at 1440 / 1024 / 390 — no min-h-dvh, no media query)\n// Docs page: docs/layout/error-surface/\n\nimport { Button, Logo, Text } from \"@godxjp/ui/general\";\nimport { AppShell, ErrorSurface, PageContainer, Sidebar } from \"@godxjp/ui/layout\";\n\n// ── WHAT THE COMPONENT OWNS ───────────────────────────────────────────────────────────\n// • status → icon + tone (ShieldAlert/warning · SearchX/muted · ServerCrash/destructive ·\n// Wrench/warning). You pass the NUMBER; you never write the lookup.\n// • EXACTLY ONE recovery action — a single slot; a 2nd element is dropped with a dev error.\n// • semantic metadata slots as real <dt>/<dd> pairs, with LIBRARY-owned localized labels:\n// requestId · permission · organization · maintenance{start,end,timeZone,progress}\n// • heading order (h2 in application mode under the PageContainer h1, h1 in system mode)\n// • the status code announced as \"HTTP status 403\", not the cardinal number \"403\"\n// PRODUCT COPY stays yours: title / description / action come from the APP's own t().\n\n// ── 403 / 404 — APPLICATION mode: PRESERVE the authenticated shell ─────────────────────\nexport function ForbiddenPage() {\n return (\n <AppShell sidebar={<Sidebar activeId=\"reports\" sections={sections} />}>\n <PageContainer title={t(\"reports.title\")} breadcrumb={[{ label: t(\"nav.home\"), to: \"/\" }, { label: t(\"reports.title\") }]}>\n <ErrorSurface\n mode=\"application\"\n status={403}\n title={t(\"errors.403.title\")}\n description={t(\"errors.403.description\")}\n permission=\"reports.view\" {/* WHY: the missing role … */}\n organization=\"Acme KK\" {/* … vs the wrong workspace */}\n action={<Button onClick={goHome}>{t(\"errors.backHome\")}</Button>}\n />\n </PageContainer>\n </AppShell>\n );\n}\n\n// ── 500 / 503 — SYSTEM mode: package-owned viewport-centred geometry ───────────────────\n// The component renders CenteredShell align=\"center\" itself → the column centres in the 100dvh\n// shell at 1440 / 1024 / 390. NO className, NO min-h-dvh, NO media query. Tall copy still scrolls\n// from the top (auto offsets collapse), so a long JA/VI message is never clipped.\nexport function MaintenancePage() {\n return (\n <ErrorSurface\n mode=\"system\"\n status={503}\n brand={<Logo glyph=\"G\" />}\n title={t(\"errors.503.title\")}\n description={t(\"errors.503.description\")}\n maintenance={{\n start: \"2026-08-02T18:00:00Z\", // ISO-8601 instants + an IANA zone → Intl.formatRange\n end: \"2026-08-02T20:00:00Z\",\n timeZone: \"Asia/Tokyo\",\n progress: 40, // 0-100, SERVER-SENT (a client clock breaks hydration)\n }}\n footer={<Text size=\"xs\" tone=\"muted\">2026 GodX</Text>}\n action={<Button onClick={reload}>{t(\"errors.reload\")}</Button>}\n />\n );\n}\n\n// ── INERTIA / SSR — one Error page component, one expression ───────────────────────────\n// Laravel: Handler::render() → Inertia::render('Error', ['status' => $response->getStatusCode()])\n//\n// export default function Error({ status, requestId }) {\n// const isSystem = status >= 500\n// return (\n// <ErrorSurface\n// mode={isSystem ? \"system\" : \"application\"}\n// status={status}\n// requestId={requestId}\n// title={t(`errors.${status}.title`)}\n// description={t(`errors.${status}.description`)}\n// action={<Button asChild><Link href=\"/\">{t(\"errors.backHome\")}</Link></Button>}\n// />\n// )\n// }\n// // persistent layout ⇒ 403/404 keep the authenticated shell; 500/503 own the page\n// Error.layout = (page) => (page.props.status < 500 ? <CrmLayout>{page}</CrmLayout> : page)\n//\n// SSR rules:\n// • ErrorSurface has NO client state / effects / portals → it renders fully server-side. An\n// exception page must be readable without hydration.\n// • Pass the maintenance window as a SERVER-SENT ISO-8601 string + an explicit IANA timeZone.\n// An argless new Date() or the server's local zone ⇒ hydration mismatch.\n// • A 500 page must NOT import the app's query client / data providers — the failure may be\n// inside them. ErrorSurface needs only the AppProvider that supplies the locale.\n//\n// THEMING: retune with tokens, never a className —\n// --error-surface-max-width | -gap | -padding-block(-compact) | -meta-gap | -meta-row-gap |\n// --error-surface-meta-border (default 'none', rule #44) | --error-surface-progress-max-width\n//\n// ANTI-PATTERNS: AuthShell + a generic Card (the workaround; AuthShell is the\n// UNAUTHENTICATED root with auth-card geometry) · rebuilding nav on the 403 page · two CTAs ·\n// className=\"min-h-dvh flex …\" · a hand-built \"18:00 - 20:00 JST\" string · writing the request id\n// into 'description' as prose instead of using the semantic slot.",
12
+ "name": "error-pages",
13
+ "tagline": "400 / 403 / 404 / 500 / 503 exception pages. USE THE COMPONENT: import { ErrorSurface } from \"@godxjp/ui/layout\". `mode=\"application\"` (400/403/404) is the body you put inside the AppShell the route already renders; `mode=\"system\"` (500/503) owns the page via CenteredShell align=\"center\".",
14
+ "tags": [
15
+ "error",
16
+ "errorsurface",
17
+ "403",
18
+ "404",
19
+ "500",
20
+ "503",
21
+ "maintenance",
22
+ "inertia",
23
+ "ssr",
24
+ "layout",
25
+ "emptystate",
26
+ "centeredshell"
27
+ ]
28
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "code": "import { Head, router } from \"@inertiajs/react\"\nimport { PageContainer, ResponsiveGrid, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, StatCard, 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=\"会員詳細\" subtitle=\"会員が見つかりません\">\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} subtitle={`${member.id} / ${member.rank}`}>\n <Flex direction=\"col\" gap=\"lg\">\n <ResponsiveGrid columns={4}>\n <StatCard label=\"累計購入額\" value={`¥${member.total.toLocaleString()}`} />\n <StatCard label=\"来店回数\" value={member.visits} />\n <StatCard label=\"ポイント\" value={member.points.toLocaleString()} />\n <StatCard label=\"LTV\" value={`¥${member.ltv.toLocaleString()}`} />\n </ResponsiveGrid>\n <Card>\n <CardContent>\n {/* Descriptions is COMPOUND — value goes in children, not a prop */}\n <Descriptions columns={2}>\n <Descriptions.Item label=\"氏名\">{member.name}</Descriptions.Item>\n <Descriptions.Item label=\"ランク\"><Badge status={member.rank} tone=\"info\" icon={null} /></Descriptions.Item>\n <Descriptions.Item label=\"ステータス\"><Badge status={member.status} /></Descriptions.Item>\n <Descriptions.Item label=\"登録日\">{formatDate(member.registeredAt)}</Descriptions.Item>\n </Descriptions>\n </CardContent>\n </Card>\n </Flex>\n </PageContainer>\n </>\n )\n}\n\nMemberShow.layout = withCrmLayout\nexport default MemberShow",
3
+ "name": "inertia-detail-page",
4
+ "tagline": "Inertia detail page — receives {id} prop, Descriptions (compound) + StatCard + EmptyState fallback.",
5
+ "tags": [
6
+ "inertia",
7
+ "detail",
8
+ "show",
9
+ "page",
10
+ "keyvaluegrid",
11
+ "crm"
12
+ ]
13
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "code": "import { Head, router } from \"@inertiajs/react\"\nimport { useMemo, useState } from \"react\"\nimport { PageContainer, ResponsiveGrid, Flex } from \"@godxjp/ui/layout\"\nimport { Card, CardContent, StatCard, 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: \"スコープ\", render: (c) => <Badge status={c.scope} tone=\"info\" icon={null} /> },\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-*/gap-* */}\n <PageContainer title=\"クーポン管理\" subtitle=\"配信中のクーポン一覧\">\n <Flex direction=\"col\" gap=\"lg\">\n <ResponsiveGrid columns={3}>\n <StatCard label=\"公開中\" value={coupons.filter((c) => c.status === \"公開中\").length} />\n <StatCard label=\"総利用数\" value={coupons.reduce((s, c) => s + c.usage, 0).toLocaleString()} />\n <StatCard label=\"件数\" value={coupons.length} />\n </ResponsiveGrid>\n\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=\"検索条件を変更してください。\" />\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",
3
+ "name": "inertia-list-page",
4
+ "tagline": "Inertia + @godxjp/ui list page — PageContainer + Toolbar + DataTable + Badge + Pagination (current primitive API).",
5
+ "tags": [
6
+ "inertia",
7
+ "list",
8
+ "table",
9
+ "page",
10
+ "filter",
11
+ "pagination",
12
+ "datatable",
13
+ "crm"
14
+ ]
15
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "code": "// resources/js/layouts/crm-layout.tsx\nimport { router, usePage } from \"@inertiajs/react\"\nimport { AppShell, Sidebar } from \"@godxjp/ui/layout\"\nimport { LayoutDashboard } from \"lucide-react\"\nimport type { ReactNode } from \"react\"\n\nexport function CrmLayout({ children }: { children: ReactNode }) {\n const { url } = usePage()\n const sections = [{ label: \"メイン\", items: [{ id: \"/dashboard\", label: \"ダッシュボード\", icon: LayoutDashboard }] }]\n return (\n <AppShell sidebar={<Sidebar activeId={url} onSelect={(id) => router.visit(id)} sections={sections} product={{ name: \"JOVY CRM\" }} />}>\n {children}\n </AppShell>\n )\n}\n\n// ⚠️ GOTCHA 1 — persistent layout MUST be the ARRAY form.\n// A render fn `(page) => <CrmLayout>{page}</CrmLayout>` is indistinguishable from a\n// component; Inertia React calls it with the page-PROPS object and renders that\n// object as a child → \"Objects are not valid as a React child {errors, auth, …}\".\nexport const withCrmLayout = [CrmLayout] // ✅ array → Inertia passes the page as children\n// page usage: Dashboard.layout = withCrmLayout\n\n// ⚠️ GOTCHA 2 — Inertia v3 SSRs even in `npm run dev`. NEVER call Math.random() or\n// argless new Date() during render (e.g. fabricating chart/demo numbers) → React\n// hydration mismatch (\"server rendered text didn't match the client\"). Seed\n// deterministically by index, or compute inside an event handler:\nconst seeded = (n: number) => { const x = Math.sin((n + 1) * 99.71) * 1e4; return x - Math.floor(x) }",
3
+ "name": "inertia-persistent-layout",
4
+ "tagline": "Inertia persistent layout (AppShell+Sidebar) — the array-form gotcha + the SSR/Math.random gotcha.",
5
+ "tags": [
6
+ "inertia",
7
+ "layout",
8
+ "appshell",
9
+ "sidebar",
10
+ "ssr",
11
+ "hydration",
12
+ "gotcha"
13
+ ]
14
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "aliases": [
3
+ "organization-switcher",
4
+ "workspace-switcher",
5
+ "received-invitations",
6
+ "memberships"
7
+ ],
8
+ "code": "// Research basis: GitHub organization membership + Slack workspace switching/invitations.\n// Each row = recognizable identity (Avatar/logo, NOT an identical decorative icon) + name +\n// a \"現在\" (Current) badge for the active org + role + a permission-aware action menu.\nimport { Card, CardContent, CardHeader, CardTitle, ListRow, Badge, Avatar, AvatarImage, AvatarFallback, EmptyState } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from \"@godxjp/ui/navigation\";\nimport { MoreHorizontal, Plus } from \"lucide-react\";\n\ntype Role = \"owner\" | \"admin\" | \"member\";\ntype Membership = { id: string; name: string; logoUrl?: string; role: Role; joinedAt: string; isCurrent: boolean; canManage: boolean };\ntype Invite = { id: string; orgName: string; invitedBy: string };\n\nconst ROLE_LABEL: Record<Role, string> = { owner: \"オーナー\", admin: \"管理者\", member: \"メンバー\" };\n\nexport function OrganizationMemberships({\n memberships, invitations, onOpen, onSwitch, onManage, onLeave, onCreate, onAccept, onDecline,\n}: {\n memberships: Membership[]; invitations: Invite[];\n onOpen: (id: string) => void; onSwitch: (id: string) => void; onManage: (id: string) => void;\n onLeave: (id: string) => void; onCreate: () => void; onAccept: (id: string) => void; onDecline: (id: string) => void;\n}) {\n return (\n <Flex direction=\"col\" gap=\"lg\">\n <Card>\n <CardHeader>\n <Flex justify=\"between\" align=\"center\">\n <CardTitle>組織</CardTitle>\n {/* Create belongs at page/section level, not per row. */}\n <Button variant=\"outline\" size=\"sm\" onClick={onCreate}><Plus className=\"size-4\" />組織を作成</Button>\n </Flex>\n </CardHeader>\n {/* flush → ListRows draw their own edge-to-edge dividers. */}\n <CardContent flush>\n {memberships.length === 0 ? (\n <EmptyState variant=\"section\" title=\"所属している組織がありません\" description=\"新しい組織を作成するか、招待を受け取ってください。\" />\n ) : (\n memberships.map((m) => (\n <ListRow\n key={m.id}\n leading={\n <Avatar>\n {m.logoUrl ? <AvatarImage src={m.logoUrl} alt=\"\" /> : null}\n <AvatarFallback>{m.name.slice(0, 1)}</AvatarFallback>\n </Avatar>\n }\n title={\n <Flex gap=\"sm\" align=\"center\">\n <span className=\"font-medium\">{m.name}</span>\n {m.isCurrent && <Badge tone=\"success\" icon={null}>現在</Badge>}\n </Flex>\n }\n description={ROLE_LABEL[m.role]}\n trailing={\n <Flex gap=\"sm\" align=\"center\">\n {m.isCurrent\n ? <Button size=\"sm\" onClick={() => onOpen(m.id)}>開く</Button>\n : <Button variant=\"outline\" size=\"sm\" onClick={() => onSwitch(m.id)}>切り替え</Button>}\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" aria-label={`${m.name} の操作`}><MoreHorizontal className=\"size-4\" /></Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem onSelect={() => onOpen(m.id)}>開く</DropdownMenuItem>\n {m.canManage && <DropdownMenuItem onSelect={() => onManage(m.id)}>管理</DropdownMenuItem>}\n {m.role !== \"owner\" && <DropdownMenuItem onSelect={() => onLeave(m.id)}>退出</DropdownMenuItem>}\n </DropdownMenuContent>\n </DropdownMenu>\n </Flex>\n }\n />\n ))\n )}\n </CardContent>\n </Card>\n\n {/* CONDITIONAL invitations — no pending invites ⇒ render NOTHING (no permanent empty Card).\n Few pending ⇒ a compact actionable list. Many/history ⇒ link to a focused route instead. */}\n {invitations.length > 0 && (\n <Card>\n <CardHeader><CardTitle>保留中の招待</CardTitle></CardHeader>\n <CardContent flush>\n {invitations.map((inv) => (\n <ListRow\n key={inv.id}\n title={<span className=\"font-medium\">{inv.orgName}</span>}\n description={`${inv.invitedBy} からの招待`}\n trailing={\n <Flex gap=\"sm\">\n <Button size=\"sm\" onClick={() => onAccept(inv.id)}>参加</Button>\n <Button variant=\"ghost\" size=\"sm\" onClick={() => onDecline(inv.id)}>辞退</Button>\n </Flex>\n }\n />\n ))}\n </CardContent>\n </Card>\n )}\n </Flex>\n );\n}\n// DON'T: passive rows with no action · identical decorative icons · unlabeled timestamps\n// (label them: \"2024年に参加\") · a large empty invitation Card shown permanently.",
9
+ "name": "organization-memberships",
10
+ "tagline": "Workspace membership list — recognizable identity, current/active state, role, permission-aware Open/Switch/Manage/Leave row actions, page-level Create, and CONDITIONAL invitations (omit when none).",
11
+ "tags": [
12
+ "organization",
13
+ "workspace",
14
+ "membership",
15
+ "invitation",
16
+ "account",
17
+ "switcher"
18
+ ]
19
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "aliases": [
3
+ "cards-in-page",
4
+ "page-layout",
5
+ "stack-cards"
6
+ ],
7
+ "code": "import { PageContainer, Flex, ResponsiveGrid } from \"@godxjp/ui/layout\";\nimport { Card, CardHeader, CardTitle, CardContent } from \"@godxjp/ui/data-display\";\nimport { Button } from \"@godxjp/ui/general\";\n\n// Every direct child of PageContainer is a page section — the page owns the gap between them\n// (--page-body-gap, the section step). Do NOT add gap-* / space-y-* / mt-* yourself.\nexport function ReportPage() {\n return (\n <PageContainer title=\"Bug report\" subtitle=\"https://app.example.com/orders\" extra={<Button>Send</Button>}>\n <Card>\n <CardHeader><CardTitle>Details</CardTitle></CardHeader>\n <CardContent>…</CardContent>\n </Card>\n <Card>\n <CardHeader><CardTitle>Screenshots</CardTitle></CardHeader>\n <CardContent>\n {/* items INSIDE a section: ResponsiveGrid for a grid, Flex for a stack/row */}\n <ResponsiveGrid columns={{ sm: 1, md: 2, lg: 3 }}>…</ResponsiveGrid>\n </CardContent>\n </Card>\n <Card>\n <CardContent>\n <Flex direction=\"col\" gap=\"md\">…</Flex>\n </CardContent>\n </Card>\n </PageContainer>\n );\n}\n",
8
+ "name": "page-sections",
9
+ "tagline": "A page made of several Card sections: PageContainer spaces its direct children by default; group items inside a section with Flex/ResponsiveGrid, never Tailwind gap-*/p-*.",
10
+ "tags": [
11
+ "layout",
12
+ "page",
13
+ "card",
14
+ "spacing",
15
+ "flex",
16
+ "grid"
17
+ ]
18
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "aliases": [
3
+ "settings-tabs",
4
+ "settings-page",
5
+ "settings-layout"
6
+ ],
7
+ "code": "// Research basis: GitHub, Google Account, Microsoft Account, Atlassian Account settings +\n// the IBM Carbon form grid. Every destination is a URL, so deep links and browser back/forward\n// work for free and the active item is derived from the route — never from local tab state.\n//\n// Desktop (>= lg): persistent vertical local nav on the left + bounded content on the right.\n// Mobile (< lg): the SAME routes become a horizontal, scrollable tab strip above the content.\n// Styling uses ONLY real semantic token classes (bg-muted / text-foreground / text-muted-foreground)\n// and responsive/visibility utilities — the library ships NO bespoke settings shell/nav CSS classes.\nimport { NavLink, Outlet, Navigate } from \"react-router-dom\";\nimport { Flex } from \"@godxjp/ui/layout\";\nimport { FormField, Input } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\n\nconst SECTIONS = [\n { to: \"general\", label: \"基本情報\" },\n { to: \"security\", label: \"セキュリティ\" },\n { to: \"notifications\", label: \"通知\" },\n] as const;\n\n// One link renderer for BOTH breakpoints. NavLink gives us the active state (and sets\n// aria-current=\"page\") straight from the URL; semantic tokens only, no invented classes.\nconst link = ({ isActive }: { isActive: boolean }) =>\n [\n \"rounded-md px-3 py-2 text-sm no-underline transition-colors whitespace-nowrap\",\n isActive\n ? \"bg-muted text-foreground font-medium\"\n : \"text-muted-foreground hover:text-foreground\",\n ].join(\" \");\n\nexport function SettingsLayout() {\n return (\n <Flex direction=\"col\" gap=\"lg\" className=\"lg:flex-row lg:items-start\">\n {/* < lg: horizontal, scrollable route tabs. Flex owns the row and the gap — hideFrom /\n hideBelow own the breakpoint, so neither nav needs a hand-rolled flex or gap utility. */}\n <Flex as=\"nav\" aria-label=\"設定\" gap={1} hideFrom=\"lg\" className=\"overflow-x-auto\">\n {SECTIONS.map((s) => (\n <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>\n ))}\n </Flex>\n {/* >= lg: persistent vertical local nav (bounded width, does not shrink). */}\n <Flex as=\"nav\" aria-label=\"設定\" direction=\"col\" gap={1} hideBelow=\"lg\" width={224} shrink={false}>\n {SECTIONS.map((s) => (\n <NavLink key={s.to} to={s.to} className={link} end>{s.label}</NavLink>\n ))}\n </Flex>\n {/* Bounded content region (~42rem) — the routed section renders here. min-w-0 lets it shrink. */}\n <main className=\"min-w-0 max-w-2xl flex-1\"><Outlet /></main>\n </Flex>\n );\n}\n\n// A settings section. Horizontal label/control rows on desktop (collapse to stacked on mobile),\n// controls bounded to a semantic width. FormField OWNS the label↔control relationship — do NOT\n// hand-roll <Label> + <input>. Save feedback = Button loading prop + a toast in onSuccess.\nexport function GeneralSettingsSection({\n defaults, onSave, saving, error,\n}: { defaults: { name: string; email: string }; onSave: (v: FormData) => void; saving: boolean; error?: string }) {\n return (\n <form onSubmit={(e) => { e.preventDefault(); onSave(new FormData(e.currentTarget)); }}>\n <Flex direction=\"col\" gap=\"md\">\n <FormField id=\"ws-name\" label=\"ワークスペース名\" layout=\"horizontal\" controlWidth=\"24rem\"\n helper=\"請求書や共有リンクに表示されます。\" error={error}>\n <Input id=\"ws-name\" name=\"name\" defaultValue={defaults.name} />\n </FormField>\n <FormField id=\"ws-email\" label=\"連絡先メール\" layout=\"horizontal\" controlWidth=\"24rem\">\n <Input id=\"ws-email\" name=\"email\" type=\"email\" defaultValue={defaults.email} />\n </FormField>\n <Flex gap=\"sm\"><Button type=\"submit\" loading={saving}>変更を保存</Button></Flex>\n </Flex>\n </form>\n );\n}\n\n// Router wiring — each destination is addressable, so deep links + back/forward just work:\n// <Route path=\"settings\" element={<SettingsLayout/>}>\n// <Route index element={<Navigate to=\"general\" replace/>} />\n// <Route path=\"general\" element={<GeneralSettingsSection .../>} />\n// <Route path=\"security\" element={<SecuritySection/>} />\n// <Route path=\"notifications\" element={<NotificationsSection/>} />\n// </Route>\n\n// ── DO / DON'T ────────────────────────────────────────────────────────────────────────────\n// ✅ Tabs (navigation) are fine for a SMALL number (2–4) of PEER views inside ONE task/section\n// — e.g. \"プロフィール\" ⇄ \"環境設定\" within a single account page — where losing the URL is OK.\n// ⛔ Tabs as the PRIMARY IA for broad settings (Account / Security / Billing / Members / …):\n// use route-backed local navigation so every area is deep-linkable and back/forward works.\n// ⛔ Do NOT wrap the whole settings page in a Card, and do NOT make desktop a horizontal tab bar\n// — persistent local nav scales to many sections; a tab strip does not.\n// ⛔ Do NOT invent CSS class names for the shell/nav/content — compose Flex + semantic tokens.",
8
+ "name": "settings-page-responsive",
9
+ "tagline": "Route-backed settings: persistent desktop local navigation, compact mobile tab strip, deep links + browser history, and bounded form content — NO Card wrapper, NO undefined CSS.",
10
+ "tags": [
11
+ "settings",
12
+ "form",
13
+ "tabs",
14
+ "admin",
15
+ "responsive",
16
+ "navigation"
17
+ ]
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "aliases": [
3
+ "settings-section",
4
+ "settings-row",
5
+ "danger-zone",
6
+ "billing-handoff",
7
+ "preferences-rows",
8
+ "account-identity"
9
+ ],
10
+ "code": "// Gate 0 (docs/COMPOSITION-VS-COMPONENT.md): a \"SettingsSection\"/\"SettingsRow\"/\n// \"DangerZone\" FAILS the Framework-Component Test — it owns no behavior (C2), is fully\n// expressible from existing primitives + tokens (C3), and its API would be screen-shaped (C4).\n// So compose it. Live reference screen: the \"settings account sections\" page in the preview\n// showcase gallery (docs/showcase/, served at /showcase/<id>).\nimport { ExternalLink, ShieldAlert } from \"lucide-react\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport {\n Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,\n Descriptions, ListRow, StatusBadge,\n} 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// The sections stack in ONE bounded column. The gap between sibling Cards belongs to that stack,\n// never to the cards themselves — four bare <Card>s in a row is what the audit means by\n// sibling-cards-need-flex, and it is the same rule the DO list at the bottom of this file states.\n<Flex direction=\"col\" gap=\"lg\" className=\"max-w-3xl\">\n\n// 1) IDENTITY — metadata is a description list, so it is Descriptions (a real <dl>), never a\n// two-column flex of styled divs. One header action goes in CardAction.\n<Card>\n <CardHeader>\n <CardTitle level={2}>アカウント情報</CardTitle>\n <CardDescription>請求書・監査ログに表示される氏名とメールアドレスです。</CardDescription>\n <CardAction><Button variant=\"outline\" size=\"sm\">編集</Button></CardAction>\n </CardHeader>\n <CardContent>\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 {/* ISO-8601 in the machine attribute, Intl for the human text. */}\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 </CardContent>\n</Card>\n\n// 2) PREFERENCE ROWS — THE setting row pattern: CardContent flush + ListRow, whose `trailing`\n// slot IS the control. align=\"start\" + overflow=\"wrap\" keep long JA/VI titles readable and let\n// the control drop to its own line at 390px (--list-row-body-min-width).\n// ⛔ NEVER: <div className=\"flex items-center justify-between border-b py-3\">…</div>\n<Card>\n <CardHeader><CardTitle level={2}>通知の環境設定</CardTitle></CardHeader>\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\n// 3) BILLING HANDOFF — the subscription state is a CANONICAL StatusBadge; the tone + icon come\n// from the library's shared domain-status map, so a page NEVER keeps its own status→colour\n// table. Money/seats/dates go through Intl (ISO 4217 minor units come from the formatter).\n// ⛔ NEVER: const STATUS_COLORS = { past_due: \"red\", trialing: \"blue\" }\n<Card>\n <CardHeader>\n <CardTitle level={2}>請求とサブスクリプション</CardTitle>\n <CardDescription>支払い方法と請求書は外部の請求ポータルで管理します。</CardDescription>\n <CardAction><StatusBadge status=\"past_due\" variant=\"outline\" /></CardAction>\n </CardHeader>\n <CardContent>\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 </CardContent>\n <CardFooter separated>\n <Button variant=\"outline\" asChild>\n <a href={portalUrl} target=\"_blank\" rel=\"noreferrer\">\n <ExternalLink aria-hidden=\"true\" />\n 請求ポータルを開く\n {/* Never signal \"leaves the app\" with the icon alone (WCAG 1.4.1 / 2.4.4). */}\n <span className=\"sr-only\">(新しいタブで開きます)</span>\n </a>\n </Button>\n </CardFooter>\n</Card>\n\n// 4) DANGER ZONE — Card accent=\"destructive\" paints the semantic leading rail from --destructive\n// through the existing --card-accent-rail-width token. NO new component, NO new token, and no\n// baked red. Colour never carries the meaning alone: the heading says \"irreversible\", each row\n// says what is lost, and delete is gated behind type-to-confirm friction.\n<Card accent=\"destructive\">\n <CardHeader>\n <CardTitle level={2} className=\"text-destructive\">取り消せない操作</CardTitle>\n <CardDescription>以下の操作は元に戻せません。</CardDescription>\n </CardHeader>\n <CardContent flush>\n <ListRow\n align=\"start\"\n overflow=\"wrap\"\n leading={<ShieldAlert className=\"text-destructive size-4\" aria-hidden=\"true\" />}\n title=\"ワークスペースと全データを完全に削除する\"\n description=\"勤怠記録・申請履歴・監査ログがすべて削除されます。\"\n trailing={\n <Button variant=\"destructive\" size=\"sm\" onClick={() => setConfirmOpen(true)}>削除</Button>\n }\n />\n </CardContent>\n</Card>\n\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// ✅ Sections stack in a bounded column: <Flex direction=\"col\" gap=\"lg\" className=\"max-w-3xl\">.\n// ✅ Every setting row is a ListRow; its control lives in `trailing`, and the visible row title is\n// a <label htmlFor> pointing at that control so the switch/select is named by what you can read.\n// ⛔ Do NOT add SettingsSection / SettingsRow / DangerZone to src/components — they fail Gate 0.\n// ⛔ Do NOT wrap the WHOLE settings page in one Card (route-backed local nav owns the IA — see the\n// responsive settings page pattern); each block is its own Card.\n// ⛔ Do NOT hand-roll padding (p-4) on a Card, a status colour map, or a <dl> replacement.",
11
+ "name": "settings-section-rows",
12
+ "tagline": "The four canonical settings BLOCKS — identity · preference rows · billing handoff · danger zone — composed from Card + ListRow + Descriptions + StatusBadge + AlertDialog. A COMPOSITION pattern: godx-ui ships no SettingsSection/SettingsRow/DangerZone component and never will.",
13
+ "tags": [
14
+ "settings",
15
+ "account",
16
+ "billing",
17
+ "danger",
18
+ "preferences",
19
+ "listrow",
20
+ "card",
21
+ "admin"
22
+ ]
23
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "code": "import { useForm } from \"react-hook-form\";\nimport { zodResolver } from \"@hookform/resolvers/zod\";\nimport { z } from \"zod\";\nimport { Card, CardHeader, CardTitle, CardContent, CardFooter } from \"@godxjp/ui/data-display\";\nimport { FormField, Input } from \"@godxjp/ui/data-entry\";\nimport { Button } from \"@godxjp/ui/general\";\nimport { Flex } from \"@godxjp/ui/layout\";\n\nconst schema = z.object({\n name: z.string().min(1, \"氏名は必須です\"),\n email: z.string().email(\"有効なメールアドレスを入力してください\"),\n});\ntype Values = z.infer<typeof schema>;\n\nexport function SignUpCard() {\n const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<Values>({ resolver: zodResolver(schema) });\n const onSubmit = handleSubmit(async (v) => {\n await fetch(\"/api/signup\", { method: \"POST\", body: JSON.stringify(v) });\n });\n return (\n <Card>\n <CardHeader><CardTitle>アカウント作成</CardTitle></CardHeader>\n <CardContent>\n <form id=\"signup\" onSubmit={onSubmit}>\n {/* Vertical rhythm comes from Flex direction=\"col\" gap — never space-y-*/gap-* utilities. */}\n <Flex direction=\"col\" gap=\"md\">\n <FormField id=\"name\" label=\"氏名\" required error={errors.name?.message}>\n <Input id=\"name\" {...register(\"name\")} />\n </FormField>\n <FormField id=\"email\" label=\"メールアドレス\" required error={errors.email?.message}>\n <Input id=\"email\" type=\"email\" {...register(\"email\")} />\n </FormField>\n </Flex>\n </form>\n </CardContent>\n <CardFooter separated>\n <Button type=\"submit\" form=\"signup\" loading={isSubmitting}>アカウントを作成</Button>\n </CardFooter>\n </Card>\n );\n}",
3
+ "name": "signup-form",
4
+ "tagline": "Card-wrapped sign-up form using react-hook-form + zod with FormField/Input and a CardFooter action bar (real @godxjp/ui API).",
5
+ "tags": [
6
+ "form",
7
+ "auth",
8
+ "sign-up",
9
+ "zod",
10
+ "validation",
11
+ "react-hook-form"
12
+ ]
13
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "aliases": [
3
+ "user-chip",
4
+ "account-menu",
5
+ "signed-in-user"
6
+ ],
7
+ "code": "import { Flex } from \"@godxjp/ui/layout\";\nimport { Avatar, AvatarImage, AvatarFallback } from \"@godxjp/ui/data-display\";\nimport { Button, Text } from \"@godxjp/ui/general\";\nimport { LogOut } from \"lucide-react\";\n\n// ❌ <div className=\"inline-flex items-center gap-1.5 rounded-full border py-0.5\">…</div>\n// → 38px tall next to 32px controls, and the audit flags every class on it.\n// ✅ a Flex row of real primitives: Avatar (control-sized), Text, an icon Button.\nexport function AccountChip({ user, onSignOut }: { user: { name: string; email: string; avatarUrl?: string }; onSignOut: () => void }) {\n return (\n <Flex gap=\"xs\" align=\"center\" title={user.email}>\n <Avatar>\n {user.avatarUrl && <AvatarImage src={user.avatarUrl} alt=\"\" />}\n <AvatarFallback>{user.name.slice(0, 1)}</AvatarFallback>\n </Avatar>\n <Text>{user.name}</Text>\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Sign out\" onClick={onSignOut}><LogOut /></Button>\n </Flex>\n );\n}\n\n// In the page header, a language Select takes width=\"auto\" so it does not swallow the row:\n// <PageContainer title=\"…\" extra={<><Select …><SelectTrigger width=\"auto\">…</SelectTrigger>…</Select><AccountChip … /><Button>Send</Button></>}>\n",
8
+ "name": "topbar-account-chip",
9
+ "tagline": "Signed-in user in a PageContainer `extra` slot: Avatar + name + sign-out as a Flex row of primitives — same control height as the buttons beside it. Never a hand-rolled rounded-full border div.",
10
+ "tags": [
11
+ "auth",
12
+ "topbar",
13
+ "avatar",
14
+ "header",
15
+ "account",
16
+ "chip"
17
+ ]
18
+ }