@squadbase/vantage 0.6.2 → 0.7.0-dev1.1197de7

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 (357) hide show
  1. package/dist/add-4GHF2TX2.js +6 -0
  2. package/dist/add-4GHF2TX2.js.map +1 -0
  3. package/dist/{build-O5TVNH4E.js → build-U7GO26HR.js} +9 -9
  4. package/dist/{build-O5TVNH4E.js.map → build-U7GO26HR.js.map} +1 -1
  5. package/dist/{check-WFBEDHDR.js → check-MRVAIHG5.js} +7 -7
  6. package/dist/{check-WFBEDHDR.js.map → check-MRVAIHG5.js.map} +1 -1
  7. package/dist/{chunk-QDZA5JSL.js → chunk-AABMKHEU.js} +4 -4
  8. package/dist/{chunk-QDZA5JSL.js.map → chunk-AABMKHEU.js.map} +1 -1
  9. package/dist/{chunk-PVTHHV7X.js → chunk-ANZLVSEP.js} +28 -7
  10. package/dist/chunk-ANZLVSEP.js.map +1 -0
  11. package/dist/chunk-CKWPK4RO.js +50 -0
  12. package/dist/chunk-CKWPK4RO.js.map +1 -0
  13. package/dist/chunk-EG4PX726.js +678 -0
  14. package/dist/chunk-EG4PX726.js.map +1 -0
  15. package/dist/{chunk-3G43C6OL.js → chunk-GNXCX4WE.js} +46 -3
  16. package/dist/chunk-GNXCX4WE.js.map +1 -0
  17. package/dist/chunk-LYUGDHSF.js +61 -0
  18. package/dist/chunk-LYUGDHSF.js.map +1 -0
  19. package/dist/{chunk-RPZAALCX.js → chunk-MQUIAP66.js} +321 -165
  20. package/dist/chunk-MQUIAP66.js.map +1 -0
  21. package/dist/chunk-P2KIHF65.js +152 -0
  22. package/dist/chunk-P2KIHF65.js.map +1 -0
  23. package/dist/{chunk-BCE22JSM.js → chunk-PJ4SPXHS.js} +52 -16
  24. package/dist/chunk-PJ4SPXHS.js.map +1 -0
  25. package/dist/{chunk-ZVANIULZ.js → chunk-RNTDBX6S.js} +4 -4
  26. package/dist/{chunk-ZVANIULZ.js.map → chunk-RNTDBX6S.js.map} +1 -1
  27. package/dist/{chunk-PXRX5PEC.js → chunk-ROHNXP7J.js} +73 -4
  28. package/dist/chunk-ROHNXP7J.js.map +1 -0
  29. package/dist/chunk-S5MDTHH7.js +466 -0
  30. package/dist/chunk-S5MDTHH7.js.map +1 -0
  31. package/dist/chunk-SDJTTMLK.js +1877 -0
  32. package/dist/chunk-SDJTTMLK.js.map +1 -0
  33. package/dist/{chunk-EP6JHXSR.js → chunk-SHZM3EXB.js} +68 -12
  34. package/dist/chunk-SHZM3EXB.js.map +1 -0
  35. package/dist/{chunk-A2UUGASH.js → chunk-U736PVHY.js} +2 -2
  36. package/dist/chunk-U736PVHY.js.map +1 -0
  37. package/dist/{chunk-JGRMRXXZ.js → chunk-UWSQUHX6.js} +4 -99
  38. package/dist/chunk-UWSQUHX6.js.map +1 -0
  39. package/dist/chunk-XRKSIC5Z.js +176 -0
  40. package/dist/chunk-XRKSIC5Z.js.map +1 -0
  41. package/dist/chunk-YJQCB3UP.js +10764 -0
  42. package/dist/chunk-YJQCB3UP.js.map +1 -0
  43. package/dist/cli.js +15 -11
  44. package/dist/cli.js.map +1 -1
  45. package/dist/client/index.d.ts +34 -3
  46. package/dist/client/index.js +211 -12
  47. package/dist/client/index.js.map +1 -1
  48. package/dist/components/index.d.ts +2389 -31
  49. package/dist/components/index.js +7 -4
  50. package/dist/delta-text-tav2Lh5d.d.ts +17 -0
  51. package/dist/{dev-NZRLOQCL.js → dev-O42JFQ64.js} +24 -13
  52. package/dist/dev-O42JFQ64.js.map +1 -0
  53. package/dist/doc/index.d.ts +215 -2
  54. package/dist/doc/index.js +423 -5
  55. package/dist/doc/index.js.map +1 -1
  56. package/dist/{docs-ZGDJUHJI.js → docs-JVP4RPKT.js} +3 -3
  57. package/dist/{docs-ZGDJUHJI.js.map → docs-JVP4RPKT.js.map} +1 -1
  58. package/dist/{doctor-VFKEMR54.js → doctor-YD3XAAOT.js} +8 -8
  59. package/dist/doctor-YD3XAAOT.js.map +1 -0
  60. package/dist/format-D34piCei.d.ts +250 -0
  61. package/dist/geo-japan-7BMSOJRX.js +8 -0
  62. package/dist/geo-japan-7BMSOJRX.js.map +1 -0
  63. package/dist/geo-us-GVOG7VXH.js +8 -0
  64. package/dist/geo-us-GVOG7VXH.js.map +1 -0
  65. package/dist/geo-world-35HL56UU.js +8 -0
  66. package/dist/geo-world-35HL56UU.js.map +1 -0
  67. package/dist/markdown/index.js +1 -1
  68. package/dist/markdown/index.js.map +1 -1
  69. package/dist/{preview-QDCIVVSR.js → preview-T6CLSQVX.js} +12 -4
  70. package/dist/preview-T6CLSQVX.js.map +1 -0
  71. package/dist/query/index.d.ts +0 -11
  72. package/dist/query/index.js +2 -2
  73. package/dist/{routes-RHP5FVGD.js → routes-PVKNT777.js} +6 -6
  74. package/dist/{routes-RHP5FVGD.js.map → routes-PVKNT777.js.map} +1 -1
  75. package/dist/{search-RFBELGY7.js → search-5LC4XPB5.js} +3 -3
  76. package/dist/{search-RFBELGY7.js.map → search-5LC4XPB5.js.map} +1 -1
  77. package/dist/slide/index.d.ts +272 -1
  78. package/dist/slide/index.js +1086 -19
  79. package/dist/slide/index.js.map +1 -1
  80. package/dist/table/index.js +13 -10
  81. package/dist/table/index.js.map +1 -1
  82. package/dist/ui/index.d.ts +148 -12
  83. package/dist/ui/index.js +1339 -88
  84. package/dist/ui/index.js.map +1 -1
  85. package/dist/{upgrade-GF2P67M6.js → upgrade-6DT7ADRP.js} +7 -7
  86. package/dist/{upgrade-GF2P67M6.js.map → upgrade-6DT7ADRP.js.map} +1 -1
  87. package/dist/vite/index.d.ts +10 -0
  88. package/dist/vite/index.js +4 -4
  89. package/docs/en/agent-skills.md +1 -1
  90. package/docs/en/changelog.md +82 -0
  91. package/docs/en/cli-reference.md +30 -0
  92. package/docs/en/components.md +42 -17
  93. package/docs/en/doc/delta-text.md +40 -0
  94. package/docs/en/doc/doc-action-items.md +49 -0
  95. package/docs/en/doc/doc-callout.md +38 -0
  96. package/docs/en/doc/doc-data-table.md +60 -0
  97. package/docs/en/doc/doc-executive-summary.md +64 -0
  98. package/docs/en/doc/doc-figure.md +56 -0
  99. package/docs/en/doc/doc-key-findings.md +61 -0
  100. package/docs/en/doc/doc-kpi-scorecard.md +80 -0
  101. package/docs/en/doc/doc-notes-and-sources.md +43 -0
  102. package/docs/en/doc/doc-report-header.md +46 -0
  103. package/docs/en/pages-and-metadata.md +15 -0
  104. package/docs/en/parts/area-chart.md +99 -0
  105. package/docs/en/parts/bar-chart.md +91 -0
  106. package/docs/en/parts/bar-list.md +50 -0
  107. package/docs/en/parts/category-bar.md +37 -0
  108. package/docs/en/parts/chart-frame.md +39 -0
  109. package/docs/en/parts/data-table.md +22 -0
  110. package/docs/en/parts/delta.md +51 -0
  111. package/docs/en/parts/donut-chart.md +72 -0
  112. package/docs/en/parts/draft-chart.md +42 -0
  113. package/docs/en/parts/format.md +43 -0
  114. package/docs/en/parts/funnel-steps.md +2 -1
  115. package/docs/en/parts/heatmap.md +53 -0
  116. package/docs/en/parts/kpi-card.md +58 -0
  117. package/docs/en/parts/legend.md +35 -0
  118. package/docs/en/parts/line-chart.md +94 -0
  119. package/docs/en/parts/metric-value.md +3 -2
  120. package/docs/en/parts/page-shell.md +2 -2
  121. package/docs/en/parts/progress-circle.md +33 -0
  122. package/docs/en/parts/sample-data.md +39 -0
  123. package/docs/en/parts/scatter-chart.md +56 -0
  124. package/docs/en/parts/sparkline.md +1 -1
  125. package/docs/en/parts/tracker.md +36 -0
  126. package/docs/en/parts/trend-indicator.md +17 -1
  127. package/docs/en/parts/updated-at.md +30 -0
  128. package/docs/en/parts/waterfall-chart.md +56 -0
  129. package/docs/en/slide/slide-chart-full.md +48 -0
  130. package/docs/en/slide/slide-chart-insight.md +52 -0
  131. package/docs/en/slide/slide-columns.md +46 -0
  132. package/docs/en/slide/slide-comparison.md +68 -0
  133. package/docs/en/slide/slide-cover.md +39 -0
  134. package/docs/en/slide/slide-exec-summary.md +47 -0
  135. package/docs/en/slide/slide-heading.md +38 -0
  136. package/docs/en/slide/slide-kpi-grid.md +55 -0
  137. package/docs/en/slide/slide-kpi-hero.md +58 -0
  138. package/docs/en/slide/slide-next-actions.md +49 -0
  139. package/docs/en/slide/slide-scorecard-table.md +54 -0
  140. package/docs/en/slide/slide-section.md +44 -0
  141. package/docs/en/slide/slide-source-line.md +34 -0
  142. package/docs/en/ui/alert-dialog.md +42 -0
  143. package/docs/en/ui/avatar.md +31 -0
  144. package/docs/en/ui/combobox.md +74 -0
  145. package/docs/en/ui/drawer.md +41 -0
  146. package/docs/en/ui/empty.md +1 -1
  147. package/docs/en/ui/error-state.md +4 -3
  148. package/docs/en/ui/field.md +42 -0
  149. package/docs/en/ui/hover-card.md +21 -0
  150. package/docs/en/ui/kbd.md +17 -0
  151. package/docs/en/ui/loading.md +1 -1
  152. package/docs/en/ui/pagination.md +36 -0
  153. package/docs/en/ui/radio-group.md +21 -0
  154. package/docs/en/ui/toast.md +44 -0
  155. package/docs/en/ui/toggle-group.md +4 -3
  156. package/docs/en/ui-and-theming.md +1 -1
  157. package/docs/index.json +1222 -132
  158. package/docs/ja/agent-skills.md +1 -1
  159. package/docs/ja/changelog.md +84 -0
  160. package/docs/ja/cli-reference.md +29 -0
  161. package/docs/ja/components.md +68 -101
  162. package/docs/ja/doc/delta-text.md +40 -0
  163. package/docs/ja/doc/doc-action-items.md +49 -0
  164. package/docs/ja/doc/doc-callout.md +38 -0
  165. package/docs/ja/doc/doc-data-table.md +60 -0
  166. package/docs/ja/doc/doc-executive-summary.md +64 -0
  167. package/docs/ja/doc/doc-figure.md +56 -0
  168. package/docs/ja/doc/doc-key-findings.md +61 -0
  169. package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
  170. package/docs/ja/doc/doc-notes-and-sources.md +43 -0
  171. package/docs/ja/doc/doc-report-header.md +46 -0
  172. package/docs/ja/page-types/csv.md +83 -0
  173. package/docs/ja/page-types/doc.md +83 -0
  174. package/docs/ja/page-types/excel.md +82 -0
  175. package/docs/ja/page-types/slide.md +133 -0
  176. package/docs/ja/page-types.md +47 -0
  177. package/docs/ja/pages-and-metadata.md +15 -0
  178. package/docs/ja/parts/area-chart.md +85 -0
  179. package/docs/ja/parts/bar-chart.md +95 -0
  180. package/docs/ja/parts/bar-list.md +58 -0
  181. package/docs/ja/parts/category-bar.md +36 -0
  182. package/docs/ja/parts/chart-frame.md +36 -0
  183. package/docs/ja/parts/data-table.md +22 -0
  184. package/docs/ja/parts/delta.md +51 -0
  185. package/docs/ja/parts/donut-chart.md +66 -0
  186. package/docs/ja/parts/draft-chart.md +40 -0
  187. package/docs/ja/parts/echart.md +3 -0
  188. package/docs/ja/parts/format.md +44 -0
  189. package/docs/ja/parts/funnel-steps.md +27 -8
  190. package/docs/ja/parts/heatmap.md +45 -0
  191. package/docs/ja/parts/kpi-card.md +56 -0
  192. package/docs/ja/parts/legend.md +36 -0
  193. package/docs/ja/parts/line-chart.md +84 -0
  194. package/docs/ja/parts/metric-value.md +3 -2
  195. package/docs/ja/parts/page-shell.md +2 -2
  196. package/docs/ja/parts/progress-circle.md +34 -0
  197. package/docs/ja/parts/sample-data.md +61 -0
  198. package/docs/ja/parts/scatter-chart.md +69 -0
  199. package/docs/ja/parts/sparkline.md +2 -2
  200. package/docs/ja/parts/tracker.md +32 -0
  201. package/docs/ja/parts/trend-indicator.md +17 -1
  202. package/docs/ja/parts/updated-at.md +27 -0
  203. package/docs/ja/parts/waterfall-chart.md +61 -0
  204. package/docs/ja/routing.md +8 -86
  205. package/docs/ja/slide/slide-chart-full.md +48 -0
  206. package/docs/ja/slide/slide-chart-insight.md +52 -0
  207. package/docs/ja/slide/slide-columns.md +46 -0
  208. package/docs/ja/slide/slide-comparison.md +68 -0
  209. package/docs/ja/slide/slide-cover.md +39 -0
  210. package/docs/ja/slide/slide-exec-summary.md +47 -0
  211. package/docs/ja/slide/slide-heading.md +38 -0
  212. package/docs/ja/slide/slide-kpi-grid.md +55 -0
  213. package/docs/ja/slide/slide-kpi-hero.md +58 -0
  214. package/docs/ja/slide/slide-next-actions.md +49 -0
  215. package/docs/ja/slide/slide-scorecard-table.md +54 -0
  216. package/docs/ja/slide/slide-section.md +44 -0
  217. package/docs/ja/slide/slide-source-line.md +34 -0
  218. package/docs/ja/ui/alert-dialog.md +41 -0
  219. package/docs/ja/ui/avatar.md +31 -0
  220. package/docs/ja/ui/combobox.md +74 -0
  221. package/docs/ja/ui/drawer.md +41 -0
  222. package/docs/ja/ui/empty.md +1 -1
  223. package/docs/ja/ui/error-state.md +4 -3
  224. package/docs/ja/ui/field.md +42 -0
  225. package/docs/ja/ui/hover-card.md +21 -0
  226. package/docs/ja/ui/kbd.md +16 -0
  227. package/docs/ja/ui/loading.md +1 -1
  228. package/docs/ja/ui/pagination.md +36 -0
  229. package/docs/ja/ui/radio-group.md +21 -0
  230. package/docs/ja/ui/toast.md +46 -0
  231. package/docs/ja/ui/toggle-group.md +4 -3
  232. package/docs/ja/ui-and-theming.md +14 -7
  233. package/package.json +43 -29
  234. package/registry/parts.json +5221 -0
  235. package/registry/ui/accordion.tsx +76 -0
  236. package/registry/ui/alert-dialog.tsx +189 -0
  237. package/registry/ui/alert.tsx +80 -0
  238. package/registry/ui/app-shell.tsx +424 -0
  239. package/registry/ui/area-chart.tsx +3960 -0
  240. package/registry/ui/attention-list.tsx +465 -0
  241. package/registry/ui/avatar.tsx +110 -0
  242. package/registry/ui/badge.tsx +56 -0
  243. package/registry/ui/bar-chart.tsx +3954 -0
  244. package/registry/ui/bar-list.tsx +3998 -0
  245. package/registry/ui/box-plot-chart.tsx +4214 -0
  246. package/registry/ui/breadcrumb.tsx +128 -0
  247. package/registry/ui/button.tsx +62 -0
  248. package/registry/ui/calendar.tsx +223 -0
  249. package/registry/ui/card.tsx +107 -0
  250. package/registry/ui/category-bar.tsx +1498 -0
  251. package/registry/ui/chart-frame.tsx +1612 -0
  252. package/registry/ui/checkbox.tsx +30 -0
  253. package/registry/ui/collapsible.tsx +23 -0
  254. package/registry/ui/combo-chart.tsx +1852 -0
  255. package/registry/ui/combobox.tsx +310 -0
  256. package/registry/ui/command.tsx +198 -0
  257. package/registry/ui/dashboard-card.tsx +219 -0
  258. package/registry/ui/data-table.tsx +899 -158
  259. package/registry/ui/date-range-picker.tsx +544 -0
  260. package/registry/ui/delta.tsx +352 -0
  261. package/registry/ui/dialog.tsx +161 -0
  262. package/registry/ui/donut-chart.tsx +4006 -0
  263. package/registry/ui/drawer.tsx +229 -0
  264. package/registry/ui/dropdown-menu.tsx +274 -0
  265. package/registry/ui/echart.tsx +1178 -0
  266. package/registry/ui/field.tsx +240 -0
  267. package/registry/ui/filter-bar.tsx +553 -0
  268. package/registry/ui/forecast-chart.tsx +4734 -0
  269. package/registry/ui/funnel-steps.tsx +3843 -0
  270. package/registry/ui/heatmap.tsx +2027 -0
  271. package/registry/ui/histogram-chart.tsx +4284 -0
  272. package/registry/ui/hover-card.tsx +52 -0
  273. package/registry/ui/input-group.tsx +160 -0
  274. package/registry/ui/input.tsx +24 -0
  275. package/registry/ui/kbd.tsx +30 -0
  276. package/registry/ui/kpi-card.tsx +907 -0
  277. package/registry/ui/label.tsx +22 -0
  278. package/registry/ui/legend.tsx +3817 -0
  279. package/registry/ui/line-chart.tsx +3958 -0
  280. package/registry/ui/metric-value.tsx +62 -0
  281. package/registry/ui/multi-select.tsx +382 -0
  282. package/registry/ui/page-shell.tsx +441 -0
  283. package/registry/ui/pagination.tsx +140 -0
  284. package/registry/ui/paired-chart.tsx +4086 -0
  285. package/registry/ui/pivot-table.tsx +1260 -0
  286. package/registry/ui/placeholder.tsx +36 -0
  287. package/registry/ui/popover.tsx +92 -0
  288. package/registry/ui/progress-circle.tsx +1033 -0
  289. package/registry/ui/progress.tsx +85 -0
  290. package/registry/ui/radio-group.tsx +39 -0
  291. package/registry/ui/sample-data.tsx +230 -0
  292. package/registry/ui/scatter-chart.tsx +4503 -0
  293. package/registry/ui/scroll-area.tsx +56 -0
  294. package/registry/ui/searchable-select.tsx +352 -0
  295. package/registry/ui/section-header.tsx +43 -0
  296. package/registry/ui/segmented-control.tsx +76 -0
  297. package/registry/ui/select.tsx +255 -0
  298. package/registry/ui/separator.tsx +27 -0
  299. package/registry/ui/sheet.tsx +139 -0
  300. package/registry/ui/sidebar.tsx +725 -0
  301. package/registry/ui/skeleton.tsx +17 -0
  302. package/registry/ui/slider.tsx +56 -0
  303. package/registry/ui/sparkline.tsx +230 -0
  304. package/registry/ui/spinner.tsx +14 -0
  305. package/registry/ui/states.tsx +247 -0
  306. package/registry/ui/status-badge.tsx +286 -0
  307. package/registry/ui/switch.tsx +34 -0
  308. package/registry/ui/table.tsx +118 -0
  309. package/registry/ui/tabs.tsx +84 -0
  310. package/registry/ui/textarea.tsx +22 -0
  311. package/registry/ui/toast.tsx +250 -0
  312. package/registry/ui/toggle-group.tsx +91 -0
  313. package/registry/ui/toggle.tsx +47 -0
  314. package/registry/ui/tooltip.tsx +72 -0
  315. package/registry/ui/tracker.tsx +1015 -0
  316. package/registry/ui/treemap.tsx +1917 -0
  317. package/registry/ui/trend-indicator.tsx +217 -0
  318. package/registry/ui/updated-at.tsx +276 -0
  319. package/registry/ui/waterfall-chart.tsx +4561 -0
  320. package/skills/vantage-add-feature/SKILL.md +14 -7
  321. package/skills/vantage-design/SKILL.md +62 -52
  322. package/skills/vantage-design/references/extract.md +26 -27
  323. package/skills/vantage-design/references/format.md +12 -172
  324. package/skills/vantage-design/references/presets.md +50 -110
  325. package/skills/vantage-pitfalls/SKILL.md +10 -1
  326. package/skills/vantage-slide/SKILL.md +71 -36
  327. package/templates/AGENTS.md +54 -16
  328. package/templates/agents/api.md +36 -0
  329. package/templates/agents/csv.md +2 -1
  330. package/templates/agents/doc.md +52 -3
  331. package/templates/agents/page.md +91 -10
  332. package/templates/agents/slide.md +90 -4
  333. package/templates/agents/xlsx.md +2 -1
  334. package/theme.css +247 -132
  335. package/dist/add-EISGSZAH.js +0 -6
  336. package/dist/add-EISGSZAH.js.map +0 -1
  337. package/dist/chunk-3G43C6OL.js.map +0 -1
  338. package/dist/chunk-5ZOP4SQR.js +0 -47
  339. package/dist/chunk-5ZOP4SQR.js.map +0 -1
  340. package/dist/chunk-A2UUGASH.js.map +0 -1
  341. package/dist/chunk-BCE22JSM.js.map +0 -1
  342. package/dist/chunk-DTDVSFRY.js +0 -29
  343. package/dist/chunk-DTDVSFRY.js.map +0 -1
  344. package/dist/chunk-EP6JHXSR.js.map +0 -1
  345. package/dist/chunk-JGRMRXXZ.js.map +0 -1
  346. package/dist/chunk-KLEIVDUV.js +0 -165
  347. package/dist/chunk-KLEIVDUV.js.map +0 -1
  348. package/dist/chunk-LSRMRIXA.js +0 -2227
  349. package/dist/chunk-LSRMRIXA.js.map +0 -1
  350. package/dist/chunk-PVTHHV7X.js.map +0 -1
  351. package/dist/chunk-PXRX5PEC.js.map +0 -1
  352. package/dist/chunk-RPZAALCX.js.map +0 -1
  353. package/dist/chunk-T3FLAGI6.js +0 -316
  354. package/dist/chunk-T3FLAGI6.js.map +0 -1
  355. package/dist/dev-NZRLOQCL.js.map +0 -1
  356. package/dist/doctor-VFKEMR54.js.map +0 -1
  357. package/dist/preview-QDCIVVSR.js.map +0 -1
