@squadbase/vantage 0.6.2-stg.3a2ba98 → 0.7.0-dev1.19083f7
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-O5TVNH4E.js → build-U7GO26HR.js} +9 -9
- package/dist/{build-O5TVNH4E.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-WFBEDHDR.js → check-MRVAIHG5.js} +7 -7
- package/dist/{check-WFBEDHDR.js.map → check-MRVAIHG5.js.map} +1 -1
- package/dist/chunk-2IAPIOPS.js +152 -0
- package/dist/chunk-2IAPIOPS.js.map +1 -0
- package/dist/chunk-3RJEOP3J.js +174 -0
- package/dist/chunk-3RJEOP3J.js.map +1 -0
- package/dist/{chunk-QDZA5JSL.js → chunk-AABMKHEU.js} +4 -4
- package/dist/{chunk-QDZA5JSL.js.map → chunk-AABMKHEU.js.map} +1 -1
- package/dist/{chunk-PVTHHV7X.js → chunk-ANZLVSEP.js} +28 -7
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/chunk-CKWPK4RO.js +50 -0
- package/dist/chunk-CKWPK4RO.js.map +1 -0
- package/dist/chunk-EG4PX726.js +678 -0
- package/dist/chunk-EG4PX726.js.map +1 -0
- package/dist/{chunk-3G43C6OL.js → chunk-GNXCX4WE.js} +46 -3
- package/dist/chunk-GNXCX4WE.js.map +1 -0
- package/dist/chunk-NYQJ5UY4.js +9028 -0
- package/dist/chunk-NYQJ5UY4.js.map +1 -0
- package/dist/chunk-NZ5QIIG4.js +74 -0
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-BCE22JSM.js → chunk-PJ4SPXHS.js} +52 -16
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-RPZAALCX.js → chunk-QNOX7WLZ.js} +256 -165
- package/dist/chunk-QNOX7WLZ.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-PXRX5PEC.js → chunk-ROHNXP7J.js} +73 -4
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/chunk-S5MDTHH7.js +466 -0
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/chunk-SDJTTMLK.js +1877 -0
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-EP6JHXSR.js → chunk-SHZM3EXB.js} +68 -12
- package/dist/chunk-SHZM3EXB.js.map +1 -0
- package/dist/chunk-TRMHNMMZ.js +61 -0
- package/dist/chunk-TRMHNMMZ.js.map +1 -0
- package/dist/{chunk-A2UUGASH.js → chunk-U736PVHY.js} +2 -2
- package/dist/chunk-U736PVHY.js.map +1 -0
- package/dist/{chunk-JGRMRXXZ.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +15 -11
- package/dist/cli.js.map +1 -1
- package/dist/client/index.d.ts +34 -3
- package/dist/client/index.js +211 -12
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +2113 -30
- package/dist/components/index.js +7 -4
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-NZRLOQCL.js → dev-O42JFQ64.js} +24 -13
- package/dist/dev-O42JFQ64.js.map +1 -0
- package/dist/doc/index.d.ts +215 -2
- package/dist/doc/index.js +423 -5
- package/dist/doc/index.js.map +1 -1
- package/dist/{docs-ZGDJUHJI.js → docs-JVP4RPKT.js} +3 -3
- package/dist/{docs-ZGDJUHJI.js.map → docs-JVP4RPKT.js.map} +1 -1
- package/dist/{doctor-VFKEMR54.js → doctor-YD3XAAOT.js} +8 -8
- package/dist/doctor-YD3XAAOT.js.map +1 -0
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/geo-japan-7BMSOJRX.js +8 -0
- package/dist/geo-japan-7BMSOJRX.js.map +1 -0
- package/dist/geo-us-GVOG7VXH.js +8 -0
- package/dist/geo-us-GVOG7VXH.js.map +1 -0
- package/dist/geo-world-35HL56UU.js +8 -0
- package/dist/geo-world-35HL56UU.js.map +1 -0
- package/dist/markdown/index.js +1 -1
- package/dist/markdown/index.js.map +1 -1
- package/dist/{preview-QDCIVVSR.js → preview-T6CLSQVX.js} +12 -4
- package/dist/preview-T6CLSQVX.js.map +1 -0
- package/dist/query/index.d.ts +0 -11
- package/dist/query/index.js +2 -2
- package/dist/{routes-RHP5FVGD.js → routes-PVKNT777.js} +6 -6
- package/dist/{routes-RHP5FVGD.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/{search-RFBELGY7.js → search-5LC4XPB5.js} +3 -3
- package/dist/{search-RFBELGY7.js.map → search-5LC4XPB5.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1086 -19
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +14 -10
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +148 -12
- package/dist/ui/index.js +1340 -88
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-GF2P67M6.js → upgrade-6DT7ADRP.js} +7 -7
- package/dist/{upgrade-GF2P67M6.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.d.ts +10 -0
- package/dist/vite/index.js +4 -4
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +82 -0
- package/docs/en/cli-reference.md +30 -0
- package/docs/en/components.md +42 -17
- package/docs/en/doc/delta-text.md +40 -0
- package/docs/en/doc/doc-action-items.md +49 -0
- package/docs/en/doc/doc-callout.md +38 -0
- package/docs/en/doc/doc-data-table.md +60 -0
- package/docs/en/doc/doc-executive-summary.md +64 -0
- package/docs/en/doc/doc-figure.md +56 -0
- package/docs/en/doc/doc-key-findings.md +61 -0
- package/docs/en/doc/doc-kpi-scorecard.md +80 -0
- package/docs/en/doc/doc-notes-and-sources.md +43 -0
- package/docs/en/doc/doc-report-header.md +46 -0
- package/docs/en/pages-and-metadata.md +15 -0
- package/docs/en/parts/area-chart.md +99 -0
- package/docs/en/parts/bar-chart.md +91 -0
- package/docs/en/parts/bar-list.md +50 -0
- package/docs/en/parts/category-bar.md +37 -0
- package/docs/en/parts/chart-frame.md +39 -0
- package/docs/en/parts/data-table.md +22 -0
- package/docs/en/parts/delta.md +51 -0
- package/docs/en/parts/donut-chart.md +72 -0
- package/docs/en/parts/format.md +43 -0
- package/docs/en/parts/funnel-steps.md +2 -1
- package/docs/en/parts/heatmap.md +53 -0
- package/docs/en/parts/kpi-card.md +58 -0
- package/docs/en/parts/legend.md +35 -0
- package/docs/en/parts/line-chart.md +94 -0
- package/docs/en/parts/metric-value.md +3 -2
- package/docs/en/parts/page-shell.md +1 -1
- package/docs/en/parts/progress-circle.md +33 -0
- package/docs/en/parts/sample-data.md +39 -0
- package/docs/en/parts/scatter-chart.md +56 -0
- package/docs/en/parts/sparkline.md +1 -1
- package/docs/en/parts/tracker.md +36 -0
- package/docs/en/parts/trend-indicator.md +17 -1
- package/docs/en/parts/updated-at.md +30 -0
- package/docs/en/parts/waterfall-chart.md +56 -0
- package/docs/en/slide/slide-chart-full.md +48 -0
- package/docs/en/slide/slide-chart-insight.md +52 -0
- package/docs/en/slide/slide-columns.md +46 -0
- package/docs/en/slide/slide-comparison.md +68 -0
- package/docs/en/slide/slide-cover.md +39 -0
- package/docs/en/slide/slide-exec-summary.md +47 -0
- package/docs/en/slide/slide-heading.md +38 -0
- package/docs/en/slide/slide-kpi-grid.md +55 -0
- package/docs/en/slide/slide-kpi-hero.md +58 -0
- package/docs/en/slide/slide-next-actions.md +49 -0
- package/docs/en/slide/slide-scorecard-table.md +54 -0
- package/docs/en/slide/slide-section.md +44 -0
- package/docs/en/slide/slide-source-line.md +34 -0
- package/docs/en/ui/alert-dialog.md +42 -0
- package/docs/en/ui/avatar.md +31 -0
- package/docs/en/ui/combobox.md +74 -0
- package/docs/en/ui/drawer.md +41 -0
- package/docs/en/ui/empty.md +1 -1
- package/docs/en/ui/error-state.md +4 -3
- package/docs/en/ui/field.md +42 -0
- package/docs/en/ui/hover-card.md +21 -0
- package/docs/en/ui/kbd.md +17 -0
- package/docs/en/ui/loading.md +1 -1
- package/docs/en/ui/pagination.md +36 -0
- package/docs/en/ui/radio-group.md +21 -0
- package/docs/en/ui/toast.md +44 -0
- package/docs/en/ui/toggle-group.md +4 -3
- package/docs/en/ui-and-theming.md +1 -1
- package/docs/index.json +1184 -114
- package/docs/ja/agent-skills.md +1 -1
- package/docs/ja/changelog.md +84 -0
- package/docs/ja/cli-reference.md +29 -0
- package/docs/ja/components.md +68 -101
- package/docs/ja/doc/delta-text.md +40 -0
- package/docs/ja/doc/doc-action-items.md +49 -0
- package/docs/ja/doc/doc-callout.md +38 -0
- package/docs/ja/doc/doc-data-table.md +60 -0
- package/docs/ja/doc/doc-executive-summary.md +64 -0
- package/docs/ja/doc/doc-figure.md +56 -0
- package/docs/ja/doc/doc-key-findings.md +61 -0
- package/docs/ja/doc/doc-kpi-scorecard.md +80 -0
- package/docs/ja/doc/doc-notes-and-sources.md +43 -0
- package/docs/ja/doc/doc-report-header.md +46 -0
- 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 +133 -0
- package/docs/ja/page-types.md +47 -0
- package/docs/ja/pages-and-metadata.md +15 -0
- package/docs/ja/parts/area-chart.md +85 -0
- package/docs/ja/parts/bar-chart.md +95 -0
- package/docs/ja/parts/bar-list.md +58 -0
- package/docs/ja/parts/category-bar.md +36 -0
- package/docs/ja/parts/chart-frame.md +36 -0
- package/docs/ja/parts/data-table.md +22 -0
- package/docs/ja/parts/delta.md +51 -0
- package/docs/ja/parts/donut-chart.md +66 -0
- package/docs/ja/parts/echart.md +3 -0
- package/docs/ja/parts/format.md +44 -0
- package/docs/ja/parts/funnel-steps.md +27 -8
- package/docs/ja/parts/heatmap.md +45 -0
- package/docs/ja/parts/kpi-card.md +56 -0
- package/docs/ja/parts/legend.md +36 -0
- package/docs/ja/parts/line-chart.md +84 -0
- package/docs/ja/parts/metric-value.md +3 -2
- package/docs/ja/parts/page-shell.md +1 -1
- package/docs/ja/parts/progress-circle.md +34 -0
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -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/parts/updated-at.md +27 -0
- package/docs/ja/parts/waterfall-chart.md +61 -0
- package/docs/ja/routing.md +8 -86
- package/docs/ja/slide/slide-chart-full.md +48 -0
- package/docs/ja/slide/slide-chart-insight.md +52 -0
- package/docs/ja/slide/slide-columns.md +46 -0
- package/docs/ja/slide/slide-comparison.md +68 -0
- package/docs/ja/slide/slide-cover.md +39 -0
- package/docs/ja/slide/slide-exec-summary.md +47 -0
- package/docs/ja/slide/slide-heading.md +38 -0
- package/docs/ja/slide/slide-kpi-grid.md +55 -0
- package/docs/ja/slide/slide-kpi-hero.md +58 -0
- package/docs/ja/slide/slide-next-actions.md +49 -0
- package/docs/ja/slide/slide-scorecard-table.md +54 -0
- package/docs/ja/slide/slide-section.md +44 -0
- package/docs/ja/slide/slide-source-line.md +34 -0
- 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/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- 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/loading.md +1 -1
- 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/toggle-group.md +4 -3
- package/docs/ja/ui-and-theming.md +14 -7
- package/package.json +43 -29
- 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 +424 -0
- package/registry/ui/area-chart.tsx +3960 -0
- package/registry/ui/attention-list.tsx +465 -0
- package/registry/ui/avatar.tsx +110 -0
- package/registry/ui/badge.tsx +56 -0
- package/registry/ui/bar-chart.tsx +3954 -0
- package/registry/ui/bar-list.tsx +3998 -0
- package/registry/ui/box-plot-chart.tsx +4214 -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 +1498 -0
- package/registry/ui/chart-frame.tsx +1604 -0
- package/registry/ui/checkbox.tsx +30 -0
- package/registry/ui/collapsible.tsx +23 -0
- package/registry/ui/combo-chart.tsx +1851 -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 +896 -157
- package/registry/ui/date-range-picker.tsx +544 -0
- package/registry/ui/delta.tsx +350 -0
- package/registry/ui/dialog.tsx +161 -0
- package/registry/ui/donut-chart.tsx +4006 -0
- package/registry/ui/drawer.tsx +229 -0
- package/registry/ui/dropdown-menu.tsx +274 -0
- package/registry/ui/echart.tsx +1178 -0
- package/registry/ui/field.tsx +240 -0
- package/registry/ui/filter-bar.tsx +551 -0
- package/registry/ui/funnel-steps.tsx +3843 -0
- package/registry/ui/heatmap.tsx +2027 -0
- package/registry/ui/histogram-chart.tsx +4284 -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 +905 -0
- package/registry/ui/label.tsx +22 -0
- package/registry/ui/legend.tsx +3817 -0
- package/registry/ui/line-chart.tsx +3958 -0
- package/registry/ui/metric-value.tsx +62 -0
- package/registry/ui/multi-select.tsx +382 -0
- package/registry/ui/page-shell.tsx +420 -0
- package/registry/ui/pagination.tsx +140 -0
- package/registry/ui/paired-chart.tsx +4086 -0
- package/registry/ui/pivot-table.tsx +1259 -0
- package/registry/ui/placeholder.tsx +36 -0
- package/registry/ui/popover.tsx +92 -0
- package/registry/ui/progress-circle.tsx +1033 -0
- package/registry/ui/progress.tsx +85 -0
- package/registry/ui/radio-group.tsx +39 -0
- package/registry/ui/sample-data.tsx +230 -0
- package/registry/ui/scatter-chart.tsx +4503 -0
- package/registry/ui/scroll-area.tsx +56 -0
- package/registry/ui/searchable-select.tsx +352 -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 +247 -0
- package/registry/ui/status-badge.tsx +286 -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 +72 -0
- package/registry/ui/tracker.tsx +1015 -0
- package/registry/ui/treemap.tsx +1916 -0
- package/registry/ui/trend-indicator.tsx +217 -0
- package/registry/ui/updated-at.tsx +276 -0
- package/registry/ui/waterfall-chart.tsx +4561 -0
- package/skills/vantage-add-feature/SKILL.md +14 -7
- package/skills/vantage-design/SKILL.md +62 -52
- package/skills/vantage-design/references/extract.md +26 -27
- package/skills/vantage-design/references/format.md +12 -172
- package/skills/vantage-design/references/presets.md +50 -110
- package/skills/vantage-pitfalls/SKILL.md +10 -1
- package/skills/vantage-slide/SKILL.md +71 -36
- package/templates/AGENTS.md +54 -16
- package/templates/agents/api.md +36 -0
- package/templates/agents/csv.md +2 -1
- package/templates/agents/doc.md +52 -3
- package/templates/agents/page.md +91 -10
- package/templates/agents/slide.md +90 -4
- package/templates/agents/xlsx.md +2 -1
- package/theme.css +247 -132
- package/dist/add-EISGSZAH.js +0 -6
- package/dist/add-EISGSZAH.js.map +0 -1
- package/dist/chunk-3G43C6OL.js.map +0 -1
- package/dist/chunk-5ZOP4SQR.js +0 -47
- package/dist/chunk-5ZOP4SQR.js.map +0 -1
- package/dist/chunk-A2UUGASH.js.map +0 -1
- package/dist/chunk-BCE22JSM.js.map +0 -1
- package/dist/chunk-DTDVSFRY.js +0 -29
- package/dist/chunk-DTDVSFRY.js.map +0 -1
- package/dist/chunk-EP6JHXSR.js.map +0 -1
- package/dist/chunk-JGRMRXXZ.js.map +0 -1
- package/dist/chunk-KLEIVDUV.js +0 -165
- package/dist/chunk-KLEIVDUV.js.map +0 -1
- package/dist/chunk-LSRMRIXA.js +0 -2227
- package/dist/chunk-LSRMRIXA.js.map +0 -1
- package/dist/chunk-PVTHHV7X.js.map +0 -1
- package/dist/chunk-PXRX5PEC.js.map +0 -1
- package/dist/chunk-RPZAALCX.js.map +0 -1
- package/dist/chunk-T3FLAGI6.js +0 -316
- package/dist/chunk-T3FLAGI6.js.map +0 -1
- package/dist/dev-NZRLOQCL.js.map +0 -1
- package/dist/doctor-VFKEMR54.js.map +0 -1
- 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
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ...
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
:
|
|
1076
|
+
: sorted === "desc"
|
|
460
1077
|
? "descending"
|
|
461
1078
|
: "none"
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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 =
|
|
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={
|
|
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
|
-
{
|
|
544
|
-
<SelectItem key={
|
|
545
|
-
{
|
|
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
|
-
|
|
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
|
}
|