@squadbase/vantage 0.6.2-dev1.b332e5a → 0.6.2-dev1.d7c0911
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/add-4GHF2TX2.js +6 -0
- package/dist/add-4GHF2TX2.js.map +1 -0
- package/dist/{build-TVKFIVFQ.js → build-X5NVCQKT.js} +7 -7
- package/dist/{build-TVKFIVFQ.js.map → build-X5NVCQKT.js.map} +1 -1
- package/dist/{check-3KHZXZMQ.js → check-ZBIH422T.js} +5 -5
- package/dist/{check-3KHZXZMQ.js.map → check-ZBIH422T.js.map} +1 -1
- package/dist/chunk-2ICNHUG6.js +1820 -0
- package/dist/chunk-2ICNHUG6.js.map +1 -0
- package/dist/{chunk-5ZOP4SQR.js → chunk-2N72FKLB.js} +12 -7
- package/dist/chunk-2N72FKLB.js.map +1 -0
- package/dist/{chunk-P7W4LR3Z.js → chunk-7DZGJQ3P.js} +3 -3
- package/dist/{chunk-P7W4LR3Z.js.map → chunk-7DZGJQ3P.js.map} +1 -1
- package/dist/{chunk-U2M6HGKQ.js → chunk-AABMKHEU.js} +4 -4
- package/dist/{chunk-U2M6HGKQ.js.map → chunk-AABMKHEU.js.map} +1 -1
- package/dist/{chunk-QI3XQKHS.js → chunk-GNXCX4WE.js} +10 -3
- package/dist/chunk-GNXCX4WE.js.map +1 -0
- package/dist/{chunk-PXRX5PEC.js → chunk-PDOBHMUQ.js} +3 -3
- package/dist/chunk-PDOBHMUQ.js.map +1 -0
- package/dist/{chunk-ZVANIULZ.js → chunk-RNTDBX6S.js} +4 -4
- package/dist/{chunk-ZVANIULZ.js.map → chunk-RNTDBX6S.js.map} +1 -1
- package/dist/{chunk-A2UUGASH.js → chunk-U736PVHY.js} +2 -2
- package/dist/chunk-U736PVHY.js.map +1 -0
- package/dist/{chunk-PVTHHV7X.js → chunk-V3LF7BE7.js} +27 -5
- package/dist/chunk-V3LF7BE7.js.map +1 -0
- package/dist/{chunk-LMUACBMA.js → chunk-WAMZWZJB.js} +53 -8
- package/dist/chunk-WAMZWZJB.js.map +1 -0
- package/dist/chunk-WRSX2MDK.js +7866 -0
- package/dist/chunk-WRSX2MDK.js.map +1 -0
- package/dist/{chunk-RPZAALCX.js → chunk-XPQSTZUU.js} +254 -164
- package/dist/chunk-XPQSTZUU.js.map +1 -0
- package/dist/{chunk-DTDVSFRY.js → chunk-XQUSGIDG.js} +17 -3
- package/dist/chunk-XQUSGIDG.js.map +1 -0
- package/dist/cli.js +12 -11
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +32 -3
- package/dist/client/index.js +205 -12
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +1975 -28
- package/dist/components/index.js +4 -4
- package/dist/{dev-GDKATCC7.js → dev-4XEDTELV.js} +8 -8
- package/dist/dev-4XEDTELV.js.map +1 -0
- package/dist/doc/index.js +2 -2
- package/dist/doc/index.js.map +1 -1
- package/dist/{docs-EJJ3BECT.js → docs-JVP4RPKT.js} +3 -3
- package/dist/{docs-EJJ3BECT.js.map → docs-JVP4RPKT.js.map} +1 -1
- package/dist/{doctor-WQXPXP6K.js → doctor-5E2VEZPK.js} +5 -5
- package/dist/{doctor-WQXPXP6K.js.map → doctor-5E2VEZPK.js.map} +1 -1
- package/dist/markdown/index.js +1 -1
- package/dist/markdown/index.js.map +1 -1
- package/dist/{preview-OGMCNQUO.js → preview-T6CLSQVX.js} +3 -3
- package/dist/{preview-OGMCNQUO.js.map → preview-T6CLSQVX.js.map} +1 -1
- package/dist/query/index.d.ts +0 -11
- package/dist/query/index.js +2 -2
- package/dist/{routes-MWBWWOJ2.js → routes-3SGAFDMC.js} +4 -4
- package/dist/{routes-MWBWWOJ2.js.map → routes-3SGAFDMC.js.map} +1 -1
- package/dist/{search-6Y422SV6.js → search-5LC4XPB5.js} +3 -3
- package/dist/{search-6Y422SV6.js.map → search-5LC4XPB5.js.map} +1 -1
- package/dist/table/index.js +10 -9
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +144 -11
- package/dist/ui/index.js +1339 -87
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-HCANX4GQ.js → upgrade-JHXZBGQS.js} +6 -6
- package/dist/{upgrade-HCANX4GQ.js.map → upgrade-JHXZBGQS.js.map} +1 -1
- package/dist/vite/index.d.ts +10 -0
- package/dist/vite/index.js +3 -3
- package/docs/en/components.md +29 -15
- package/docs/en/parts/data-table.md +22 -0
- package/docs/en/parts/delta.md +50 -0
- package/docs/en/parts/format.md +43 -0
- package/docs/en/parts/kpi-card.md +57 -0
- package/docs/en/parts/metric-value.md +3 -2
- package/docs/en/parts/sparkline.md +1 -1
- package/docs/en/parts/trend-indicator.md +17 -1
- package/docs/en/ui-and-theming.md +1 -1
- package/docs/index.json +366 -76
- package/docs/ja/changelog.md +31 -0
- package/docs/ja/components.md +57 -101
- package/docs/ja/page-types/csv.md +83 -0
- package/docs/ja/page-types/doc.md +83 -0
- package/docs/ja/page-types/excel.md +82 -0
- package/docs/ja/page-types/slide.md +132 -0
- package/docs/ja/page-types.md +47 -0
- package/docs/ja/parts/area-chart.md +61 -0
- package/docs/ja/parts/bar-chart.md +69 -0
- package/docs/ja/parts/bar-list.md +35 -0
- package/docs/ja/parts/category-bar.md +36 -0
- package/docs/ja/parts/data-table.md +22 -0
- package/docs/ja/parts/delta.md +50 -0
- package/docs/ja/parts/donut-chart.md +47 -0
- package/docs/ja/parts/echart.md +3 -0
- package/docs/ja/parts/format.md +44 -0
- package/docs/ja/parts/kpi-card.md +55 -0
- package/docs/ja/parts/line-chart.md +58 -0
- package/docs/ja/parts/metric-value.md +3 -2
- package/docs/ja/parts/progress-circle.md +34 -0
- package/docs/ja/parts/sparkline.md +2 -2
- package/docs/ja/parts/tracker.md +32 -0
- package/docs/ja/parts/trend-indicator.md +17 -1
- package/docs/ja/routing.md +8 -86
- package/docs/ja/ui/alert-dialog.md +41 -0
- package/docs/ja/ui/avatar.md +31 -0
- package/docs/ja/ui/combobox.md +74 -0
- package/docs/ja/ui/drawer.md +41 -0
- package/docs/ja/ui/field.md +42 -0
- package/docs/ja/ui/hover-card.md +21 -0
- package/docs/ja/ui/kbd.md +16 -0
- package/docs/ja/ui/pagination.md +36 -0
- package/docs/ja/ui/radio-group.md +21 -0
- package/docs/ja/ui/toast.md +46 -0
- package/docs/ja/ui-and-theming.md +14 -7
- package/package.json +13 -4
- package/registry/ui/accordion.tsx +76 -0
- package/registry/ui/alert-dialog.tsx +189 -0
- package/registry/ui/alert.tsx +80 -0
- package/registry/ui/app-shell.tsx +287 -0
- package/registry/ui/area-chart.tsx +3639 -0
- package/registry/ui/avatar.tsx +110 -0
- package/registry/ui/badge.tsx +56 -0
- package/registry/ui/bar-chart.tsx +3636 -0
- package/registry/ui/bar-list.tsx +3680 -0
- package/registry/ui/breadcrumb.tsx +128 -0
- package/registry/ui/button.tsx +62 -0
- package/registry/ui/calendar.tsx +223 -0
- package/registry/ui/card.tsx +107 -0
- package/registry/ui/category-bar.tsx +1255 -0
- package/registry/ui/chart-frame.tsx +1358 -0
- package/registry/ui/checkbox.tsx +30 -0
- package/registry/ui/collapsible.tsx +23 -0
- package/registry/ui/combobox.tsx +310 -0
- package/registry/ui/command.tsx +198 -0
- package/registry/ui/dashboard-card.tsx +217 -0
- package/registry/ui/data-table.tsx +693 -146
- package/registry/ui/date-range-picker.tsx +390 -0
- package/registry/ui/delta.tsx +188 -0
- package/registry/ui/dialog.tsx +161 -0
- package/registry/ui/donut-chart.tsx +3688 -0
- package/registry/ui/drawer.tsx +229 -0
- package/registry/ui/dropdown-menu.tsx +274 -0
- package/registry/ui/echart.tsx +890 -0
- package/registry/ui/field.tsx +240 -0
- package/registry/ui/filter-bar.tsx +392 -0
- package/registry/ui/funnel-steps.tsx +3541 -0
- package/registry/ui/heatmap.tsx +1752 -0
- package/registry/ui/hover-card.tsx +52 -0
- package/registry/ui/input-group.tsx +160 -0
- package/registry/ui/input.tsx +24 -0
- package/registry/ui/kbd.tsx +30 -0
- package/registry/ui/kpi-card.tsx +677 -0
- package/registry/ui/label.tsx +22 -0
- package/registry/ui/legend.tsx +3500 -0
- package/registry/ui/line-chart.tsx +3637 -0
- package/registry/ui/metric-value.tsx +62 -0
- package/registry/ui/multi-select.tsx +232 -0
- package/registry/ui/page-shell.tsx +282 -0
- package/registry/ui/pagination.tsx +140 -0
- package/registry/ui/placeholder.tsx +36 -0
- package/registry/ui/popover.tsx +92 -0
- package/registry/ui/progress-circle.tsx +867 -0
- package/registry/ui/progress.tsx +85 -0
- package/registry/ui/radio-group.tsx +39 -0
- package/registry/ui/sample-data.tsx +86 -0
- package/registry/ui/scatter-chart.tsx +4150 -0
- package/registry/ui/scroll-area.tsx +56 -0
- package/registry/ui/searchable-select.tsx +214 -0
- package/registry/ui/section-header.tsx +43 -0
- package/registry/ui/segmented-control.tsx +76 -0
- package/registry/ui/select.tsx +255 -0
- package/registry/ui/separator.tsx +27 -0
- package/registry/ui/sheet.tsx +139 -0
- package/registry/ui/sidebar.tsx +725 -0
- package/registry/ui/skeleton.tsx +17 -0
- package/registry/ui/slider.tsx +56 -0
- package/registry/ui/sparkline.tsx +230 -0
- package/registry/ui/spinner.tsx +14 -0
- package/registry/ui/states.tsx +90 -0
- package/registry/ui/status-badge.tsx +121 -0
- package/registry/ui/switch.tsx +34 -0
- package/registry/ui/table.tsx +118 -0
- package/registry/ui/tabs.tsx +84 -0
- package/registry/ui/textarea.tsx +22 -0
- package/registry/ui/toast.tsx +250 -0
- package/registry/ui/toggle-group.tsx +91 -0
- package/registry/ui/toggle.tsx +47 -0
- package/registry/ui/tooltip.tsx +68 -0
- package/registry/ui/tracker.tsx +843 -0
- package/registry/ui/trend-indicator.tsx +74 -0
- package/registry/ui/updated-at.tsx +122 -0
- package/registry/ui/waterfall-chart.tsx +4186 -0
- package/skills/vantage-add-feature/SKILL.md +9 -7
- package/skills/vantage-pitfalls/SKILL.md +5 -1
- package/templates/AGENTS.md +22 -1
- package/templates/agents/csv.md +2 -1
- package/templates/agents/doc.md +4 -3
- package/templates/agents/page.md +20 -6
- package/templates/agents/xlsx.md +2 -1
- package/theme.css +237 -136
- package/dist/add-DY4AXANK.js +0 -6
- package/dist/add-DY4AXANK.js.map +0 -1
- package/dist/chunk-5ZOP4SQR.js.map +0 -1
- package/dist/chunk-A2UUGASH.js.map +0 -1
- package/dist/chunk-DTDVSFRY.js.map +0 -1
- package/dist/chunk-LMUACBMA.js.map +0 -1
- package/dist/chunk-NKFBFDRZ.js +0 -2241
- package/dist/chunk-NKFBFDRZ.js.map +0 -1
- package/dist/chunk-PVTHHV7X.js.map +0 -1
- package/dist/chunk-PXRX5PEC.js.map +0 -1
- package/dist/chunk-QI3XQKHS.js.map +0 -1
- package/dist/chunk-RPZAALCX.js.map +0 -1
- package/dist/chunk-VA776YE5.js +0 -316
- package/dist/chunk-VA776YE5.js.map +0 -1
- package/dist/dev-GDKATCC7.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 `npm 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,197 @@ 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
|
-
|
|
44
|
-
|
|
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"
|
|
63
|
-
|
|
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"
|
|
64
78
|
|
|
65
79
|
/* ------------------------------------------------------------------- types */
|
|
66
80
|
|
|
81
|
+
declare module "@tanstack/react-table" {
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
83
|
+
interface ColumnMeta<TData extends RowData, TValue> {
|
|
84
|
+
/**
|
|
85
|
+
* Classes put on the column's header and body cells — e.g.
|
|
86
|
+
* `"hidden md:table-cell"` to drop a column on narrow screens.
|
|
87
|
+
*/
|
|
88
|
+
className?: string
|
|
89
|
+
/**
|
|
90
|
+
* 列の数値書式。`cell` を持たない列だけに効き、右寄せ・tabular-nums で描く。
|
|
91
|
+
* 行ごとに通貨が変わるときは関数で行を受ける。CSV の「表示」値にも使われる。
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* meta: { format: "currency" }
|
|
95
|
+
* meta: { format: (row) => ({ type: "currency", currency: row.currency, notation: "standard" }) }
|
|
96
|
+
*/
|
|
97
|
+
format?: ValueFormat | ((row: TData) => ValueFormat)
|
|
98
|
+
/**
|
|
99
|
+
* 日付の列の書式(`formatDate` の `style`)。`cell` を持たない列だけに効く。左寄せ・tabular-nums で描き、
|
|
100
|
+
* 値は `Date`・エポックミリ秒・日付文字列(`"2026-10-20"`、ISO 8601)。CSV の「表示」値にも使われる。
|
|
101
|
+
* `format`(数値)とは併用しない。
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* meta: { dateFormat: "date" }
|
|
105
|
+
* meta: { dateFormat: "datetime", nullLabel: "未更新" }
|
|
106
|
+
*/
|
|
107
|
+
dateFormat?: DateStyle
|
|
108
|
+
/**
|
|
109
|
+
* `dateFormat` の列の日時を出すタイムゾーン(IANA 名。例 `"Asia/Tokyo"`)。既定は実行環境(ブラウザ)のタイムゾーン。
|
|
110
|
+
* サーバーとブラウザで表示が食い違う(SSR)なら固定する。`dateFormat` の列だけに効き、CSV の「表示」値にも同じ値を使う。
|
|
111
|
+
* `"2026-10-20"` のような日付だけの文字列は、どのタイムゾーンでもその暦日のまま。
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* meta: { dateFormat: "datetime", timeZone: "Asia/Tokyo" }
|
|
115
|
+
*/
|
|
116
|
+
timeZone?: string
|
|
117
|
+
/**
|
|
118
|
+
* 値が無いときの表示。既定 `"—"`。null / undefined(数値は NaN も)に効き、文字列の列では空文字 `""` も値なしとして扱う。
|
|
119
|
+
* `format` / `dateFormat` を持つ列と、書式なしの文字列の列に効く。`cell` を自前で書いた列には効かない(`cell` の中で扱う)。
|
|
120
|
+
* CSV の「表示」値は、書式付き・日付の列だけ `nullLabel` で書き、書式なしの文字列の列は空欄のまま。
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* meta: { format: "number", nullLabel: "未集計" }
|
|
124
|
+
* meta: { nullLabel: "未設定" } // 書式なしの文字列の列
|
|
125
|
+
*/
|
|
126
|
+
nullLabel?: string
|
|
127
|
+
/**
|
|
128
|
+
* 値の範囲でセルを意味色に塗り分ける規則(最初に当たった規則。どれにも当たらない値は通常色)。
|
|
129
|
+
* `cell` を持たない列だけに効き、BarChart / BarList の `thresholds` と同じ型・同じ判定。
|
|
130
|
+
* 色だけに頼らず、色に応じた記号(○ / △ / × / −)を値の前に添える。CSV は生の値のまま。
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* meta: { format: { type: "percent", divisor: 100 }, thresholds: [{ lt: 40, tone: "negative" }, { lt: 70, tone: "warning" }] }
|
|
134
|
+
*/
|
|
135
|
+
thresholds?: ChartThreshold[]
|
|
136
|
+
/**
|
|
137
|
+
* 狭い幅で列を隠す順。数が小さい列から隠れる(表の置き場の幅が 1 は 640px 未満、2 は 520px 未満、3 は 400px 未満)。
|
|
138
|
+
* 書かない列と 4 以上の列は、どの幅でも出す(部署名・離職率のような肝心の列に使う)。
|
|
139
|
+
* 画面の幅ではなく、表を置いた場所の幅(コンテナ幅)で決まる。隠した列は DOM にも読み上げにも出ず、CSV の出力には含まれる。
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* meta: { format: "number", priority: 1 } // 幅が足りないときに最初に隠す
|
|
143
|
+
*/
|
|
144
|
+
priority?: number
|
|
145
|
+
/**
|
|
146
|
+
* true にすると、増減(`Delta`)など「直前の列の値との比較」を表示する列になる。表の置き場が 320px 未満に狭まると、
|
|
147
|
+
* この列は隠れる代わりに、直前の列(値の列)のセルの下に同じ内容を小さく(text-xs)積んで出す。比較は消えない。
|
|
148
|
+
* `priority` で隠す順は最後(どの列よりも後)になり、この列の `priority` は無視される。直前の列も `priority` に関わらず残る。
|
|
149
|
+
* 列の `cell` に `<Delta variant="inline" … />` を書く。直前に列が無いときは積まず、通常の列として出す。CSV には通常の列として含まれる。
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* { accessorKey: "revenue", header: "売上", meta: { format: "currency" } },
|
|
153
|
+
* { 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" }} /> }
|
|
154
|
+
*/
|
|
155
|
+
comparison?: boolean
|
|
156
|
+
/**
|
|
157
|
+
* 列の寄せ。見出し(ソートボタンを含む)とセルの両方に効く。既定は、`format` / `thresholds` の列と
|
|
158
|
+
* `comparison` の列(増減 `Delta`)が `"right"`、それ以外は `"left"`。右寄せの列は tabular-nums で描く。
|
|
159
|
+
*
|
|
160
|
+
* @example
|
|
161
|
+
* meta: { align: "right" }
|
|
162
|
+
*/
|
|
163
|
+
align?: "left" | "right"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ------------------------------------------------------------ cell formatting */
|
|
168
|
+
|
|
169
|
+
/** Resolve a column's `meta.format` for one row. */
|
|
170
|
+
function resolveFormat<TData>(
|
|
171
|
+
format: ValueFormat | ((row: TData) => ValueFormat) | undefined,
|
|
172
|
+
row: TData,
|
|
173
|
+
): ValueFormat | undefined {
|
|
174
|
+
return typeof format === "function" ? format(row) : format
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** 値なし: null / undefined / NaN / 空文字。 */
|
|
178
|
+
function isMissingCell(value: unknown): boolean {
|
|
179
|
+
return value === null || value === undefined || value === "" || (typeof value === "number" && Number.isNaN(value))
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Display string of a column's value (`meta.format` / `meta.dateFormat` / plain);
|
|
184
|
+
* a missing value renders as `nullLabel` (default `—`).
|
|
185
|
+
*/
|
|
186
|
+
function formatCellText<TData>(
|
|
187
|
+
value: unknown,
|
|
188
|
+
row: TData,
|
|
189
|
+
meta: { format?: ValueFormat | ((row: TData) => ValueFormat); dateFormat?: DateStyle; timeZone?: string; nullLabel?: string },
|
|
190
|
+
): string {
|
|
191
|
+
if (isMissingCell(value)) return meta.nullLabel ?? EMPTY_VALUE
|
|
192
|
+
if (meta.dateFormat && (value instanceof Date || typeof value === "string" || typeof value === "number")) {
|
|
193
|
+
const text = formatDate(value, meta.dateFormat, undefined, { timeZone: meta.timeZone })
|
|
194
|
+
return text === EMPTY_VALUE ? String(value) : text
|
|
195
|
+
}
|
|
196
|
+
if (typeof value !== "number") return String(value)
|
|
197
|
+
return formatValue(value, resolveFormat(meta.format, row))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 意味色ごとの記号と色クラス。Tailwind が拾えるよう、クラスはリテラルで書く。 */
|
|
201
|
+
const THRESHOLD_TONE_STYLES: Record<ChartTone, { Icon: LucideIcon; text: string; label: string }> = {
|
|
202
|
+
positive: { Icon: CircleCheck, text: "text-positive", label: "良好" },
|
|
203
|
+
warning: { Icon: TriangleAlert, text: "text-warning", label: "注意" },
|
|
204
|
+
negative: { Icon: CircleX, text: "text-negative", label: "要対応" },
|
|
205
|
+
neutral: { Icon: Minus, text: "text-neutral", label: "中立" },
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** `meta.thresholds` 列のセル。当たった規則の意味色と記号を値に添える(当たらなければ値だけ)。 */
|
|
209
|
+
function ThresholdCell({ value, text, thresholds }: { value: unknown; text: string; thresholds: ChartThreshold[] }) {
|
|
210
|
+
const tone = typeof value === "number" ? toneForValue(value, thresholds) : undefined
|
|
211
|
+
if (!tone) return <div className="text-right tabular-nums">{text}</div>
|
|
212
|
+
const { Icon, text: toneClass, label } = THRESHOLD_TONE_STYLES[tone]
|
|
213
|
+
return (
|
|
214
|
+
<div
|
|
215
|
+
data-slot="data-table-threshold-cell"
|
|
216
|
+
data-tone={tone}
|
|
217
|
+
className={cn("flex items-center justify-end gap-1.5 tabular-nums", toneClass)}
|
|
218
|
+
>
|
|
219
|
+
<Icon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
220
|
+
<span>{text}</span>
|
|
221
|
+
<span className="sr-only">({label})</span>
|
|
222
|
+
</div>
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
|
|
67
226
|
interface DataTableContextValue<TData> {
|
|
68
227
|
table: TanstackTable<TData>
|
|
69
228
|
selectedRows: TData[]
|
|
@@ -74,6 +233,7 @@ interface DataTableContextValue<TData> {
|
|
|
74
233
|
enablePagination: boolean
|
|
75
234
|
enableRowSelection: boolean
|
|
76
235
|
visibleColumnCount: number
|
|
236
|
+
isLoading: boolean
|
|
77
237
|
}
|
|
78
238
|
|
|
79
239
|
/**
|
|
@@ -89,7 +249,8 @@ export interface DataTableProps<TData, TValue> {
|
|
|
89
249
|
* literal が `string` に拡張され型エラーになる。
|
|
90
250
|
*
|
|
91
251
|
* @example
|
|
92
|
-
* import type
|
|
252
|
+
* import { DataTable, type ColumnDef } from "@/components/data-table"
|
|
253
|
+
* // Vantage: import { DataTable, type ColumnDef } from "@squadbase/vantage/components"
|
|
93
254
|
*
|
|
94
255
|
* const columns: ColumnDef<User>[] = [
|
|
95
256
|
* { accessorKey: "id", header: "ID" },
|
|
@@ -116,7 +277,42 @@ export interface DataTableProps<TData, TValue> {
|
|
|
116
277
|
enablePagination?: boolean
|
|
117
278
|
pagination?: PaginationState
|
|
118
279
|
onPaginationChange?: OnChangeFn<PaginationState>
|
|
280
|
+
/**
|
|
281
|
+
* サーバー側ページング時の総ページ数。指定すると manualPagination になる。
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* <DataTable pageCount={data.pageCount} … />
|
|
285
|
+
*/
|
|
119
286
|
pageCount?: number
|
|
287
|
+
/**
|
|
288
|
+
* サーバー側ページング時の総件数。指定すると manualPagination になり、ページ数は
|
|
289
|
+
* `rowCount / pageSize` から導かれ、「N件中」もこの値を使う(`pageCount` を同時に渡せばそちらが優先)。
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* <DataTable rowCount={1240} pagination={pagination} onPaginationChange={setPagination} … />
|
|
293
|
+
*/
|
|
294
|
+
rowCount?: number
|
|
295
|
+
/**
|
|
296
|
+
* true にすると絞り込みを表では行わず、`data` をそのまま使う(サーバー側で絞り込む)。
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* <DataTable manualFiltering globalFilter={q} onGlobalFilterChange={setQ} … />
|
|
300
|
+
*/
|
|
301
|
+
manualFiltering?: boolean
|
|
302
|
+
/**
|
|
303
|
+
* true にすると並べ替えを表では行わず、`data` の順をそのまま使う(サーバー側で並べ替える)。
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* <DataTable manualSorting sorting={sorting} onSortingChange={setSorting} … />
|
|
307
|
+
*/
|
|
308
|
+
manualSorting?: boolean
|
|
309
|
+
/**
|
|
310
|
+
* 読み込み中。Content はスケルトン行、Pagination のボタンは無効、表は aria-busy になる。
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* <DataTable isLoading={query.isFetching} … />
|
|
314
|
+
*/
|
|
315
|
+
isLoading?: boolean
|
|
120
316
|
|
|
121
317
|
// Row selection
|
|
122
318
|
enableRowSelection?: boolean
|
|
@@ -127,6 +323,13 @@ export interface DataTableProps<TData, TValue> {
|
|
|
127
323
|
columnVisibility?: VisibilityState
|
|
128
324
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>
|
|
129
325
|
|
|
326
|
+
/**
|
|
327
|
+
* TanStack Table の `meta`。表全体で共有する値(行を更新する関数など)を渡し、列定義の `cell` から
|
|
328
|
+
* `table.options.meta` で読む。列ごとの書式は列定義の `meta.format` に書く(こちらではない)。
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* <DataTable meta={{ updateRow: (id, patch) => mutate({ id, patch }) }} … />
|
|
332
|
+
*/
|
|
130
333
|
meta?: TableMeta<TData>
|
|
131
334
|
className?: string
|
|
132
335
|
|
|
@@ -142,6 +345,33 @@ export interface DataTableToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
142
345
|
|
|
143
346
|
export interface DataTableContentProps extends HTMLAttributes<HTMLDivElement> {}
|
|
144
347
|
|
|
348
|
+
export interface DataTableExportProps {
|
|
349
|
+
/**
|
|
350
|
+
* 保存するファイル名。既定 `"export.csv"`。
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* <DataTableExport fileName="sales-2025.csv" />
|
|
354
|
+
*/
|
|
355
|
+
fileName?: string
|
|
356
|
+
/**
|
|
357
|
+
* `"display"`(既定)は `meta.format` どおりの表示文字列、`"raw"` は生の値を書き出す。
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* <DataTableExport values="raw" />
|
|
361
|
+
*/
|
|
362
|
+
values?: CsvValues
|
|
363
|
+
/**
|
|
364
|
+
* `"filtered"`(既定)は並べ替え・絞り込み後の全ページ、`"page"` は表示中のページだけ。
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* <DataTableExport scope="page" />
|
|
368
|
+
*/
|
|
369
|
+
scope?: CsvScope
|
|
370
|
+
/** ボタンの文言。既定 `"CSV を書き出す"`。 */
|
|
371
|
+
children?: ReactNode
|
|
372
|
+
className?: string
|
|
373
|
+
}
|
|
374
|
+
|
|
145
375
|
export interface DataTablePaginationProps extends HTMLAttributes<HTMLDivElement> {
|
|
146
376
|
pageSizeOptions?: number[]
|
|
147
377
|
}
|
|
@@ -152,40 +382,31 @@ export interface DataTableColumnVisibilityProps {
|
|
|
152
382
|
className?: string
|
|
153
383
|
}
|
|
154
384
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
385
|
+
/** The preset takes every `DataTable` prop except `children`, plus a few slots. */
|
|
386
|
+
export type DataTablePresetProps<TData, TValue> = Omit<
|
|
387
|
+
DataTableProps<TData, TValue>,
|
|
388
|
+
"children"
|
|
389
|
+
> & {
|
|
178
390
|
enableColumnVisibility?: boolean
|
|
179
|
-
columnVisibility?: VisibilityState
|
|
180
|
-
onColumnVisibilityChange?: OnChangeFn<VisibilityState>
|
|
181
391
|
columnLabels?: Record<string, string>
|
|
392
|
+
/**
|
|
393
|
+
* true にすると toolbar に CSV 書き出しボタン(`DataTableExport`)を置く。既定 false。
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* <DataTablePreset exportable … />
|
|
397
|
+
*/
|
|
398
|
+
exportable?: boolean
|
|
399
|
+
/**
|
|
400
|
+
* `exportable` のときの保存ファイル名。既定 `"export.csv"`。
|
|
401
|
+
*
|
|
402
|
+
* @example
|
|
403
|
+
* <DataTablePreset exportable exportFileName="sales.csv" … />
|
|
404
|
+
*/
|
|
405
|
+
exportFileName?: string
|
|
182
406
|
|
|
183
407
|
/** Extension slots */
|
|
184
408
|
toolbar?: ReactNode
|
|
185
409
|
bulkActions?: (selectedRows: TData[]) => ReactNode
|
|
186
|
-
|
|
187
|
-
meta?: TableMeta<TData>
|
|
188
|
-
className?: string
|
|
189
410
|
}
|
|
190
411
|
|
|
191
412
|
/* ------------------------------------------------------------------ helper */
|
|
@@ -226,9 +447,12 @@ export function useDataTable<TData>(): DataTableContextValue<TData> {
|
|
|
226
447
|
|
|
227
448
|
/* -------------------------------------------------------------------- root */
|
|
228
449
|
|
|
450
|
+
/** Columns / rows not given yet (a draft fills props in steps): draw the empty state, keep the same array between renders. */
|
|
451
|
+
const NO_ROWS: never[] = []
|
|
452
|
+
|
|
229
453
|
export function DataTable<TData, TValue>({
|
|
230
|
-
columns,
|
|
231
|
-
data,
|
|
454
|
+
columns = NO_ROWS,
|
|
455
|
+
data = NO_ROWS,
|
|
232
456
|
enableSorting = true,
|
|
233
457
|
sorting: sortingProp,
|
|
234
458
|
onSortingChange,
|
|
@@ -241,6 +465,10 @@ export function DataTable<TData, TValue>({
|
|
|
241
465
|
pagination: paginationProp,
|
|
242
466
|
onPaginationChange,
|
|
243
467
|
pageCount,
|
|
468
|
+
rowCount,
|
|
469
|
+
manualFiltering,
|
|
470
|
+
manualSorting,
|
|
471
|
+
isLoading = false,
|
|
244
472
|
enableRowSelection = false,
|
|
245
473
|
rowSelection: rowSelectionProp,
|
|
246
474
|
onRowSelectionChange,
|
|
@@ -284,7 +512,29 @@ export function DataTable<TData, TValue>({
|
|
|
284
512
|
|
|
285
513
|
// Row selection prepends a checkbox column that the caller never declares.
|
|
286
514
|
const allColumns = useMemo<ColumnDef<TData, TValue>[]>(() => {
|
|
287
|
-
|
|
515
|
+
// A column with `meta.format` / `meta.thresholds` and no `cell` gets a formatted, right-aligned
|
|
516
|
+
// cell; thresholds add the tone colour plus a symbol. `meta.dateFormat` gives a left-aligned date.
|
|
517
|
+
// A plain accessor column (no `cell`) shows `nullLabel` / `—` for a missing value.
|
|
518
|
+
const formatted = columns.map((column) => {
|
|
519
|
+
const meta = column.meta
|
|
520
|
+
if (column.cell !== undefined) return column
|
|
521
|
+
const hasAccessor = "accessorKey" in column || "accessorFn" in column
|
|
522
|
+
if (!(meta?.format || meta?.thresholds || meta?.dateFormat) && !hasAccessor) return column
|
|
523
|
+
const thresholds = meta?.thresholds
|
|
524
|
+
const aligned = Boolean(meta?.format || thresholds)
|
|
525
|
+
return {
|
|
526
|
+
...column,
|
|
527
|
+
cell: ({ row, getValue }) => {
|
|
528
|
+
const value = getValue()
|
|
529
|
+
const text = formatCellText(value, row.original, meta ?? {})
|
|
530
|
+
if (thresholds) return <ThresholdCell value={value} text={text} thresholds={thresholds} />
|
|
531
|
+
if (aligned) return <div className="text-right tabular-nums">{text}</div>
|
|
532
|
+
if (meta?.dateFormat) return <span className="tabular-nums">{text}</span>
|
|
533
|
+
return text
|
|
534
|
+
},
|
|
535
|
+
} as ColumnDef<TData, TValue>
|
|
536
|
+
})
|
|
537
|
+
if (!enableRowSelection) return formatted
|
|
288
538
|
|
|
289
539
|
const selectionColumn: ColumnDef<TData, TValue> = {
|
|
290
540
|
id: "_select",
|
|
@@ -307,10 +557,26 @@ export function DataTable<TData, TValue>({
|
|
|
307
557
|
enableHiding: false,
|
|
308
558
|
}
|
|
309
559
|
|
|
310
|
-
return [selectionColumn, ...
|
|
560
|
+
return [selectionColumn, ...formatted]
|
|
311
561
|
}, [columns, enableRowSelection])
|
|
312
562
|
|
|
563
|
+
// TanStack's own `autoResetPageIndex` queues `resetPageIndex()` from inside a render-time memo, which
|
|
564
|
+
// can fire before the component has mounted ("Can't perform a React state update on a component that
|
|
565
|
+
// hasn't mounted yet"). Turn it off and send the page back to 1 from an effect instead, so it only
|
|
566
|
+
// runs after mount and only when the sort / filter / data actually changed.
|
|
567
|
+
const autoResetPage = enablePagination && pageCount === undefined && rowCount === undefined
|
|
568
|
+
const resetKey = useRef<readonly unknown[] | null>(null)
|
|
569
|
+
useEffect(() => {
|
|
570
|
+
const next = [data, sorting, columnFilters, globalFilter]
|
|
571
|
+
const prev = resetKey.current
|
|
572
|
+
resetKey.current = next
|
|
573
|
+
if (!autoResetPage || prev === null) return
|
|
574
|
+
if (prev.every((value, i) => value === next[i])) return
|
|
575
|
+
setPagination((old) => (old.pageIndex === 0 ? old : { ...old, pageIndex: 0 }))
|
|
576
|
+
}, [autoResetPage, data, sorting, columnFilters, globalFilter, setPagination])
|
|
577
|
+
|
|
313
578
|
const table = useReactTable({
|
|
579
|
+
autoResetPageIndex: false,
|
|
314
580
|
data,
|
|
315
581
|
columns: allColumns,
|
|
316
582
|
state: {
|
|
@@ -329,7 +595,11 @@ export function DataTable<TData, TValue>({
|
|
|
329
595
|
onRowSelectionChange: setRowSelection,
|
|
330
596
|
onColumnVisibilityChange: setColumnVisibility,
|
|
331
597
|
...(enablePagination ? { onPaginationChange: setPagination } : {}),
|
|
332
|
-
...(pageCount !== undefined
|
|
598
|
+
...(pageCount !== undefined || rowCount !== undefined
|
|
599
|
+
? { pageCount, rowCount, manualPagination: true }
|
|
600
|
+
: {}),
|
|
601
|
+
...(manualFiltering ? { manualFiltering: true } : {}),
|
|
602
|
+
...(manualSorting ? { manualSorting: true } : {}),
|
|
333
603
|
getCoreRowModel: getCoreRowModel(),
|
|
334
604
|
getSortedRowModel: getSortedRowModel(),
|
|
335
605
|
getFilteredRowModel: getFilteredRowModel(),
|
|
@@ -357,6 +627,7 @@ export function DataTable<TData, TValue>({
|
|
|
357
627
|
enablePagination,
|
|
358
628
|
enableRowSelection,
|
|
359
629
|
visibleColumnCount: allColumns.length,
|
|
630
|
+
isLoading,
|
|
360
631
|
}),
|
|
361
632
|
// `useReactTable` returns the same object every render, so each piece of
|
|
362
633
|
// table state has to be tracked explicitly to re-render the consumers.
|
|
@@ -371,6 +642,8 @@ export function DataTable<TData, TValue>({
|
|
|
371
642
|
enablePagination,
|
|
372
643
|
enableRowSelection,
|
|
373
644
|
allColumns.length,
|
|
645
|
+
isLoading,
|
|
646
|
+
rowCount,
|
|
374
647
|
sorting,
|
|
375
648
|
columnFilters,
|
|
376
649
|
columnVisibility,
|
|
@@ -381,7 +654,7 @@ export function DataTable<TData, TValue>({
|
|
|
381
654
|
|
|
382
655
|
return (
|
|
383
656
|
<DataTableContext.Provider value={contextValue as DataTableContextValue<unknown>}>
|
|
384
|
-
<div data-slot="data-table" className={cn("space-y-4", className)}>
|
|
657
|
+
<div data-slot="data-table" aria-busy={isLoading || undefined} className={cn("space-y-4", className)}>
|
|
385
658
|
{children}
|
|
386
659
|
</div>
|
|
387
660
|
</DataTableContext.Provider>
|
|
@@ -389,6 +662,68 @@ export function DataTable<TData, TValue>({
|
|
|
389
662
|
}
|
|
390
663
|
DataTable.displayName = "DataTable"
|
|
391
664
|
|
|
665
|
+
/* ------------------------------------------------------ sparkline column */
|
|
666
|
+
|
|
667
|
+
/** Fewest points a row needs for a trend (one point has no direction). */
|
|
668
|
+
const SPARKLINE_MIN_POINTS = 2
|
|
669
|
+
|
|
670
|
+
export interface SparklineColumnOptions<TData> {
|
|
671
|
+
/** 列 ID(CSV・列の表示切り替えで使う)。 */
|
|
672
|
+
id: string
|
|
673
|
+
/** 見出し。 */
|
|
674
|
+
header: string
|
|
675
|
+
/**
|
|
676
|
+
* 行の数列を返す(古い順。例: 直近 8 週の売上)。null / undefined / 2 点未満の行は「—」。
|
|
677
|
+
* NaN などの有限でない値は取り除いてから数える。
|
|
678
|
+
*/
|
|
679
|
+
values: (row: TData) => readonly (number | null | undefined)[] | null | undefined
|
|
680
|
+
/** スクリーンリーダー向けの数値の書式(`meta.format` と同じ語彙)。既定は `"number"`。 */
|
|
681
|
+
format?: ValueFormat
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* 行の数列をセルの小さなトレンド(Sparkline)にする列。色は `--chart-1` 固定、軸・ツールチップ・ラベルなし。
|
|
686
|
+
* 図は読み上げず、同じセルの sr-only に「N 点、最初 → 最後、最小、最大」を書式付きで添える。
|
|
687
|
+
* 数列の並べ替えは意味が無いので無効。DataTable 本体の props は増やさず、`columns` に置くだけで使う。
|
|
688
|
+
*
|
|
689
|
+
* @example
|
|
690
|
+
* const columns: ColumnDef<Store>[] = [
|
|
691
|
+
* { accessorKey: "store", header: "店舗" },
|
|
692
|
+
* sparklineColumn<Store>({ id: "trend", header: "直近 8 週", values: (r) => r.weekly, format: "currency" }),
|
|
693
|
+
* ]
|
|
694
|
+
*/
|
|
695
|
+
export function sparklineColumn<TData>({
|
|
696
|
+
id,
|
|
697
|
+
header,
|
|
698
|
+
values,
|
|
699
|
+
format = "number",
|
|
700
|
+
}: SparklineColumnOptions<TData>): ColumnDef<TData, unknown> {
|
|
701
|
+
return {
|
|
702
|
+
id,
|
|
703
|
+
header,
|
|
704
|
+
accessorFn: (row) => values(row) ?? null,
|
|
705
|
+
enableSorting: false,
|
|
706
|
+
cell: ({ getValue }) => {
|
|
707
|
+
const raw = getValue() as readonly (number | null | undefined)[] | null
|
|
708
|
+
const series = (raw ?? []).filter((v): v is number => typeof v === "number" && Number.isFinite(v))
|
|
709
|
+
if (series.length < SPARKLINE_MIN_POINTS) return <span className="text-muted-foreground">{EMPTY_VALUE}</span>
|
|
710
|
+
const f = (v: number) => formatValue(v, format)
|
|
711
|
+
const first = series[0] as number
|
|
712
|
+
const last = series[series.length - 1] as number
|
|
713
|
+
return (
|
|
714
|
+
<div data-slot="data-table-sparkline-cell" className="flex h-5 items-center">
|
|
715
|
+
<div aria-hidden="true" className="h-5 w-24 shrink-0">
|
|
716
|
+
<Sparkline data={series.map((value) => ({ value }))} height={20} />
|
|
717
|
+
</div>
|
|
718
|
+
<span className="sr-only">
|
|
719
|
+
{series.length}点の推移、{f(first)}から{f(last)}、最小{f(Math.min(...series))}、最大{f(Math.max(...series))}
|
|
720
|
+
</span>
|
|
721
|
+
</div>
|
|
722
|
+
)
|
|
723
|
+
},
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
392
727
|
/* ----------------------------------------------------------------- toolbar */
|
|
393
728
|
|
|
394
729
|
export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps>(
|
|
@@ -431,63 +766,199 @@ DataTableToolbar.displayName = "DataTableToolbar"
|
|
|
431
766
|
|
|
432
767
|
/* ----------------------------------------------------------------- content */
|
|
433
768
|
|
|
769
|
+
/**
|
|
770
|
+
* `meta.priority` → the classes that hide the column below a container width (`@container` on the content wrapper).
|
|
771
|
+
* The class strings are literals so Tailwind picks them up.
|
|
772
|
+
*/
|
|
773
|
+
const PRIORITY_CLASS: Record<number, string> = {
|
|
774
|
+
1: "hidden @min-[640px]:table-cell",
|
|
775
|
+
2: "hidden @min-[520px]:table-cell",
|
|
776
|
+
3: "hidden @min-[400px]:table-cell",
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* A `meta.comparison` column is the last tier: it stays as its own column down to 320px, below which it is hidden and its
|
|
781
|
+
* content is stacked under the value cell instead (`COMPARISON_STACK_CLASS`, visible only below the same width).
|
|
782
|
+
*/
|
|
783
|
+
const COMPARISON_CLASS = "hidden @min-[320px]:table-cell"
|
|
784
|
+
const COMPARISON_STACK_CLASS = "@min-[320px]:hidden"
|
|
785
|
+
|
|
786
|
+
/** A column's hide-on-narrow classes: none without a priority (or at 4 and above), the lowest tier for anything below 1. */
|
|
787
|
+
export function priorityClassName(priority: number | undefined): string | undefined {
|
|
788
|
+
if (priority === undefined || !Number.isFinite(priority) || priority >= 4) return undefined
|
|
789
|
+
return PRIORITY_CLASS[Math.max(1, Math.floor(priority))]
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
type CellMeta = {
|
|
793
|
+
className?: string
|
|
794
|
+
format?: unknown
|
|
795
|
+
thresholds?: unknown
|
|
796
|
+
priority?: number
|
|
797
|
+
comparison?: boolean
|
|
798
|
+
align?: "left" | "right"
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/** `meta.align` wins; otherwise numeric columns (`format` / `thresholds`) and Delta (`comparison`) columns are right-aligned, header included. */
|
|
802
|
+
function isRightAligned(meta: CellMeta | undefined): boolean {
|
|
803
|
+
if (meta?.align) return meta.align === "right"
|
|
804
|
+
return Boolean(meta?.format || meta?.thresholds || meta?.comparison)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* A column that carries a stacked comparison (`isHost`) never hides, and a comparison column ignores its own
|
|
809
|
+
* `priority` and takes the last tier.
|
|
810
|
+
*/
|
|
811
|
+
function columnClassName(meta: CellMeta | undefined, options: { isComparison?: boolean; isHost?: boolean } = {}) {
|
|
812
|
+
const tier = options.isComparison
|
|
813
|
+
? COMPARISON_CLASS
|
|
814
|
+
: options.isHost
|
|
815
|
+
? undefined
|
|
816
|
+
: priorityClassName(meta?.priority)
|
|
817
|
+
return cn(tier, meta?.className, isRightAligned(meta) ? "text-right tabular-nums" : undefined) || undefined
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Pair every `meta.comparison` column with the visible column right before it (the value it compares).
|
|
822
|
+
* A comparison column with nothing before it stays an ordinary column.
|
|
823
|
+
*/
|
|
824
|
+
function pairComparisons<TData>(columns: Column<TData, unknown>[]) {
|
|
825
|
+
const comparisonOf = new Map<string, Column<TData, unknown>>() // host column id → its comparison column
|
|
826
|
+
const comparisonIds = new Set<string>()
|
|
827
|
+
columns.forEach((column, i) => {
|
|
828
|
+
if (i > 0 && column.columnDef.meta?.comparison && !columns[i - 1]!.columnDef.meta?.comparison) {
|
|
829
|
+
comparisonOf.set(columns[i - 1]!.id, column)
|
|
830
|
+
comparisonIds.add(column.id)
|
|
831
|
+
}
|
|
832
|
+
})
|
|
833
|
+
return { comparisonOf, comparisonIds }
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const SORT_ICON_CLASS = "size-4 shrink-0"
|
|
837
|
+
|
|
838
|
+
/** The sort icon: always shown on the sorted column, only on hover / keyboard focus on the others. */
|
|
839
|
+
function SortIcon({ sorted }: { sorted: false | "asc" | "desc" }) {
|
|
840
|
+
if (sorted === "asc") return <ArrowUp className={SORT_ICON_CLASS} aria-hidden="true" />
|
|
841
|
+
if (sorted === "desc") return <ArrowDown className={SORT_ICON_CLASS} aria-hidden="true" />
|
|
842
|
+
return (
|
|
843
|
+
<ArrowUpDown
|
|
844
|
+
className={cn(
|
|
845
|
+
SORT_ICON_CLASS,
|
|
846
|
+
"text-muted-foreground opacity-0 group-hover/sort:opacity-100 group-focus-visible/sort:opacity-100",
|
|
847
|
+
)}
|
|
848
|
+
aria-hidden="true"
|
|
849
|
+
/>
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
|
|
434
853
|
export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps>(
|
|
435
854
|
({ className, ...props }, ref) => {
|
|
436
|
-
const { table, visibleColumnCount } = useDataTable()
|
|
855
|
+
const { table, visibleColumnCount, isLoading, enablePagination } = useDataTable()
|
|
856
|
+
const { comparisonOf, comparisonIds } = pairComparisons(table.getVisibleLeafColumns())
|
|
437
857
|
|
|
438
858
|
return (
|
|
439
859
|
<div
|
|
440
860
|
ref={ref}
|
|
441
861
|
data-slot="data-table-content"
|
|
442
|
-
|
|
862
|
+
aria-busy={isLoading || undefined}
|
|
863
|
+
className={cn("@container overflow-x-auto", className)}
|
|
443
864
|
{...props}
|
|
444
865
|
>
|
|
445
|
-
<Table>
|
|
866
|
+
<Table className="[&_th:first-child]:pl-0 [&_td:first-child]:pl-0 [&_th:last-child]:pr-0 [&_td:last-child]:pr-0">
|
|
446
867
|
<TableHeader>
|
|
447
868
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
448
869
|
<TableRow key={headerGroup.id}>
|
|
449
|
-
{headerGroup.headers.map((header) =>
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
870
|
+
{headerGroup.headers.map((header) => {
|
|
871
|
+
const column = header.column
|
|
872
|
+
const meta = column.columnDef.meta
|
|
873
|
+
const sorted = column.getIsSorted()
|
|
874
|
+
const canSort = column.getCanSort()
|
|
875
|
+
const label = header.isPlaceholder ? null : flexRender(column.columnDef.header, header.getContext())
|
|
876
|
+
return (
|
|
877
|
+
<TableHead
|
|
878
|
+
key={header.id}
|
|
879
|
+
className={columnClassName(meta, {
|
|
880
|
+
isComparison: comparisonIds.has(column.id),
|
|
881
|
+
isHost: comparisonOf.has(column.id),
|
|
882
|
+
})}
|
|
883
|
+
aria-sort={
|
|
884
|
+
canSort && !header.isPlaceholder
|
|
885
|
+
? sorted === "asc"
|
|
458
886
|
? "ascending"
|
|
459
|
-
:
|
|
887
|
+
: sorted === "desc"
|
|
460
888
|
? "descending"
|
|
461
889
|
: "none"
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
890
|
+
: undefined
|
|
891
|
+
}
|
|
892
|
+
>
|
|
893
|
+
{header.isPlaceholder ? null : canSort ? (
|
|
894
|
+
<button
|
|
895
|
+
type="button"
|
|
896
|
+
className={cn(
|
|
897
|
+
"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",
|
|
898
|
+
isRightAligned(meta) ? "flex w-full justify-end" : "inline-flex",
|
|
899
|
+
)}
|
|
900
|
+
onClick={column.getToggleSortingHandler()}
|
|
901
|
+
>
|
|
902
|
+
{isRightAligned(meta) && <SortIcon sorted={sorted} />}
|
|
903
|
+
{label}
|
|
904
|
+
{!isRightAligned(meta) && <SortIcon sorted={sorted} />}
|
|
905
|
+
</button>
|
|
906
|
+
) : (
|
|
907
|
+
label
|
|
908
|
+
)}
|
|
909
|
+
</TableHead>
|
|
910
|
+
)
|
|
911
|
+
})}
|
|
478
912
|
</TableRow>
|
|
479
913
|
))}
|
|
480
914
|
</TableHeader>
|
|
481
915
|
|
|
482
916
|
<TableBody>
|
|
483
|
-
{
|
|
917
|
+
{isLoading ? (
|
|
918
|
+
Array.from({ length: enablePagination ? table.getState().pagination.pageSize : 5 }).map(
|
|
919
|
+
(_, i) => (
|
|
920
|
+
<TableRow key={i} data-slot="data-table-skeleton-row">
|
|
921
|
+
{Array.from({ length: visibleColumnCount }).map((__, j) => (
|
|
922
|
+
<TableCell key={j}>
|
|
923
|
+
<Skeleton className="h-4 w-full" />
|
|
924
|
+
</TableCell>
|
|
925
|
+
))}
|
|
926
|
+
</TableRow>
|
|
927
|
+
),
|
|
928
|
+
)
|
|
929
|
+
) : table.getRowModel().rows.length ? (
|
|
484
930
|
table.getRowModel().rows.map((row) => (
|
|
485
931
|
<TableRow key={row.id} data-state={row.getIsSelected() ? "selected" : undefined}>
|
|
486
|
-
{row.getVisibleCells().map((cell) =>
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
932
|
+
{row.getVisibleCells().map((cell) => {
|
|
933
|
+
const meta = cell.column.columnDef.meta
|
|
934
|
+
const comparisonColumn = comparisonOf.get(cell.column.id)
|
|
935
|
+
const comparisonCell = comparisonColumn
|
|
936
|
+
? row.getVisibleCells().find((c) => c.column.id === comparisonColumn.id)
|
|
937
|
+
: undefined
|
|
938
|
+
return (
|
|
939
|
+
<TableCell
|
|
940
|
+
key={cell.id}
|
|
941
|
+
className={columnClassName(meta, {
|
|
942
|
+
isComparison: comparisonIds.has(cell.column.id),
|
|
943
|
+
isHost: comparisonOf.has(cell.column.id),
|
|
944
|
+
})}
|
|
945
|
+
>
|
|
946
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
947
|
+
{comparisonCell && (
|
|
948
|
+
<div
|
|
949
|
+
data-slot="data-table-stacked-comparison"
|
|
950
|
+
className={cn(
|
|
951
|
+
COMPARISON_STACK_CLASS,
|
|
952
|
+
"mt-0.5 text-xs [&_[data-slot=delta]]:text-xs",
|
|
953
|
+
isRightAligned(meta) ? "text-right" : undefined,
|
|
954
|
+
)}
|
|
955
|
+
>
|
|
956
|
+
{flexRender(comparisonCell.column.columnDef.cell, comparisonCell.getContext())}
|
|
957
|
+
</div>
|
|
958
|
+
)}
|
|
959
|
+
</TableCell>
|
|
960
|
+
)
|
|
961
|
+
})}
|
|
491
962
|
</TableRow>
|
|
492
963
|
))
|
|
493
964
|
) : (
|
|
@@ -512,18 +983,28 @@ DataTableContent.displayName = "DataTableContent"
|
|
|
512
983
|
|
|
513
984
|
export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginationProps>(
|
|
514
985
|
({ pageSizeOptions = [10, 20, 50], className, ...props }, ref) => {
|
|
515
|
-
const { table } = useDataTable()
|
|
986
|
+
const { table, isLoading } = useDataTable()
|
|
516
987
|
|
|
517
|
-
const totalRows =
|
|
988
|
+
const totalRows =
|
|
989
|
+
table.options.rowCount !== undefined
|
|
990
|
+
? table.getRowCount()
|
|
991
|
+
: table.getFilteredRowModel().rows.length
|
|
518
992
|
const currentPageSize = table.getState().pagination.pageSize
|
|
519
993
|
const currentPageIndex = table.getState().pagination.pageIndex
|
|
520
994
|
const rangeStart = currentPageIndex * currentPageSize + 1
|
|
521
995
|
const rangeEnd = Math.min((currentPageIndex + 1) * currentPageSize, totalRows)
|
|
996
|
+
// One list feeds both the menu and `items`, so the trigger shows "10件"
|
|
997
|
+
// rather than the raw "10" under Base UI's SelectValue.
|
|
998
|
+
const pageSizeItems = pageSizeOptions.map((size) => ({
|
|
999
|
+
value: String(size),
|
|
1000
|
+
label: `${size}件`,
|
|
1001
|
+
}))
|
|
522
1002
|
|
|
523
1003
|
return (
|
|
524
1004
|
<div
|
|
525
1005
|
ref={ref}
|
|
526
1006
|
data-slot="data-table-pagination"
|
|
1007
|
+
aria-busy={isLoading || undefined}
|
|
527
1008
|
className={cn("flex flex-wrap items-center justify-between gap-4", className)}
|
|
528
1009
|
{...props}
|
|
529
1010
|
>
|
|
@@ -532,7 +1013,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
532
1013
|
{totalRows}件中 {rangeStart} - {rangeEnd} を表示
|
|
533
1014
|
</span>
|
|
534
1015
|
<Select
|
|
535
|
-
items={
|
|
1016
|
+
items={pageSizeItems}
|
|
536
1017
|
value={String(currentPageSize)}
|
|
537
1018
|
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
538
1019
|
>
|
|
@@ -540,9 +1021,9 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
540
1021
|
<SelectValue />
|
|
541
1022
|
</SelectTrigger>
|
|
542
1023
|
<SelectContent>
|
|
543
|
-
{
|
|
544
|
-
<SelectItem key={
|
|
545
|
-
{
|
|
1024
|
+
{pageSizeItems.map((item) => (
|
|
1025
|
+
<SelectItem key={item.value} value={item.value}>
|
|
1026
|
+
{item.label}
|
|
546
1027
|
</SelectItem>
|
|
547
1028
|
))}
|
|
548
1029
|
</SelectContent>
|
|
@@ -554,7 +1035,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
554
1035
|
variant="outline"
|
|
555
1036
|
size="sm"
|
|
556
1037
|
onClick={() => table.firstPage()}
|
|
557
|
-
disabled={!table.getCanPreviousPage()}
|
|
1038
|
+
disabled={isLoading || !table.getCanPreviousPage()}
|
|
558
1039
|
>
|
|
559
1040
|
最初
|
|
560
1041
|
</Button>
|
|
@@ -562,7 +1043,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
562
1043
|
variant="outline"
|
|
563
1044
|
size="sm"
|
|
564
1045
|
onClick={() => table.previousPage()}
|
|
565
|
-
disabled={!table.getCanPreviousPage()}
|
|
1046
|
+
disabled={isLoading || !table.getCanPreviousPage()}
|
|
566
1047
|
>
|
|
567
1048
|
前へ
|
|
568
1049
|
</Button>
|
|
@@ -570,7 +1051,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
570
1051
|
variant="outline"
|
|
571
1052
|
size="sm"
|
|
572
1053
|
onClick={() => table.nextPage()}
|
|
573
|
-
disabled={!table.getCanNextPage()}
|
|
1054
|
+
disabled={isLoading || !table.getCanNextPage()}
|
|
574
1055
|
>
|
|
575
1056
|
次へ
|
|
576
1057
|
</Button>
|
|
@@ -578,7 +1059,7 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
578
1059
|
variant="outline"
|
|
579
1060
|
size="sm"
|
|
580
1061
|
onClick={() => table.lastPage()}
|
|
581
|
-
disabled={!table.getCanNextPage()}
|
|
1062
|
+
disabled={isLoading || !table.getCanNextPage()}
|
|
582
1063
|
>
|
|
583
1064
|
最後
|
|
584
1065
|
</Button>
|
|
@@ -607,8 +1088,9 @@ export const DataTableColumnVisibility = forwardRef<
|
|
|
607
1088
|
ref={ref}
|
|
608
1089
|
render={<Button variant="outline" size="sm" className={cn(className)} />}
|
|
609
1090
|
data-slot="data-table-column-visibility"
|
|
1091
|
+
aria-label="表示する列"
|
|
610
1092
|
>
|
|
611
|
-
<Columns3 className="size-4" />
|
|
1093
|
+
<Columns3 className="size-4" aria-hidden="true" />
|
|
612
1094
|
</DropdownMenuTrigger>
|
|
613
1095
|
<DropdownMenuContent align="end" className="min-w-[10rem]">
|
|
614
1096
|
{hideableColumns.map((column) => {
|
|
@@ -631,75 +1113,140 @@ export const DataTableColumnVisibility = forwardRef<
|
|
|
631
1113
|
})
|
|
632
1114
|
DataTableColumnVisibility.displayName = "DataTableColumnVisibility"
|
|
633
1115
|
|
|
1116
|
+
/* ------------------------------------------------------------------ export */
|
|
1117
|
+
|
|
1118
|
+
export type CsvCell = string | number | null | undefined
|
|
1119
|
+
export type CsvValues = "display" | "raw"
|
|
1120
|
+
export type CsvScope = "filtered" | "page"
|
|
1121
|
+
|
|
1122
|
+
// A signed number with an optional short currency / unit ("-¥1,200", "+530万", "-12.5%").
|
|
1123
|
+
const SIGNED_NUMBER = /^[+-]\s?[^\d\s=+\-@]{0,3}\d[\d,.]*[^\d\s=+\-@]{0,4}$/
|
|
1124
|
+
|
|
1125
|
+
/**
|
|
1126
|
+
* CSV 文字列を作る純関数(RFC 4180: CRLF、`,` `"` 改行を含む欄は `"` で囲み `"` は二重化)。
|
|
1127
|
+
* 既定で BOM 付き UTF-8(Excel で文字化けしない)。`guardFormulas` が true の間、先頭が
|
|
1128
|
+
* `= + - @` の文字列欄には `'` を付けて数式として実行されないようにする(数値と符号付きの数値表記は除く)。
|
|
1129
|
+
*
|
|
1130
|
+
* @example
|
|
1131
|
+
* toCsv(["月", "売上"], [["2025-01", 1200], ["2025-02", null]])
|
|
1132
|
+
* // "\uFEFF月,売上\r\n2025-01,1200\r\n2025-02,"
|
|
1133
|
+
*/
|
|
1134
|
+
export function toCsv(
|
|
1135
|
+
header: string[],
|
|
1136
|
+
rows: CsvCell[][],
|
|
1137
|
+
options: { bom?: boolean; guardFormulas?: boolean } = {},
|
|
1138
|
+
): string {
|
|
1139
|
+
const { bom = true, guardFormulas = false } = options
|
|
1140
|
+
const field = (cell: CsvCell, guard: boolean): string => {
|
|
1141
|
+
let text = cell === null || cell === undefined ? "" : String(cell)
|
|
1142
|
+
if (guard && typeof cell === "string" && /^[=+\-@]/.test(text) && !SIGNED_NUMBER.test(text)) {
|
|
1143
|
+
text = `'${text}`
|
|
1144
|
+
}
|
|
1145
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
|
|
1146
|
+
}
|
|
1147
|
+
const lines = [
|
|
1148
|
+
header.map((h) => field(h, false)).join(","),
|
|
1149
|
+
...rows.map((row) => row.map((cell) => field(cell, guardFormulas)).join(",")),
|
|
1150
|
+
]
|
|
1151
|
+
return (bom ? "\uFEFF" : "") + lines.join("\r\n")
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* 表の現在の状態から CSV を作る。行は画面と同じ並べ替え・絞り込み後の順、列は表示中の列のみ。
|
|
1156
|
+
*
|
|
1157
|
+
* @example
|
|
1158
|
+
* const csv = tableToCsv(table, { values: "raw", scope: "page" })
|
|
1159
|
+
*/
|
|
1160
|
+
export function tableToCsv<TData>(
|
|
1161
|
+
table: TanstackTable<TData>,
|
|
1162
|
+
{ values = "display", scope = "filtered" }: { values?: CsvValues; scope?: CsvScope } = {},
|
|
1163
|
+
): string {
|
|
1164
|
+
const columns = table.getVisibleLeafColumns().filter((column) => column.id !== "_select")
|
|
1165
|
+
const rows = scope === "page" ? table.getRowModel().rows : table.getSortedRowModel().rows
|
|
1166
|
+
const header = columns.map((column) =>
|
|
1167
|
+
typeof column.columnDef.header === "string" ? column.columnDef.header : column.id,
|
|
1168
|
+
)
|
|
1169
|
+
const body = rows.map((row) =>
|
|
1170
|
+
columns.map((column): CsvCell => {
|
|
1171
|
+
const value = row.getValue(column.id)
|
|
1172
|
+
const meta = column.columnDef.meta
|
|
1173
|
+
if (values === "display" && (meta?.format || meta?.dateFormat)) return formatCellText(value, row.original, meta)
|
|
1174
|
+
if (value === null || value === undefined) return ""
|
|
1175
|
+
if (typeof value === "number" || typeof value === "string") return value
|
|
1176
|
+
if (value instanceof Date) return value.toISOString()
|
|
1177
|
+
return String(value)
|
|
1178
|
+
}),
|
|
1179
|
+
)
|
|
1180
|
+
return toCsv(header, body, { guardFormulas: values === "display" })
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/** toolbar に置く CSV 書き出しボタン。`useDataTable()` の table から画面と同じ順・同じ集合を書き出す。 */
|
|
1184
|
+
export function DataTableExport({
|
|
1185
|
+
fileName = "export.csv",
|
|
1186
|
+
values = "display",
|
|
1187
|
+
scope = "filtered",
|
|
1188
|
+
children = "CSV を書き出す",
|
|
1189
|
+
className,
|
|
1190
|
+
}: DataTableExportProps) {
|
|
1191
|
+
const { table, isLoading } = useDataTable()
|
|
1192
|
+
|
|
1193
|
+
const onClick = () => {
|
|
1194
|
+
const csv = tableToCsv(table, { values, scope })
|
|
1195
|
+
const url = URL.createObjectURL(new Blob([csv], { type: "text/csv;charset=utf-8" }))
|
|
1196
|
+
const a = document.createElement("a")
|
|
1197
|
+
a.href = url
|
|
1198
|
+
a.download = fileName
|
|
1199
|
+
a.click()
|
|
1200
|
+
URL.revokeObjectURL(url)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return (
|
|
1204
|
+
<Button
|
|
1205
|
+
type="button"
|
|
1206
|
+
variant="outline"
|
|
1207
|
+
size="sm"
|
|
1208
|
+
className={className}
|
|
1209
|
+
data-slot="data-table-export"
|
|
1210
|
+
onClick={onClick}
|
|
1211
|
+
disabled={isLoading}
|
|
1212
|
+
>
|
|
1213
|
+
<Download className="size-4" aria-hidden="true" />
|
|
1214
|
+
{children}
|
|
1215
|
+
</Button>
|
|
1216
|
+
)
|
|
1217
|
+
}
|
|
1218
|
+
DataTableExport.displayName = "DataTableExport"
|
|
1219
|
+
|
|
634
1220
|
/* ------------------------------------------------------------------ preset */
|
|
635
1221
|
|
|
636
1222
|
/** The usual arrangement — toolbar, body and pagination — in one component. */
|
|
637
1223
|
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
1224
|
enableColumnVisibility = false,
|
|
656
|
-
columnVisibility,
|
|
657
|
-
onColumnVisibilityChange,
|
|
658
1225
|
columnLabels,
|
|
1226
|
+
exportable = false,
|
|
1227
|
+
exportFileName,
|
|
659
1228
|
toolbar,
|
|
660
1229
|
bulkActions,
|
|
661
|
-
|
|
662
|
-
className,
|
|
1230
|
+
...tableProps
|
|
663
1231
|
}: DataTablePresetProps<TData, TValue>) {
|
|
664
1232
|
const toolbarSlot =
|
|
665
|
-
toolbar || enableColumnVisibility ? (
|
|
1233
|
+
toolbar || enableColumnVisibility || exportable ? (
|
|
666
1234
|
<>
|
|
667
1235
|
{toolbar}
|
|
1236
|
+
{exportable && <DataTableExport fileName={exportFileName} />}
|
|
668
1237
|
{enableColumnVisibility && <DataTableColumnVisibility columnLabels={columnLabels} />}
|
|
669
1238
|
</>
|
|
670
1239
|
) : undefined
|
|
671
1240
|
|
|
672
1241
|
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
|
-
>
|
|
1242
|
+
<DataTable<TData, TValue> {...tableProps}>
|
|
696
1243
|
<DataTableToolbar
|
|
697
|
-
showSearch={enableFiltering}
|
|
1244
|
+
showSearch={tableProps.enableFiltering}
|
|
698
1245
|
toolbar={toolbarSlot}
|
|
699
1246
|
bulkActions={bulkActions as ((selectedRows: unknown[]) => ReactNode) | undefined}
|
|
700
1247
|
/>
|
|
701
1248
|
<DataTableContent />
|
|
702
|
-
{enablePagination && <DataTablePagination />}
|
|
1249
|
+
{tableProps.enablePagination && <DataTablePagination />}
|
|
703
1250
|
</DataTable>
|
|
704
1251
|
)
|
|
705
1252
|
}
|