@@ -1,6 +1,11 @@
1
1
  // Copied by `vantage add ui data-table`. This file is yours to edit.
2
+ // Generated from @squadbase/squadkit registry/components/data-table.tsx by `pnpm run registry` (in Vantage, edit packages/squadkit, not registry/).
3
+ "use client"
4
+
2
5
  /**
3
- * DataTable — a compound table over TanStack Table. `<DataTable>` owns the
6
+ * DataTablePreset — a table (toolbar, body, pagination) over TanStack Table.
7
+ *
8
+ * `DataTable` is the compound form: `<DataTable>` owns the
4
9
  * table instance and publishes it on context; the toolbar, body, pagination and
5
10
  * column-visibility parts read from that context, so a page composes only the
6
11
  * pieces it wants. `DataTablePreset` assembles the usual arrangement.
@@ -14,7 +19,9 @@ import {
14
19
  forwardRef,
15
20
  useCallback,
16
21
  useContext,
22
+ useEffect,
17
23
  useMemo,
24
+ useRef,
18
25
  useState,
19
26
  type HTMLAttributes,
20
27
  type ReactNode,
@@ -27,43 +34,375 @@ import {
27
34
  getPaginationRowModel,
28
35
  getSortedRowModel,
29
36
  useReactTable,
37
+ type Column,
30
38
  type ColumnDef,
31
39
  type ColumnFiltersState,
32
40
  type OnChangeFn,
33
41
  type PaginationState,
42
+ type RowData,
34
43
  type RowSelectionState,
35
44
  type SortingState,
36
45
  type Table as TanstackTable,
37
46
  type TableMeta,
38
47
  type VisibilityState,
39
48
  } from "@tanstack/react-table"
40
- import { ArrowDown, ArrowUp, ArrowUpDown, Columns3, Search } from "lucide-react"
41
-
42
49
  import {
43
- Button,
44
- Checkbox,
50
+ ArrowDown,
51
+ ArrowUp,
52
+ ArrowUpDown,
53
+ CircleCheck,
54
+ CircleX,
55
+ Columns3,
56
+ Download,
57
+ Minus,
58
+ Search,
59
+ TriangleAlert,
60
+ type LucideIcon,
61
+ } from "lucide-react"
62
+
63
+ import { Sparkline } from "@squadbase/vantage/components"
64
+ import { toneForValue } from "@squadbase/vantage/components"
65
+ import { EMPTY_VALUE, formatDate, formatValue, type DateStyle, type ValueFormat } from "@squadbase/vantage/components"
66
+ import type { ChartThreshold, ChartTone } from "@squadbase/vantage/components"
67
+ import { cn } from "@squadbase/vantage/ui"
68
+ import { Button } from "@squadbase/vantage/ui"
69
+ import { Checkbox } from "@squadbase/vantage/ui"
70
+ import {
45
71
  DropdownMenu,
46
72
  DropdownMenuCheckboxItem,
47
73
  DropdownMenuContent,
48
74
  DropdownMenuTrigger,
49
- Input,
50
- Select,
51
- SelectContent,
52
- SelectItem,
53
- SelectTrigger,
54
- SelectValue,
55
- Table,
56
- TableBody,
57
- TableCell,
58
- TableHead,
59
- TableHeader,
60
- TableRow,
61
- cn,
62
75
  } from "@squadbase/vantage/ui"
76
+ import { Input } from "@squadbase/vantage/ui"
77
+ import { Skeleton } from "@squadbase/vantage/ui"
78
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@squadbase/vantage/ui"
79
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@squadbase/vantage/ui"
80
+ import { useSyncExternalStore } from "react"
81
+
82
+ /* ---------- inlined from @squadbase/squadkit registry/lib/locale.ts (internal to @squadbase/vantage) ---------- */
83
+
84
+ /**
85
+ * The display language of the parts and the defaults that follow it.
86
+ *
87
+ * The language comes from `<html lang>`: a tag starting with `ja` reads Japanese, anything else English.
88
+ * Without a usable `lang` (missing, invalid, or no `document` — SSR, Node) the locale is `FALLBACK_LOCALE`
89
+ * (`en-US`). A `locale` passed explicitly (a prop, an argument) always wins.
90
+ *
91
+ * Parts read the locale through `useLocale` (`hooks/use-locale`) and pass it down; the pure helpers here
92
+ * and in `format` / `comparison` / `chart-*` take it as an argument and fall back to `documentLocale()`.
93
+ */
94
+
95
+ /** The language of the built-in wording: Japanese or English. */
96
+ type UiLanguage = "ja" | "en"
97
+
98
+ /** The locale used when `<html lang>` gives none (and on the server). */
99
+ const FALLBACK_LOCALE = "en-US"
100
+
101
+ /** `"ja"` when `locale` starts with `ja` (`ja`, `ja-JP`), else `"en"`. A missing locale is `"en"`. */
102
+ function uiLanguage(locale: string | null | undefined): UiLanguage {
103
+ return typeof locale === "string" && locale.trim().toLowerCase().startsWith("ja") ? "ja" : "en"
104
+ }
105
+
106
+ /** A BCP 47 tag in its canonical form (`en-gb` → `en-GB`), or `FALLBACK_LOCALE` when it is empty or invalid. */
107
+ function normalizeLocale(tag: string | null | undefined): string {
108
+ const value = tag?.trim()
109
+ if (!value) return FALLBACK_LOCALE
110
+ try {
111
+ return Intl.getCanonicalLocales(value)[0] ?? FALLBACK_LOCALE
112
+ } catch {
113
+ return FALLBACK_LOCALE
114
+ }
115
+ }
116
+
117
+ /**
118
+ * The locale of the page: `document.documentElement.lang`, canonicalized. `FALLBACK_LOCALE` when it is empty or
119
+ * invalid, or there is no `document`. Parts read it through `useLocale`, never while rendering.
120
+ */
121
+ function documentLocale(): string {
122
+ if (typeof document === "undefined") return FALLBACK_LOCALE
123
+ return normalizeLocale(document.documentElement?.getAttribute("lang"))
124
+ }
125
+
126
+ /** `locale` when given, else the page's locale (`documentLocale()`). */
127
+ function resolveLocale(locale?: string | null): string {
128
+ return locale || documentLocale()
129
+ }
130
+
131
+ /** Picks the wording table of the locale's language. */
132
+ function pickLocale<T>(table: { ja: T; en: T }, locale?: string | null): T {
133
+ return table[uiLanguage(resolveLocale(locale))]
134
+ }
135
+
136
+ /** The currency `format="currency"` uses without a `currency`: JPY for Japanese, USD otherwise. */
137
+ function defaultCurrency(locale?: string | null): string {
138
+ return uiLanguage(resolveLocale(locale)) === "ja" ? "JPY" : "USD"
139
+ }
140
+
141
+ /** The month a fiscal year starts in (1–12) when none is given: April for Japanese, January otherwise. */
142
+ function defaultFiscalYearStartMonth(locale?: string | null): number {
143
+ return uiLanguage(resolveLocale(locale)) === "ja" ? 4 : 1
144
+ }
145
+
146
+ /** A day of the week, 0 = Sunday … 6 = Saturday (the `Date#getDay` / date-fns numbering). */
147
+ type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6
63
148
 
