@squadbase/vantage 0.6.1 → 0.6.2-dev1.0010f67

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