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