149
+ /** Locales whose week starts on Sunday when the runtime has no week data. */
150
+ const SUNDAY_FIRST = new Set(["en", "en-US", "en-CA"])
151
+
152
+ /**
153
+ * The first day of the week: Monday for Japanese; otherwise the locale's convention from `Intl.Locale`
154
+ * (`en-US` Sunday, `en-GB` Monday). Without week data in the runtime: Sunday for `en` / `en-US` / `en-CA`,
155
+ * Monday for the rest.
156
+ */
157
+ function weekStartsOn(locale?: string | null): WeekDay {
158
+ const tag = normalizeLocale(resolveLocale(locale))
159
+ if (uiLanguage(tag) === "ja") return 1
160
+ try {
161
+ const info = new Intl.Locale(tag) as Intl.Locale & {
162
+ getWeekInfo?: () => { firstDay: number }
163
+ weekInfo?: { firstDay: number }
164
+ }
165
+ const firstDay = info.getWeekInfo?.().firstDay ?? info.weekInfo?.firstDay
166
+ if (typeof firstDay === "number" && firstDay >= 1 && firstDay <= 7) return (firstDay % 7) as WeekDay
167
+ } catch {
168
+ // Fall through to the table below.
169
+ }
170
+ return SUNDAY_FIRST.has(tag) ? 0 : 1
171
+ }
172
+
173
+ /* ---------- inlined from @squadbase/squadkit registry/hooks/use-locale.ts (internal to @squadbase/vantage) ---------- */
174
+
175
+ const listeners = new Set<() => void>()
176
+ let observer: MutationObserver | null = null
177
+
178
+ /** One observer on `<html lang>` for every subscribed part. */
179
+ function subscribe(onChange: () => void): () => void {
180
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {}
181
+ listeners.add(onChange)
182
+ if (!observer) {
183
+ observer = new MutationObserver(() => {
184
+ for (const listener of [...listeners]) listener()
185
+ })
186
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] })
187
+ }
188
+ return () => {
189
+ listeners.delete(onChange)
190
+ if (listeners.size === 0 && observer) {
191
+ observer.disconnect()
192
+ observer = null
193
+ }
194
+ }
195
+ }
196
+
197
+ const serverLocale = () => FALLBACK_LOCALE
198
+
199
+ /**
200
+ * The page's locale (`<html lang>`), following changes made while the page runs. On the server and during
201
+ * hydration it is `FALLBACK_LOCALE` (`en-US`), so the markup matches; it switches right after.
202
+ */
203
+ function useDocumentLocale(): string {
204
+ return useSyncExternalStore(subscribe, documentLocale, serverLocale)
205
+ }
206
+
207
+ /** The locale a part renders in: its `locale` prop when given, else the page's (`useDocumentLocale`). */
208
+ function useLocale(locale?: string | null): string {
209
+ const page = useDocumentLocale()
210
+ return locale || page
211
+ }
212
+
213
+ /* ---------- end of inlined code ---------- */
64
214
 
65
215
  /* ------------------------------------------------------------------- types */
66
216
 
217
+ declare module "@tanstack/react-table" {
218
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
219
+ interface ColumnMeta<TData extends RowData, TValue> {
220
+ /**
221
+ * Classes put on the column's header and body cells — e.g.
222
+ * `"hidden md:table-cell"` to drop a column on narrow screens.
223
+ */
224
+ className?: string
225
+ /**
226
+ * 列の数値書式。`cell` を持たない列だけに効き、右寄せ・tabular-nums で描く。
227
+ * 行ごとに通貨が変わるときは関数で行を受ける。CSV の「表示」値にも使われる。
228
+ *
229
+ * @example
230
+ * meta: { format: "currency" }
231
+ * meta: { format: (row) => ({ type: "currency", currency: row.currency, notation: "standard" }) }
232
+ */
233
+ format?: ValueFormat | ((row: TData) => ValueFormat)
234
+ /**
235
+ * 日付の列の書式(`formatDate` の `style`)。`cell` を持たない列だけに効く。左寄せ・tabular-nums で描き、
236
+ * 値は `Date`・エポックミリ秒・日付文字列(`"2026-10-20"`、ISO 8601)。CSV の「表示」値にも使われる。
237
+ * `format`(数値)とは併用しない。
238
+ *
239
+ * @example
240
+ * meta: { dateFormat: "date" }
241
+ * meta: { dateFormat: "datetime", nullLabel: "未更新" }
242
+ */
243
+ dateFormat?: DateStyle
244
+ /**
245
+ * `dateFormat` の列の日時を出すタイムゾーン(IANA 名。例 `"Asia/Tokyo"`)。既定は実行環境(ブラウザ)のタイムゾーン。
246
+ * サーバーとブラウザで表示が食い違う(SSR)なら固定する。`dateFormat` の列だけに効き、CSV の「表示」値にも同じ値を使う。
247
+ * `"2026-10-20"` のような日付だけの文字列は、どのタイムゾーンでもその暦日のまま。
248
+ *
249
+ * @example
250
+ * meta: { dateFormat: "datetime", timeZone: "Asia/Tokyo" }
251
+ */
252
+ timeZone?: string
253
+ /**
254
+ * 値が無いときの表示。既定 `"—"`。null / undefined(数値は NaN も)に効き、文字列の列では空文字 `""` も値なしとして扱う。
255
+ * `format` / `dateFormat` を持つ列と、書式なしの文字列の列に効く。`cell` を自前で書いた列には効かない(`cell` の中で扱う)。
256
+ * CSV の「表示」値は、書式付き・日付の列だけ `nullLabel` で書き、書式なしの文字列の列は空欄のまま。
257
+ *
258
+ * @example
259
+ * meta: { format: "number", nullLabel: "未集計" }
260
+ * meta: { nullLabel: "未設定" } // 書式なしの文字列の列
261
+ */
262
+ nullLabel?: string
263
+ /**
264
+ * 値の範囲でセルを意味色に塗り分ける規則(最初に当たった規則。どれにも当たらない値は通常色)。
265
+ * `cell` を持たない列だけに効き、BarChart / BarList の `thresholds` と同じ型・同じ判定。
266
+ * 色だけに頼らず、色に応じた記号(○ / △ / × / −)を値の前に添える。CSV は生の値のまま。
267
+ *
268
+ * @example
269
+ * meta: { format: { type: "percent", divisor: 100 }, thresholds: [{ lt: 40, tone: "negative" }, { lt: 70, tone: "warning" }] }
270
+ */
271
+ thresholds?: ChartThreshold[]
272
+ /**
273
+ * 狭い幅で列を隠す順。数が小さい列から隠れる(表の置き場の幅が 1 は 640px 未満、2 は 520px 未満、3 は 400px 未満)。
274
+ * 書かない列と 4 以上の列は、どの幅でも出す(部署名・離職率のような肝心の列に使う)。
275
+ * 画面の幅ではなく、表を置いた場所の幅(コンテナ幅)で決まる。隠した列は DOM にも読み上げにも出ず、CSV の出力には含まれる。
276
+ *
277
+ * @example
278
+ * meta: { format: "number", priority: 1 } // 幅が足りないときに最初に隠す
279
+ */
280
+ priority?: number
281
+ /**
282
+ * true にすると、増減(`Delta`)など「直前の列の値との比較」を表示する列になる。表の置き場が 320px 未満に狭まると、
283
+ * この列は隠れる代わりに、直前の列(値の列)のセルの下に同じ内容を小さく(text-xs)積んで出す。比較は消えない。
284
+ * `priority` で隠す順は最後(どの列よりも後)になり、この列の `priority` は無視される。直前の列も `priority` に関わらず残る。
285
+ * 列の `cell` に `<Delta variant="inline" … />` を書く。直前に列が無いときは積まず、通常の列として出す。CSV には通常の列として含まれる。
286
+ *
287
+ * @example
288
+ * { accessorKey: "revenue", header: "売上", meta: { format: "currency" } },
289
+ * { id: "revenueDelta", header: "前月比", meta: { comparison: true }, cell: ({ row }) => <Delta variant="inline" value={row.original.revenue} comparison={{ previous: row.original.prev, vs: "prior-period", grain: "month" }} /> }
290
+ */
291
+ comparison?: boolean
292
+ /**
293
+ * 列の寄せ。見出し(ソートボタンを含む)とセルの両方に効く。既定は、`format` / `thresholds` の列と
294
+ * `comparison` の列(増減 `Delta`)が `"right"`、それ以外は `"left"`。右寄せの列は tabular-nums で描く。
295
+ *
296
+ * @example
297
+ * meta: { align: "right" }
298
+ */
299
+ align?: "left" | "right"
300
+ }
301
+ }
302
+
303
+ /* ------------------------------------------------------------ wording */
304
+
305
+ const TEXT = {
306
+ ja: {
307
+ tones: { positive: "良好", warning: "注意", negative: "要対応", neutral: "中立" } as Record<ChartTone, string>,
308
+ toneNote: (label: string) => `(${label})`,
309
+ selectAll: "全て選択",
310
+ selectRow: "行を選択",
311
+ trend: (points: number, first: string, last: string, min: string, max: string) =>
312
+ `${points}点の推移、${first}から${last}、最小${min}、最大${max}`,
313
+ noData: "データがありません",
314
+ pageSize: (size: number) => `${size}件`,
315
+ range: (total: number, start: number, end: number) => `${total}件中 ${start} - ${end} を表示`,
316
+ first: "最初",
317
+ previous: "前へ",
318
+ next: "次へ",
319
+ last: "最後",
320
+ columns: "表示する列",
321
+ exportCsv: "CSV を書き出す",
322
+ },
323
+ en: {
324
+ tones: { positive: "Good", warning: "Caution", negative: "Needs action", neutral: "Neutral" } as Record<ChartTone, string>,
325
+ toneNote: (label: string) => ` (${label})`,
326
+ selectAll: "Select all",
327
+ selectRow: "Select row",
328
+ trend: (points: number, first: string, last: string, min: string, max: string) =>
329
+ `Trend over ${points} points: ${first} to ${last}, min ${min}, max ${max}`,
330
+ noData: "No results.",
331
+ pageSize: (size: number) => `${size} / page`,
332
+ range: (total: number, start: number, end: number) => `Showing ${start}–${end} of ${total}`,
333
+ first: "First",
334
+ previous: "Previous",
335
+ next: "Next",
336
+ last: "Last",
337
+ columns: "Columns",
338
+ exportCsv: "Export CSV",
339
+ },
340
+ } satisfies Record<UiLanguage, unknown>
341
+
342
+ const useText = () => TEXT[uiLanguage(useLocale())]
343
+
344
+ /* ------------------------------------------------------------ cell formatting */
345
+
346
+ /** Resolve a column's `meta.format` for one row. */
347
+ function resolveFormat<TData>(
348
+ format: ValueFormat | ((row: TData) => ValueFormat) | undefined,
349
+ row: TData,
350
+ ): ValueFormat | undefined {
351
+ return typeof format === "function" ? format(row) : format
352
+ }
353
+
354
+ /** 値なし: null / undefined / NaN / 空文字。 */
355
+ function isMissingCell(value: unknown): boolean {
356
+ return value === null || value === undefined || value === "" || (typeof value === "number" && Number.isNaN(value))
357
+ }
358
+
359
+ /**
360
+ * Display string of a column's value (`meta.format` / `meta.dateFormat` / plain);
361
+ * a missing value renders as `nullLabel` (default `—`).
362
+ */
363
+ function formatCellText<TData>(
364
+ value: unknown,
365
+ row: TData,
366
+ meta: { format?: ValueFormat | ((row: TData) => ValueFormat); dateFormat?: DateStyle; timeZone?: string; nullLabel?: string },
367
+ locale: string,
368
+ ): string {
369
+ if (isMissingCell(value)) return meta.nullLabel ?? EMPTY_VALUE
370
+ if (meta.dateFormat && (value instanceof Date || typeof value === "string" || typeof value === "number")) {
371
+ const text = formatDate(value, meta.dateFormat, locale, { timeZone: meta.timeZone })
372
+ return text === EMPTY_VALUE ? String(value) : text
373
+ }
374
+ if (typeof value !== "number") return String(value)
375
+ return formatValue(value, resolveFormat(meta.format, row), locale)
376
+ }
377
+
378
+ /** 意味色ごとの記号と色クラス。Tailwind が拾えるよう、クラスはリテラルで書く。 */
379
+ const THRESHOLD_TONE_STYLES: Record<ChartTone, { Icon: LucideIcon; text: string }> = {
380
+ positive: { Icon: CircleCheck, text: "text-positive" },
381
+ warning: { Icon: TriangleAlert, text: "text-warning" },
382
+ negative: { Icon: CircleX, text: "text-negative" },
383
+ neutral: { Icon: Minus, text: "text-neutral" },
384
+ }
385
+
386
+ /** `meta.thresholds` 列のセル。当たった規則の意味色と記号を値に添える(当たらなければ値だけ)。 */
387
+ function ThresholdCell({ value, text, thresholds }: { value: unknown; text: string; thresholds: ChartThreshold[] }) {
388
+ const t = useText()
389
+ const tone = typeof value === "number" ? toneForValue(value, thresholds) : undefined
390
+ if (!tone) return <div className="text-right tabular-nums">{text}</div>
391
+ const { Icon, text: toneClass } = THRESHOLD_TONE_STYLES[tone]
392
+ const label = t.tones[tone]
393
+ return (
394
+ <div
395
+ data-slot="data-table-threshold-cell"
396
+ data-tone={tone}
397
+ className={cn("flex items-center justify-end gap-1.5 tabular-nums", toneClass)}
398
+ >
399
+ <Icon className="size-3.5 shrink-0" aria-hidden="true" />
400
+ <span>{text}</span>
401
+ <span className="sr-only">{t.toneNote(label)}</span>
402
+ </div>
403
+ )
404
+ }
405
+
67
406
  interface DataTableContextValue<TData> {
68
407
  table: TanstackTable<TData>
69
408
  selectedRows: TData[]
@@ -74,6 +413,7 @@ interface DataTableContextValue<TData> {
74
413
  enablePagination: boolean
75
414
  enableRowSelection: boolean
76
415
  visibleColumnCount: number
416
+ isLoading: boolean
77
417
  }
78
418
 
79
419
  /**
@@ -89,7 +429,8 @@ export interface DataTableProps<TData, TValue> {
89
429
  * literal が `string` に拡張され型エラーになる。
90
430
  *
91
431
  * @example
92
- * import type { ColumnDef } from "@squadbase/vantage/components"
432
+ * import { DataTable, type ColumnDef } from "@/components/data-table"
433
+ * // Vantage: import { DataTable, type ColumnDef } from "@squadbase/vantage/components"
93
434
  *
94
435
  * const columns: ColumnDef<User>[] = [
95
436
  * { accessorKey: "id", header: "ID" },
@@ -116,7 +457,42 @@ export interface DataTableProps<TData, TValue> {
116
457
  enablePagination?: boolean
117
458
  pagination?: PaginationState
118
459
  onPaginationChange?: OnChangeFn<PaginationState>
460
+ /**
461
+ * サーバー側ページング時の総ページ数。指定すると manualPagination になる。
462
+ *
463
+ * @example
464
+ * <DataTable pageCount={data.pageCount} … />
465
+ */
119
466
  pageCount?: number
467
+ /**
468
+ * サーバー側ページング時の総件数。指定すると manualPagination になり、ページ数は
469
+ * `rowCount / pageSize` から導かれ、「N件中」もこの値を使う(`pageCount` を同時に渡せばそちらが優先)。
470
+ *
471
+ * @example
472
+ * <DataTable rowCount={1240} pagination={pagination} onPaginationChange={setPagination} … />
473
+ */
474
+ rowCount?: number
475
+ /**
476
+ * true にすると絞り込みを表では行わず、`data` をそのまま使う(サーバー側で絞り込む)。
477
+ *
478
+ * @example
479
+ * <DataTable manualFiltering globalFilter={q} onGlobalFilterChange={setQ} … />
480
+ */
481
+ manualFiltering?: boolean
482
+ /**
483
+ * true にすると並べ替えを表では行わず、`data` の順をそのまま使う(サーバー側で並べ替える)。
484
+ *
485
+ * @example
486
+ * <DataTable manualSorting sorting={sorting} onSortingChange={setSorting} … />
487
+ */
488
+ manualSorting?: boolean
489
+ /**
490
+ * 読み込み中。Content はスケルトン行、Pagination のボタンは無効、表は aria-busy になる。
491
+ *
492
+ * @example
493
+ * <DataTable isLoading={query.isFetching} … />
494
+ */
495
+ isLoading?: boolean
120
496
 
121
497
  // Row selection
122
498
  enableRowSelection?: boolean
@@ -127,6 +503,13 @@ export interface DataTableProps<TData, TValue> {
127
503
  columnVisibility?: VisibilityState
128
504
  onColumnVisibilityChange?: OnChangeFn<VisibilityState>
129
505
 
506
+ /**
507
+ * TanStack Table の `meta`。表全体で共有する値(行を更新する関数など)を渡し、列定義の `cell` から
508
+ * `table.options.meta` で読む。列ごとの書式は列定義の `meta.format` に書く(こちらではない)。
509
+ *
510
+ * @example
511
+ * <DataTable meta={{ updateRow: (id, patch) => mutate({ id, patch }) }} … />
512
+ */
130
513
  meta?: TableMeta<TData>
131
514
  className?: string
132
515
 
@@ -142,6 +525,33 @@ export interface DataTableToolbarProps extends HTMLAttributes<HTMLDivElement> {
142
525
 
143
526
  export interface DataTableContentProps extends HTMLAttributes<HTMLDivElement> {}
144
527
 
528
+ export interface DataTableExportProps {
529
+ /**
530
+ * 保存するファイル名。既定 `"export.csv"`。
531
+ *
532
+ * @example
533
+ * <DataTableExport fileName="sales-2025.csv" />
534
+ */
535
+ fileName?: string
536
+ /**
537
+ * `"display"`(既定)は `meta.format` どおりの表示文字列、`"raw"` は生の値を書き出す。
538
+ *
539
+ * @example
540
+ * <DataTableExport values="raw" />
541
+ */
542
+ values?: CsvValues
543
+ /**
544
+ * `"filtered"`(既定)は並べ替え・絞り込み後の全ページ、`"page"` は表示中のページだけ。
545
+ *
546
+ * @example
547
+ * <DataTableExport scope="page" />
548
+ */
549
+ scope?: CsvScope
550
+ /** ボタンの文言。既定はページの言語で `"CSV を書き出す"`(英語は `"Export CSV"`)。 */
551
+ children?: ReactNode
552
+ className?: string
553
+ }
554
+
145
555
  export interface DataTablePaginationProps extends HTMLAttributes<HTMLDivElement> {
146
556
  pageSizeOptions?: number[]
147
557
  }
@@ -152,40 +562,31 @@ export interface DataTableColumnVisibilityProps {
152
562
  className?: string
153
563
  }
154
564
 
155
- export interface DataTablePresetProps<TData, TValue> {
156
- columns: ColumnDef<TData, TValue>[]
157
- data: TData[]
158
-
159
- enableSorting?: boolean
160
- sorting?: SortingState
161
- onSortingChange?: OnChangeFn<SortingState>
162
-
163
- enableFiltering?: boolean
164
- columnFilters?: ColumnFiltersState
165
- onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>
166
- globalFilter?: string
167
- onGlobalFilterChange?: OnChangeFn<string>
168
-
169
- enablePagination?: boolean
170
- pagination?: PaginationState
171
- onPaginationChange?: OnChangeFn<PaginationState>
172
- pageCount?: number
173
-
174
- enableRowSelection?: boolean
175
- rowSelection?: RowSelectionState
176
- onRowSelectionChange?: OnChangeFn<RowSelectionState>
177
-
565
+ /** The preset takes every `DataTable` prop except `children`, plus a few slots. */
566
+ export type DataTablePresetProps<TData, TValue> = Omit<
567
+ DataTableProps<TData, TValue>,
568
+ "children"
569
+ > & {
178
570
  enableColumnVisibility?: boolean
179
- columnVisibility?: VisibilityState
180
- onColumnVisibilityChange?: OnChangeFn<VisibilityState>
181
571
  columnLabels?: Record<string, string>
572
+ /**
573
+ * true にすると toolbar に CSV 書き出しボタン(`DataTableExport`)を置く。既定 false。
574
+ *
575
+ * @example
576
+ * <DataTablePreset exportable … />
577
+ */
578
+ exportable?: boolean
579
+ /**
580
+ * `exportable` のときの保存ファイル名。既定 `"export.csv"`。
581
+ *
582
+ * @example
583
+ * <DataTablePreset exportable exportFileName="sales.csv" … />
584
+ */
585
+ exportFileName?: string
182
586
 
183
587
  /** Extension slots */
184
588
  toolbar?: ReactNode
185
589
  bulkActions?: (selectedRows: TData[]) => ReactNode
186
-
187
- meta?: TableMeta<TData>
188
- className?: string
189
590
  }
190
591
 
191
592
  /* ------------------------------------------------------------------ helper */
@@ -226,9 +627,12 @@ export function useDataTable<TData>(): DataTableContextValue<TData> {
226
627
 
227
628
  /* -------------------------------------------------------------------- root */
228
629
 
630
+ /** Columns / rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
631
+ const NO_ROWS: never[] = []
632
+
229
633
  export function DataTable<TData, TValue>({
230
- columns,
231
- data,
634
+ columns = NO_ROWS,
635
+ data = NO_ROWS,
232
636
  enableSorting = true,
233
637
  sorting: sortingProp,
234
638
  onSortingChange,
@@ -241,6 +645,10 @@ export function DataTable<TData, TValue>({
241
645
  pagination: paginationProp,
242
646
  onPaginationChange,
243
647
  pageCount,
648
+ rowCount,
649
+ manualFiltering,
650
+ manualSorting,
651
+ isLoading = false,
244
652
  enableRowSelection = false,
245
653
  rowSelection: rowSelectionProp,
246
654
  onRowSelectionChange,
@@ -250,6 +658,8 @@ export function DataTable<TData, TValue>({
250
658
  className,
251
659
  children,
252
660
  }: DataTableProps<TData, TValue>) {
661
+ const locale = useLocale()
662
+ const t = TEXT[uiLanguage(locale)]
253
663
  const [sorting, setSorting] = useControlledState<SortingState>(sortingProp, [], onSortingChange)
254
664
 
255
665
  const [columnFilters, setColumnFilters] = useControlledState<ColumnFiltersState>(
@@ -284,7 +694,29 @@ export function DataTable<TData, TValue>({
284
694
 
285
695
  // Row selection prepends a checkbox column that the caller never declares.
286
696
  const allColumns = useMemo<ColumnDef<TData, TValue>[]>(() => {
287
- if (!enableRowSelection) return columns
697
+ // A column with `meta.format` / `meta.thresholds` and no `cell` gets a formatted, right-aligned
698
+ // cell; thresholds add the tone colour plus a symbol. `meta.dateFormat` gives a left-aligned date.
699
+ // A plain accessor column (no `cell`) shows `nullLabel` / `—` for a missing value.
700
+ const formatted = columns.map((column) => {
701
+ const meta = column.meta
702
+ if (column.cell !== undefined) return column
703
+ const hasAccessor = "accessorKey" in column || "accessorFn" in column
704
+ if (!(meta?.format || meta?.thresholds || meta?.dateFormat) && !hasAccessor) return column
705
+ const thresholds = meta?.thresholds
706
+ const aligned = Boolean(meta?.format || thresholds)
707
+ return {
708
+ ...column,
709
+ cell: ({ row, getValue }) => {
710
+ const value = getValue()
711
+ const text = formatCellText(value, row.original, meta ?? {}, locale)
712
+ if (thresholds) return <ThresholdCell value={value} text={text} thresholds={thresholds} />
713
+ if (aligned) return <div className="text-right tabular-nums">{text}</div>
714
+ if (meta?.dateFormat) return <span className="tabular-nums">{text}</span>
715
+ return text
716
+ },
717
+ } as ColumnDef<TData, TValue>
718
+ })
719
+ if (!enableRowSelection) return formatted
288
720
 
289
721
  const selectionColumn: ColumnDef<TData, TValue> = {
290
722
  id: "_select",
@@ -293,24 +725,40 @@ export function DataTable<TData, TValue>({
293
725
  checked={table.getIsAllPageRowsSelected()}
294
726
  indeterminate={table.getIsSomePageRowsSelected()}
295
727
  onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
296
- aria-label="全て選択"
728
+ aria-label={t.selectAll}
297
729
  />
298
730
  ),
299
731
  cell: ({ row }) => (
300
732
  <Checkbox
301
733
  checked={row.getIsSelected()}
302
734
  onCheckedChange={(checked) => row.toggleSelected(checked)}
303
- aria-label="行を選択"
735
+ aria-label={t.selectRow}
304
736
  />
305
737
  ),
306
738
  enableSorting: false,
307
739
  enableHiding: false,
308
740
  }
309
741
 
310
- return [selectionColumn, ...columns]
311
- }, [columns, enableRowSelection])
742
+ return [selectionColumn, ...formatted]
743
+ }, [columns, enableRowSelection, locale, t])
744
+
745
+ // TanStack's own `autoResetPageIndex` queues `resetPageIndex()` from inside a render-time memo, which
746
+ // can fire before the component has mounted ("Can't perform a React state update on a component that
747
+ // hasn't mounted yet"). Turn it off and send the page back to 1 from an effect instead, so it only
748
+ // runs after mount and only when the sort / filter / data actually changed.
749
+ const autoResetPage = enablePagination && pageCount === undefined && rowCount === undefined
750
+ const resetKey = useRef<readonly unknown[] | null>(null)
751
+ useEffect(() => {
752
+ const next = [data, sorting, columnFilters, globalFilter]
753
+ const prev = resetKey.current
754
+ resetKey.current = next
755
+ if (!autoResetPage || prev === null) return
756
+ if (prev.every((value, i) => value === next[i])) return
757
+ setPagination((old) => (old.pageIndex === 0 ? old : { ...old, pageIndex: 0 }))
758
+ }, [autoResetPage, data, sorting, columnFilters, globalFilter, setPagination])
312
759
 
313
760
  const table = useReactTable({
761
+ autoResetPageIndex: false,
314
762
  data,
315
763
  columns: allColumns,
316
764
  state: {
@@ -329,7 +777,11 @@ export function DataTable<TData, TValue>({
329
777
  onRowSelectionChange: setRowSelection,
330
778
  onColumnVisibilityChange: setColumnVisibility,
331
779
  ...(enablePagination ? { onPaginationChange: setPagination } : {}),
332
- ...(pageCount !== undefined ? { pageCount, manualPagination: true } : {}),
780
+ ...(pageCount !== undefined || rowCount !== undefined
781
+ ? { pageCount, rowCount, manualPagination: true }
782
+ : {}),
783
+ ...(manualFiltering ? { manualFiltering: true } : {}),
784
+ ...(manualSorting ? { manualSorting: true } : {}),
333
785
  getCoreRowModel: getCoreRowModel(),
334
786
  getSortedRowModel: getSortedRowModel(),
335
787
  getFilteredRowModel: getFilteredRowModel(),
@@ -357,6 +809,7 @@ export function DataTable<TData, TValue>({
357
809
  enablePagination,
358
810
  enableRowSelection,
359
811
  visibleColumnCount: allColumns.length,
812
+ isLoading,
360
813
  }),
361
814
  // `useReactTable` returns the same object every render, so each piece of
362
815
  // table state has to be tracked explicitly to re-render the consumers.
@@ -371,6 +824,8 @@ export function DataTable<TData, TValue>({
371
824
  enablePagination,
372
825
  enableRowSelection,
373
826
  allColumns.length,
827
+ isLoading,
828
+ rowCount,
374
829
  sorting,
375
830
  columnFilters,
376
831
  columnVisibility,
@@ -381,7 +836,7 @@ export function DataTable<TData, TValue>({
381
836
 
382
837
  return (
383
838
  <DataTableContext.Provider value={contextValue as DataTableContextValue<unknown>}>
384
- <div data-slot="data-table" className={cn("space-y-4", className)}>
839
+ <div data-slot="data-table" aria-busy={isLoading || undefined} className={cn("space-y-4", className)}>
385
840
  {children}
386
841
  </div>
387
842
  </DataTableContext.Provider>
@@ -389,6 +844,76 @@ export function DataTable<TData, TValue>({
389
844
  }
390
845
  DataTable.displayName = "DataTable"
391
846
 
847
+ /* ------------------------------------------------------ sparkline column */
848
+
849
+ /** Fewest points a row needs for a trend (one point has no direction). */
850
+ const SPARKLINE_MIN_POINTS = 2
851
+
852
+ export interface SparklineColumnOptions<TData> {
853
+ /** 列 ID(CSV・列の表示切り替えで使う)。 */
854
+ id: string
855
+ /** 見出し。 */
856
+ header: string
857
+ /**
858
+ * 行の数列を返す(古い順。例: 直近 8 週の売上)。null / undefined / 2 点未満の行は「—」。
859
+ * NaN などの有限でない値は取り除いてから数える。
860
+ */
861
+ values: (row: TData) => readonly (number | null | undefined)[] | null | undefined
862
+ /** スクリーンリーダー向けの数値の書式(`meta.format` と同じ語彙)。既定は `"number"`。 */
863
+ format?: ValueFormat
864
+ }
865
+
866
+ /**
867
+ * 行の数列をセルの小さなトレンド(Sparkline)にする列。色は `--chart-1` 固定、軸・ツールチップ・ラベルなし。
868
+ * 図は読み上げず、同じセルの sr-only に「N 点、最初 → 最後、最小、最大」を書式付きで添える。
869
+ * 数列の並べ替えは意味が無いので無効。DataTable 本体の props は増やさず、`columns` に置くだけで使う。
870
+ *
871
+ * @example
872
+ * const columns: ColumnDef<Store>[] = [
873
+ * { accessorKey: "store", header: "店舗" },
874
+ * sparklineColumn<Store>({ id: "trend", header: "直近 8 週", values: (r) => r.weekly, format: "currency" }),
875
+ * ]
876
+ */
877
+ export function sparklineColumn<TData>({
878
+ id,
879
+ header,
880
+ values,
881
+ format = "number",
882
+ }: SparklineColumnOptions<TData>): ColumnDef<TData, unknown> {
883
+ return {
884
+ id,
885
+ header,
886
+ accessorFn: (row) => values(row) ?? null,
887
+ enableSorting: false,
888
+ cell: ({ getValue }) => {
889
+ const raw = getValue() as readonly (number | null | undefined)[] | null
890
+ const series = (raw ?? []).filter((v): v is number => typeof v === "number" && Number.isFinite(v))
891
+ if (series.length < SPARKLINE_MIN_POINTS) return <span className="text-muted-foreground">{EMPTY_VALUE}</span>
892
+ return (
893
+ <div data-slot="data-table-sparkline-cell" className="flex h-5 items-center">
894
+ <div aria-hidden="true" className="h-5 w-24 shrink-0">
895
+ <Sparkline data={series.map((value) => ({ value }))} height={20} />
896
+ </div>
897
+ <SparklineSummary series={series} format={format} />
898
+ </div>
899
+ )
900
+ },
901
+ }
902
+ }
903
+
904
+ /** The sparkline cell's screen-reader text, in the page's language. */
905
+ function SparklineSummary({ series, format }: { series: number[]; format: ValueFormat }) {
906
+ const locale = useLocale()
907
+ const f = (v: number) => formatValue(v, format, locale)
908
+ const first = series[0] as number
909
+ const last = series[series.length - 1] as number
910
+ return (
911
+ <span className="sr-only">
912
+ {TEXT[uiLanguage(locale)].trend(series.length, f(first), f(last), f(Math.min(...series)), f(Math.max(...series)))}
913
+ </span>
914
+ )
915
+ }
916
+
392
917
  /* ----------------------------------------------------------------- toolbar */
393
918
 
394
919
  export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps>(
@@ -431,63 +956,200 @@ DataTableToolbar.displayName = "DataTableToolbar"
431
956
 
432
957
  /* ----------------------------------------------------------------- content */
433
958
 
959
+ /**
960
+ * `meta.priority` → the classes that hide the column below a container width (`@container` on the content wrapper).
961
+ * The class strings are literals so Tailwind picks them up.
962
+ */
963
+ const PRIORITY_CLASS: Record<number, string> = {
964
+ 1: "hidden @min-[640px]:table-cell",
965
+ 2: "hidden @min-[520px]:table-cell",
966
+ 3: "hidden @min-[400px]:table-cell",
967
+ }
968
+
969
+ /**
970
+ * A `meta.comparison` column is the last tier: it stays as its own column down to 320px, below which it is hidden and its
971
+ * content is stacked under the value cell instead (`COMPARISON_STACK_CLASS`, visible only below the same width).
972
+ */
973
+ const COMPARISON_CLASS = "hidden @min-[320px]:table-cell"
974
+ const COMPARISON_STACK_CLASS = "@min-[320px]:hidden"
975
+
976
+ /** A column's hide-on-narrow classes: none without a priority (or at 4 and above), the lowest tier for anything below 1. */
977
+ export function priorityClassName(priority: number | undefined): string | undefined {
978
+ if (priority === undefined || !Number.isFinite(priority) || priority >= 4) return undefined
979
+ return PRIORITY_CLASS[Math.max(1, Math.floor(priority))]
980
+ }
981
+
982
+ type CellMeta = {
983
+ className?: string
984
+ format?: unknown
985
+ thresholds?: unknown
986
+ priority?: number
987
+ comparison?: boolean
988
+ align?: "left" | "right"
989
+ }
990
+
991
+ /** `meta.align` wins; otherwise numeric columns (`format` / `thresholds`) and Delta (`comparison`) columns are right-aligned, header included. */
992
+ function isRightAligned(meta: CellMeta | undefined): boolean {
993
+ if (meta?.align) return meta.align === "right"
994
+ return Boolean(meta?.format || meta?.thresholds || meta?.comparison)
995
+ }
996
+
997
+ /**
998
+ * A column that carries a stacked comparison (`isHost`) never hides, and a comparison column ignores its own
999
+ * `priority` and takes the last tier.
1000
+ */
1001
+ function columnClassName(meta: CellMeta | undefined, options: { isComparison?: boolean; isHost?: boolean } = {}) {
1002
+ const tier = options.isComparison
1003
+ ? COMPARISON_CLASS
1004
+ : options.isHost
1005
+ ? undefined
1006
+ : priorityClassName(meta?.priority)
1007
+ return cn(tier, meta?.className, isRightAligned(meta) ? "text-right tabular-nums" : undefined) || undefined
1008
+ }
1009
+
1010
+ /**
1011
+ * Pair every `meta.comparison` column with the visible column right before it (the value it compares).
1012
+ * A comparison column with nothing before it stays an ordinary column.
1013
+ */
1014
+ function pairComparisons<TData>(columns: Column<TData, unknown>[]) {
1015
+ const comparisonOf = new Map<string, Column<TData, unknown>>() // host column id → its comparison column
1016
+ const comparisonIds = new Set<string>()
1017
+ columns.forEach((column, i) => {
1018
+ if (i > 0 && column.columnDef.meta?.comparison && !columns[i - 1]!.columnDef.meta?.comparison) {
1019
+ comparisonOf.set(columns[i - 1]!.id, column)
1020
+ comparisonIds.add(column.id)
1021
+ }
1022
+ })
1023
+ return { comparisonOf, comparisonIds }
1024
+ }
1025
+
1026
+ const SORT_ICON_CLASS = "size-4 shrink-0"
1027
+
1028
+ /** The sort icon: always shown on the sorted column, only on hover / keyboard focus on the others. */
1029
+ function SortIcon({ sorted }: { sorted: false | "asc" | "desc" }) {
1030
+ if (sorted === "asc") return <ArrowUp className={SORT_ICON_CLASS} aria-hidden="true" />
1031
+ if (sorted === "desc") return <ArrowDown className={SORT_ICON_CLASS} aria-hidden="true" />
1032
+ return (
1033
+ <ArrowUpDown
1034
+ className={cn(
1035
+ SORT_ICON_CLASS,
1036
+ "text-muted-foreground opacity-0 group-hover/sort:opacity-100 group-focus-visible/sort:opacity-100",
1037
+ )}
1038
+ aria-hidden="true"
1039
+ />
1040
+ )
1041
+ }
1042
+
434
1043
  export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps>(
435
1044
  ({ className, ...props }, ref) => {
436
- const { table, visibleColumnCount } = useDataTable()
1045
+ const { table, visibleColumnCount, isLoading, enablePagination } = useDataTable()
1046
+ const t = useText()
1047
+ const { comparisonOf, comparisonIds } = pairComparisons(table.getVisibleLeafColumns())
437
1048
 
438
1049
  return (
439
1050
  <div
440
1051
  ref={ref}
441
1052
  data-slot="data-table-content"
442
- className={cn("overflow-x-auto", className)}
1053
+ aria-busy={isLoading || undefined}
1054
+ className={cn("@container overflow-x-auto", className)}
443
1055
  {...props}
444
1056
  >
445
- <Table>
1057
+ <Table className="[&_th:first-child]:pl-0 [&_td:first-child]:pl-0 [&_th:last-child]:pr-0 [&_td:last-child]:pr-0">
446
1058
  <TableHeader>
447
1059
  {table.getHeaderGroups().map((headerGroup) => (
448
1060
  <TableRow key={headerGroup.id}>
449
- {headerGroup.headers.map((header) => (
450
- <TableHead key={header.id}>
451
- {header.isPlaceholder ? null : header.column.getCanSort() ? (
452
- <button
453
- type="button"
454
- className="inline-flex items-center gap-1 rounded hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none"
455
- onClick={header.column.getToggleSortingHandler()}
456
- aria-sort={
457
- header.column.getIsSorted() === "asc"
1061
+ {headerGroup.headers.map((header) => {
1062
+ const column = header.column
1063
+ const meta = column.columnDef.meta
1064
+ const sorted = column.getIsSorted()
1065
+ const canSort = column.getCanSort()
1066
+ const label = header.isPlaceholder ? null : flexRender(column.columnDef.header, header.getContext())
1067
+ return (
1068
+ <TableHead
1069
+ key={header.id}
1070
+ className={columnClassName(meta, {
1071
+ isComparison: comparisonIds.has(column.id),
1072
+ isHost: comparisonOf.has(column.id),
1073
+ })}
1074
+ aria-sort={
1075
+ canSort && !header.isPlaceholder
1076
+ ? sorted === "asc"
458
1077
  ? "ascending"
459
- : header.column.getIsSorted() === "desc"
1078
+ : sorted === "desc"
460
1079
  ? "descending"
461
1080
  : "none"
462
- }
463
- >
464
- {flexRender(header.column.columnDef.header, header.getContext())}
465
- {header.column.getIsSorted() === "asc" ? (
466
- <ArrowUp className="size-4" />
467
- ) : header.column.getIsSorted() === "desc" ? (
468
- <ArrowDown className="size-4" />
469
- ) : (
470
- <ArrowUpDown className="size-4 text-muted-foreground" />
471
- )}
472
- </button>
473
- ) : (
474
- flexRender(header.column.columnDef.header, header.getContext())
475
- )}
476
- </TableHead>
477
- ))}
1081
+ : undefined
1082
+ }
1083
+ >
1084
+ {header.isPlaceholder ? null : canSort ? (
1085
+ <button
1086
+ type="button"
1087
+ className={cn(
1088
+ "group/sort relative items-center gap-1 rounded leading-5 after:absolute after:inset-x-0 after:top-1/2 after:h-6 after:-translate-y-1/2 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none",
1089
+ isRightAligned(meta) ? "flex w-full justify-end" : "inline-flex",
1090
+ )}
1091
+ onClick={column.getToggleSortingHandler()}
1092
+ >
1093
+ {isRightAligned(meta) && <SortIcon sorted={sorted} />}
1094
+ {label}
1095
+ {!isRightAligned(meta) && <SortIcon sorted={sorted} />}
1096
+ </button>
1097
+ ) : (
1098
+ label
1099
+ )}
1100
+ </TableHead>
1101
+ )
1102
+ })}
478
1103
  </TableRow>
479
1104
  ))}
480
1105
  </TableHeader>
481
1106
 
482
1107
  <TableBody>
483
- {table.getRowModel().rows.length ? (
1108
+ {isLoading ? (
1109
+ Array.from({ length: enablePagination ? table.getState().pagination.pageSize : 5 }).map(
1110
+ (_, i) => (
1111
+ <TableRow key={i} data-slot="data-table-skeleton-row">
1112
+ {Array.from({ length: visibleColumnCount }).map((__, j) => (
1113
+ <TableCell key={j}>
1114
+ <Skeleton className="h-4 w-full" />
1115
+ </TableCell>
1116
+ ))}
1117
+ </TableRow>
1118
+ ),
1119
+ )
1120
+ ) : table.getRowModel().rows.length ? (
484
1121
  table.getRowModel().rows.map((row) => (
485
1122
  <TableRow key={row.id} data-state={row.getIsSelected() ? "selected" : undefined}>
486
- {row.getVisibleCells().map((cell) => (
487
- <TableCell key={cell.id}>
488
- {flexRender(cell.column.columnDef.cell, cell.getContext())}
489
- </TableCell>
490
- ))}
1123
+ {row.getVisibleCells().map((cell) => {
1124
+ const meta = cell.column.columnDef.meta
1125
+ const comparisonColumn = comparisonOf.get(cell.column.id)
1126
+ const comparisonCell = comparisonColumn
1127
+ ? row.getVisibleCells().find((c) => c.column.id === comparisonColumn.id)
1128
+ : undefined
1129
+ return (
1130
+ <TableCell
1131
+ key={cell.id}
1132
+ className={columnClassName(meta, {
1133
+ isComparison: comparisonIds.has(cell.column.id),
1134
+ isHost: comparisonOf.has(cell.column.id),
1135
+ })}
1136
+ >
1137
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
1138
+ {comparisonCell && (
1139
+ <div
1140
+ data-slot="data-table-stacked-comparison"
1141
+ className={cn(
1142
+ COMPARISON_STACK_CLASS,
1143
+ "mt-0.5 text-xs [&_[data-slot=delta]]:text-xs",
1144
+ isRightAligned(meta) ? "text-right" : undefined,
1145
+ )}
1146
+ >
1147
+ {flexRender(comparisonCell.column.columnDef.cell, comparisonCell.getContext())}
1148
+ </div>
1149
+ )}
1150
+ </TableCell>
1151
+ )
1152
+ })}
491
1153
  </TableRow>
492
1154
  ))
493
1155
  ) : (
@@ -496,7 +1158,7 @@ export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps
496
1158
  colSpan={visibleColumnCount}
497
1159
  className="h-24 text-center text-muted-foreground"
498
1160
  >
499
- データがありません
1161
+ {t.noData}
500
1162
  </TableCell>
501
1163
  </TableRow>
502
1164
  )}
@@ -512,37 +1174,46 @@ DataTableContent.displayName = "DataTableContent"
512
1174
 
513
1175
  export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginationProps>(
514
1176
  ({ pageSizeOptions = [10, 20, 50], className, ...props }, ref) => {
515
- const { table } = useDataTable()
1177
+ const { table, isLoading } = useDataTable()
1178
+ const t = useText()
516
1179
 
517
- const totalRows = table.getFilteredRowModel().rows.length
1180
+ const totalRows =
1181
+ table.options.rowCount !== undefined
1182
+ ? table.getRowCount()
1183
+ : table.getFilteredRowModel().rows.length
518
1184
  const currentPageSize = table.getState().pagination.pageSize
519
1185
  const currentPageIndex = table.getState().pagination.pageIndex
520
1186
  const rangeStart = currentPageIndex * currentPageSize + 1
521
1187
  const rangeEnd = Math.min((currentPageIndex + 1) * currentPageSize, totalRows)
1188
+ // One list feeds both the menu and `items`, so the trigger shows "10 / page" (ja: "10件")
1189
+ // rather than the raw "10" under Base UI's SelectValue.
1190
+ const pageSizeItems = pageSizeOptions.map((size) => ({
1191
+ value: String(size),
1192
+ label: t.pageSize(size),
1193
+ }))
522
1194
 
523
1195
  return (
524
1196
  <div
525
1197
  ref={ref}
526
1198
  data-slot="data-table-pagination"
1199
+ aria-busy={isLoading || undefined}
527
1200
  className={cn("flex flex-wrap items-center justify-between gap-4", className)}
528
1201
  {...props}
529
1202
  >
530
1203
  <div className="flex items-center gap-2 text-sm text-muted-foreground">
531
- <span>
532
- {totalRows}件中 {rangeStart} - {rangeEnd} を表示
533
- </span>
1204
+ <span>{t.range(totalRows, rangeStart, rangeEnd)}</span>
534
1205
  <Select
535
- items={Object.fromEntries(pageSizeOptions.map((size) => [String(size), `${size}件`]))}
1206
+ items={pageSizeItems}
536
1207
  value={String(currentPageSize)}
537
1208
  onValueChange={(value) => table.setPageSize(Number(value))}
538
1209
  >
539
- <SelectTrigger size="sm" className="w-[80px]">
1210
+ <SelectTrigger size="sm" className="min-w-[80px]">
540
1211
  <SelectValue />
541
1212
  </SelectTrigger>
542
1213
  <SelectContent>
543
- {pageSizeOptions.map((size) => (
544
- <SelectItem key={size} value={String(size)}>
545
- {size}件
1214
+ {pageSizeItems.map((item) => (
1215
+ <SelectItem key={item.value} value={item.value}>
1216
+ {item.label}
546
1217
  </SelectItem>
547
1218
  ))}
548
1219
  </SelectContent>
@@ -554,33 +1225,33 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
554
1225
  variant="outline"
555
1226
  size="sm"
556
1227
  onClick={() => table.firstPage()}
557
- disabled={!table.getCanPreviousPage()}
1228
+ disabled={isLoading || !table.getCanPreviousPage()}
558
1229
  >
559
- 最初
1230
+ {t.first}
560
1231
  </Button>
561
1232
  <Button
562
1233
  variant="outline"
563
1234
  size="sm"
564
1235
  onClick={() => table.previousPage()}
565
- disabled={!table.getCanPreviousPage()}
1236
+ disabled={isLoading || !table.getCanPreviousPage()}
566
1237
  >
567
- 前へ
1238
+ {t.previous}
568
1239
  </Button>
569
1240
  <Button
570
1241
  variant="outline"
571
1242
  size="sm"
572
1243
  onClick={() => table.nextPage()}
573
- disabled={!table.getCanNextPage()}
1244
+ disabled={isLoading || !table.getCanNextPage()}
574
1245
  >
575
- 次へ
1246
+ {t.next}
576
1247
  </Button>
577
1248
  <Button
578
1249
  variant="outline"
579
1250
  size="sm"
580
1251
  onClick={() => table.lastPage()}
581
- disabled={!table.getCanNextPage()}
1252
+ disabled={isLoading || !table.getCanNextPage()}
582
1253
  >
583
- 最後
1254
+ {t.last}
584
1255
  </Button>
585
1256
  </div>
586
1257
  </div>
@@ -596,6 +1267,7 @@ export const DataTableColumnVisibility = forwardRef<
596
1267
  DataTableColumnVisibilityProps
597
1268
  >(({ columnLabels, className }, ref) => {
598
1269
  const { table } = useDataTable()
1270
+ const t = useText()
599
1271
 
600
1272
  const hideableColumns = table.getAllColumns().filter((column) => column.getCanHide())
601
1273
 
@@ -607,8 +1279,9 @@ export const DataTableColumnVisibility = forwardRef<
607
1279
  ref={ref}
608
1280
  render={<Button variant="outline" size="sm" className={cn(className)} />}
609
1281
  data-slot="data-table-column-visibility"
1282
+ aria-label={t.columns}
610
1283
  >
611
- <Columns3 className="size-4" />
1284
+ <Columns3 className="size-4" aria-hidden="true" />
612
1285
  </DropdownMenuTrigger>
613
1286
  <DropdownMenuContent align="end" className="min-w-[10rem]">
614
1287
  {hideableColumns.map((column) => {
@@ -631,75 +1304,143 @@ export const DataTableColumnVisibility = forwardRef<
631
1304
  })
632
1305
  DataTableColumnVisibility.displayName = "DataTableColumnVisibility"
633
1306
 
1307
+ /* ------------------------------------------------------------------ export */
1308
+
1309
+ export type CsvCell = string | number | null | undefined
1310
+ export type CsvValues = "display" | "raw"
1311
+ export type CsvScope = "filtered" | "page"
1312
+
1313
+ // A signed number with an optional short currency / unit ("-¥1,200", "+530万", "-12.5%").
1314
+ const SIGNED_NUMBER = /^[+-]\s?[^\d\s=+\-@]{0,3}\d[\d,.]*[^\d\s=+\-@]{0,4}$/
1315
+
1316
+ /**
1317
+ * CSV 文字列を作る純関数(RFC 4180: CRLF、`,` `"` 改行を含む欄は `"` で囲み `"` は二重化)。
1318
+ * 既定で BOM 付き UTF-8(Excel で文字化けしない)。`guardFormulas` が true の間、先頭が
1319
+ * `= + - @` の文字列欄には `'` を付けて数式として実行されないようにする(数値と符号付きの数値表記は除く)。
1320
+ *
1321
+ * @example
1322
+ * toCsv(["月", "売上"], [["2025-01", 1200], ["2025-02", null]])
1323
+ * // "\uFEFF月,売上\r\n2025-01,1200\r\n2025-02,"
1324
+ */
1325
+ export function toCsv(
1326
+ header: string[],
1327
+ rows: CsvCell[][],
1328
+ options: { bom?: boolean; guardFormulas?: boolean } = {},
1329
+ ): string {
1330
+ const { bom = true, guardFormulas = false } = options
1331
+ const field = (cell: CsvCell, guard: boolean): string => {
1332
+ let text = cell === null || cell === undefined ? "" : String(cell)
1333
+ if (guard && typeof cell === "string" && /^[=+\-@]/.test(text) && !SIGNED_NUMBER.test(text)) {
1334
+ text = `'${text}`
1335
+ }
1336
+ return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
1337
+ }
1338
+ const lines = [
1339
+ header.map((h) => field(h, false)).join(","),
1340
+ ...rows.map((row) => row.map((cell) => field(cell, guardFormulas)).join(",")),
1341
+ ]
1342
+ return (bom ? "\uFEFF" : "") + lines.join("\r\n")
1343
+ }
1344
+
1345
+ /**
1346
+ * 表の現在の状態から CSV を作る。行は画面と同じ並べ替え・絞り込み後の順、列は表示中の列のみ。
1347
+ * `"display"` の書式のロケールは `locale`(既定はページのロケール、`<html lang>`)。
1348
+ *
1349
+ * @example
1350
+ * const csv = tableToCsv(table, { values: "raw", scope: "page" })
1351
+ */
1352
+ export function tableToCsv<TData>(
1353
+ table: TanstackTable<TData>,
1354
+ { values = "display", scope = "filtered", locale: localeArg }: { values?: CsvValues; scope?: CsvScope; locale?: string } = {},
1355
+ ): string {
1356
+ const locale = resolveLocale(localeArg)
1357
+ const columns = table.getVisibleLeafColumns().filter((column) => column.id !== "_select")
1358
+ const rows = scope === "page" ? table.getRowModel().rows : table.getSortedRowModel().rows
1359
+ const header = columns.map((column) =>
1360
+ typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
1361
+ )
1362
+ const body = rows.map((row) =>
1363
+ columns.map((column): CsvCell => {
1364
+ const value = row.getValue(column.id)
1365
+ const meta = column.columnDef.meta
1366
+ if (values === "display" && (meta?.format || meta?.dateFormat)) return formatCellText(value, row.original, meta, locale)
1367
+ if (value === null || value === undefined) return ""
1368
+ if (typeof value === "number" || typeof value === "string") return value
1369
+ if (value instanceof Date) return value.toISOString()
1370
+ return String(value)
1371
+ }),
1372
+ )
1373
+ return toCsv(header, body, { guardFormulas: values === "display" })
1374
+ }
1375
+
1376
+ /** toolbar に置く CSV 書き出しボタン。`useDataTable()` の table から画面と同じ順・同じ集合を書き出す。 */
1377
+ export function DataTableExport({
1378
+ fileName = "export.csv",
1379
+ values = "display",
1380
+ scope = "filtered",
1381
+ children,
1382
+ className,
1383
+ }: DataTableExportProps) {
1384
+ const { table, isLoading } = useDataTable()
1385
+ const locale = useLocale()
1386
+
1387
+ const onClick = () => {
1388
+ const csv = tableToCsv(table, { values, scope, locale })
1389
+ const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }))
1390
+ const a = document.createElement("a")
1391
+ a.href = url
1392
+ a.download = fileName
1393
+ a.click()
1394
+ URL.revokeObjectURL(url)
1395
+ }
1396
+
1397
+ return (
1398
+ <Button
1399
+ type="button"
1400
+ variant="outline"
1401
+ size="sm"
1402
+ className={className}
1403
+ data-slot="data-table-export"
1404
+ onClick={onClick}
1405
+ disabled={isLoading}
1406
+ >
1407
+ <Download className="size-4" aria-hidden="true" />
1408
+ {children ?? TEXT[uiLanguage(locale)].exportCsv}
1409
+ </Button>
1410
+ )
1411
+ }
1412
+ DataTableExport.displayName = "DataTableExport"
1413
+
634
1414
  /* ------------------------------------------------------------------ preset */
635
1415
 
636
1416
  /** The usual arrangement — toolbar, body and pagination — in one component. */
637
1417
  export function DataTablePreset<TData, TValue>({
638
- columns,
639
- data,
640
- enableSorting,
641
- sorting,
642
- onSortingChange,
643
- enableFiltering,
644
- columnFilters,
645
- onColumnFiltersChange,
646
- globalFilter,
647
- onGlobalFilterChange,
648
- enablePagination,
649
- pagination,
650
- onPaginationChange,
651
- pageCount,
652
- enableRowSelection,
653
- rowSelection,
654
- onRowSelectionChange,
655
1418
  enableColumnVisibility = false,
656
- columnVisibility,
657
- onColumnVisibilityChange,
658
1419
  columnLabels,
1420
+ exportable = false,
1421
+ exportFileName,
659
1422
  toolbar,
660
1423
  bulkActions,
661
- meta,
662
- className,
1424
+ ...tableProps
663
1425
  }: DataTablePresetProps<TData, TValue>) {
664
1426
  const toolbarSlot =
665
- toolbar || enableColumnVisibility ? (
1427
+ toolbar || enableColumnVisibility || exportable ? (
666
1428
  <>
667
1429
  {toolbar}
1430
+ {exportable && <DataTableExport fileName={exportFileName} />}
668
1431
  {enableColumnVisibility && <DataTableColumnVisibility columnLabels={columnLabels} />}
669
1432
  </>
670
1433
  ) : undefined
671
1434
 
672
1435
  return (
673
- <DataTable<TData, TValue>
674
- columns={columns}
675
- data={data}
676
- enableSorting={enableSorting}
677
- sorting={sorting}
678
- onSortingChange={onSortingChange}
679
- enableFiltering={enableFiltering}
680
- columnFilters={columnFilters}
681
- onColumnFiltersChange={onColumnFiltersChange}
682
- globalFilter={globalFilter}
683
- onGlobalFilterChange={onGlobalFilterChange}
684
- enablePagination={enablePagination}
685
- pagination={pagination}
686
- onPaginationChange={onPaginationChange}
687
- pageCount={pageCount}
688
- enableRowSelection={enableRowSelection}
689
- rowSelection={rowSelection}
690
- onRowSelectionChange={onRowSelectionChange}
691
- columnVisibility={columnVisibility}
692
- onColumnVisibilityChange={onColumnVisibilityChange}
693
- meta={meta}
694
- className={className}
695
- >
1436
+ <DataTable<TData, TValue> {...tableProps}>
696
1437
  <DataTableToolbar
697
- showSearch={enableFiltering}
1438
+ showSearch={tableProps.enableFiltering}
698
1439
  toolbar={toolbarSlot}
699
1440
  bulkActions={bulkActions as ((selectedRows: unknown[]) => ReactNode) | undefined}
700
1441
  />
701
1442
  <DataTableContent />
702
- {enablePagination && <DataTablePagination />}
1443
+ {tableProps.enablePagination && <DataTablePagination />}
703
1444
  </DataTable>
704
1445
  )
705
1446
  }