@squadbase/vantage 0.6.2-dev1.42c4e48 → 0.6.2-dev1.51e401a
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/{build-MFWRPYKW.js → build-U7GO26HR.js} +7 -7
- package/dist/{build-MFWRPYKW.js.map → build-U7GO26HR.js.map} +1 -1
- package/dist/{check-ZBIH422T.js → check-MRVAIHG5.js} +5 -5
- package/dist/{check-ZBIH422T.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-77CISBMG.js +7581 -0
- package/dist/chunk-77CISBMG.js.map +1 -0
- package/dist/{chunk-V3LF7BE7.js → chunk-ANZLVSEP.js} +4 -5
- package/dist/chunk-ANZLVSEP.js.map +1 -0
- package/dist/{chunk-XQUSGIDG.js → chunk-CKWPK4RO.js} +10 -3
- 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-NZ5QIIG4.js +74 -0
- package/dist/chunk-NZ5QIIG4.js.map +1 -0
- package/dist/{chunk-7DZGJQ3P.js → chunk-PJ4SPXHS.js} +23 -6
- package/dist/chunk-PJ4SPXHS.js.map +1 -0
- package/dist/{chunk-OB6UBJXF.js → chunk-QNOX7WLZ.js} +127 -36
- package/dist/chunk-QNOX7WLZ.js.map +1 -0
- package/dist/{chunk-PXRX5PEC.js → chunk-ROHNXP7J.js} +73 -4
- package/dist/chunk-ROHNXP7J.js.map +1 -0
- package/dist/{chunk-CEXAQQ5W.js → chunk-S5MDTHH7.js} +180 -18
- package/dist/chunk-S5MDTHH7.js.map +1 -0
- package/dist/{chunk-IBGOCWJB.js → chunk-SDJTTMLK.js} +247 -86
- package/dist/chunk-SDJTTMLK.js.map +1 -0
- package/dist/{chunk-QEWCYW3I.js → chunk-SHZM3EXB.js} +29 -8
- 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-JKLLFVEU.js → chunk-UWSQUHX6.js} +4 -99
- package/dist/chunk-UWSQUHX6.js.map +1 -0
- package/dist/cli.js +6 -6
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/components/index.d.ts +1617 -101
- package/dist/components/index.js +7 -4
- package/dist/delta-text-CbrOaW83.d.ts +17 -0
- package/dist/{dev-DCTOHQ6W.js → dev-O42JFQ64.js} +7 -7
- package/dist/{dev-DCTOHQ6W.js.map → dev-O42JFQ64.js.map} +1 -1
- 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/{doctor-5E2VEZPK.js → doctor-YD3XAAOT.js} +5 -5
- package/dist/{doctor-5E2VEZPK.js.map → doctor-YD3XAAOT.js.map} +1 -1
- package/dist/format-DihQkL1N.d.ts +249 -0
- package/dist/markdown/index.js +1 -1
- package/dist/markdown/index.js.map +1 -1
- package/dist/query/index.js +2 -2
- package/dist/{routes-3SGAFDMC.js → routes-PVKNT777.js} +4 -4
- package/dist/{routes-3SGAFDMC.js.map → routes-PVKNT777.js.map} +1 -1
- package/dist/slide/index.d.ts +272 -1
- package/dist/slide/index.js +1025 -4
- package/dist/slide/index.js.map +1 -1
- package/dist/table/index.js +11 -8
- package/dist/table/index.js.map +1 -1
- package/dist/ui/index.d.ts +8 -2
- package/dist/ui/index.js +22 -112
- package/dist/ui/index.js.map +1 -1
- package/dist/{upgrade-DBBE6GIR.js → upgrade-6DT7ADRP.js} +4 -4
- package/dist/{upgrade-DBBE6GIR.js.map → upgrade-6DT7ADRP.js.map} +1 -1
- package/dist/vite/index.js +3 -3
- package/docs/en/agent-skills.md +1 -1
- package/docs/en/changelog.md +2 -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/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 +917 -77
- package/docs/ja/agent-skills.md +1 -1
- package/docs/ja/changelog.md +2 -0
- package/docs/ja/components.md +39 -16
- 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/slide.md +1 -0
- package/docs/ja/pages-and-metadata.md +15 -0
- package/docs/ja/parts/area-chart.md +49 -25
- package/docs/ja/parts/bar-chart.md +50 -24
- package/docs/ja/parts/bar-list.md +31 -8
- 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 +45 -26
- 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 +48 -22
- package/docs/ja/parts/metric-value.md +3 -2
- package/docs/ja/parts/sample-data.md +61 -0
- package/docs/ja/parts/scatter-chart.md +69 -0
- package/docs/ja/parts/sparkline.md +1 -1
- 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/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/empty.md +1 -1
- package/docs/ja/ui/error-state.md +4 -3
- package/docs/ja/ui/loading.md +1 -1
- package/docs/ja/ui/toggle-group.md +4 -3
- package/docs/ja/ui-and-theming.md +2 -2
- package/package.json +26 -24
- package/registry/ui/accordion.tsx +4 -2
- package/registry/ui/alert-dialog.tsx +3 -3
- package/registry/ui/alert.tsx +3 -1
- package/registry/ui/app-shell.tsx +145 -2
- package/registry/ui/area-chart.tsx +3111 -166
- package/registry/ui/attention-list.tsx +465 -0
- package/registry/ui/avatar.tsx +1 -1
- package/registry/ui/badge.tsx +3 -1
- package/registry/ui/bar-chart.tsx +3105 -164
- package/registry/ui/bar-list.tsx +3877 -181
- package/registry/ui/box-plot-chart.tsx +4214 -0
- package/registry/ui/breadcrumb.tsx +3 -1
- package/registry/ui/button.tsx +3 -1
- package/registry/ui/calendar.tsx +3 -1
- package/registry/ui/card.tsx +4 -2
- package/registry/ui/category-bar.tsx +1233 -16
- package/registry/ui/chart-frame.tsx +1604 -0
- package/registry/ui/checkbox.tsx +3 -1
- package/registry/ui/collapsible.tsx +3 -1
- package/registry/ui/combobox.tsx +2 -2
- package/registry/ui/command.tsx +3 -1
- package/registry/ui/dashboard-card.tsx +71 -28
- package/registry/ui/data-table.tsx +882 -68
- package/registry/ui/date-range-picker.tsx +400 -65
- package/registry/ui/delta.tsx +350 -0
- package/registry/ui/dialog.tsx +5 -3
- package/registry/ui/donut-chart.tsx +3184 -182
- package/registry/ui/drawer.tsx +1 -1
- package/registry/ui/dropdown-menu.tsx +5 -3
- package/registry/ui/echart.tsx +761 -69
- package/registry/ui/field.tsx +1 -1
- package/registry/ui/filter-bar.tsx +212 -26
- package/registry/ui/funnel-steps.tsx +3622 -53
- package/registry/ui/heatmap.tsx +2027 -0
- package/registry/ui/histogram-chart.tsx +4284 -0
- package/registry/ui/hover-card.tsx +2 -2
- package/registry/ui/input-group.tsx +3 -1
- package/registry/ui/input.tsx +3 -1
- package/registry/ui/kbd.tsx +3 -1
- package/registry/ui/kpi-card.tsx +905 -0
- package/registry/ui/label.tsx +3 -1
- package/registry/ui/legend.tsx +3817 -0
- package/registry/ui/line-chart.tsx +3109 -158
- package/registry/ui/metric-value.tsx +9 -1
- package/registry/ui/multi-select.tsx +170 -14
- package/registry/ui/page-shell.tsx +244 -51
- package/registry/ui/pagination.tsx +3 -1
- package/registry/ui/paired-chart.tsx +4086 -0
- package/registry/ui/placeholder.tsx +8 -2
- package/registry/ui/popover.tsx +4 -2
- package/registry/ui/progress-circle.tsx +773 -12
- package/registry/ui/progress.tsx +3 -1
- package/registry/ui/radio-group.tsx +1 -1
- package/registry/ui/sample-data.tsx +230 -0
- package/registry/ui/scatter-chart.tsx +4503 -0
- package/registry/ui/scroll-area.tsx +3 -2
- package/registry/ui/searchable-select.tsx +153 -10
- package/registry/ui/section-header.tsx +5 -3
- package/registry/ui/segmented-control.tsx +7 -2
- package/registry/ui/select.tsx +4 -2
- package/registry/ui/separator.tsx +3 -1
- package/registry/ui/sheet.tsx +4 -2
- package/registry/ui/sidebar.tsx +4 -2
- package/registry/ui/skeleton.tsx +4 -2
- package/registry/ui/slider.tsx +4 -2
- package/registry/ui/sparkline.tsx +84 -9
- package/registry/ui/spinner.tsx +3 -1
- package/registry/ui/states.tsx +194 -8
- package/registry/ui/status-badge.tsx +246 -39
- package/registry/ui/switch.tsx +3 -1
- package/registry/ui/table.tsx +4 -2
- package/registry/ui/tabs.tsx +5 -3
- package/registry/ui/textarea.tsx +3 -1
- package/registry/ui/toast.tsx +2 -2
- package/registry/ui/toggle-group.tsx +3 -1
- package/registry/ui/toggle.tsx +3 -1
- package/registry/ui/tooltip.tsx +10 -4
- package/registry/ui/tracker.tsx +798 -19
- package/registry/ui/trend-indicator.tsx +164 -9
- package/registry/ui/updated-at.tsx +276 -0
- package/registry/ui/waterfall-chart.tsx +4561 -0
- package/skills/vantage-add-feature/SKILL.md +5 -0
- package/skills/vantage-design/SKILL.md +60 -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 +5 -0
- package/skills/vantage-slide/SKILL.md +54 -36
- package/templates/AGENTS.md +45 -16
- package/templates/agents/api.md +7 -0
- package/templates/agents/doc.md +48 -0
- package/templates/agents/page.md +69 -2
- package/templates/agents/slide.md +77 -3
- package/theme.css +247 -149
- package/dist/chunk-5ZOP4SQR.js +0 -47
- package/dist/chunk-5ZOP4SQR.js.map +0 -1
- package/dist/chunk-7DZGJQ3P.js.map +0 -1
- package/dist/chunk-A2UUGASH.js.map +0 -1
- package/dist/chunk-AG6RZ2RR.js +0 -3208
- package/dist/chunk-AG6RZ2RR.js.map +0 -1
- package/dist/chunk-CEXAQQ5W.js.map +0 -1
- package/dist/chunk-IBGOCWJB.js.map +0 -1
- package/dist/chunk-JKLLFVEU.js.map +0 -1
- package/dist/chunk-OB6UBJXF.js.map +0 -1
- package/dist/chunk-PXRX5PEC.js.map +0 -1
- package/dist/chunk-QEWCYW3I.js.map +0 -1
- package/dist/chunk-V3LF7BE7.js.map +0 -1
- package/dist/chunk-XQUSGIDG.js.map +0 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Copied by `vantage add ui data-table`. This file is yours to edit.
|
|
2
|
-
// Generated from @squadbase/
|
|
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
|
+
|
|
3
5
|
/**
|
|
4
6
|
* DataTable — a compound table over TanStack Table. `<DataTable>` owns the
|
|
5
7
|
* table instance and publishes it on context; the toolbar, body, pagination and
|
|
@@ -15,7 +17,9 @@ import {
|
|
|
15
17
|
forwardRef,
|
|
16
18
|
useCallback,
|
|
17
19
|
useContext,
|
|
20
|
+
useEffect,
|
|
18
21
|
useMemo,
|
|
22
|
+
useRef,
|
|
19
23
|
useState,
|
|
20
24
|
type HTMLAttributes,
|
|
21
25
|
type ReactNode,
|
|
@@ -28,18 +32,36 @@ import {
|
|
|
28
32
|
getPaginationRowModel,
|
|
29
33
|
getSortedRowModel,
|
|
30
34
|
useReactTable,
|
|
35
|
+
type Column,
|
|
31
36
|
type ColumnDef,
|
|
32
37
|
type ColumnFiltersState,
|
|
33
38
|
type OnChangeFn,
|
|
34
39
|
type PaginationState,
|
|
40
|
+
type RowData,
|
|
35
41
|
type RowSelectionState,
|
|
36
42
|
type SortingState,
|
|
37
43
|
type Table as TanstackTable,
|
|
38
44
|
type TableMeta,
|
|
39
45
|
type VisibilityState,
|
|
40
46
|
} from "@tanstack/react-table"
|
|
41
|
-
import {
|
|
42
|
-
|
|
47
|
+
import {
|
|
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"
|
|
43
65
|
import { cn } from "@squadbase/vantage/ui"
|
|
44
66
|
import { Button } from "@squadbase/vantage/ui"
|
|
45
67
|
import { Checkbox } from "@squadbase/vantage/ui"
|
|
@@ -50,11 +72,335 @@ import {
|
|
|
50
72
|
DropdownMenuTrigger,
|
|
51
73
|
} from "@squadbase/vantage/ui"
|
|
52
74
|
import { Input } from "@squadbase/vantage/ui"
|
|
75
|
+
import { Skeleton } from "@squadbase/vantage/ui"
|
|
53
76
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@squadbase/vantage/ui"
|
|
54
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"])
|
|
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 ---------- */
|
|
55
212
|
|
|
56
213
|
/* ------------------------------------------------------------------- types */
|
|
57
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
|
+
|
|
58
404
|
interface DataTableContextValue<TData> {
|
|
59
405
|
table: TanstackTable<TData>
|
|
60
406
|
selectedRows: TData[]
|
|
@@ -65,6 +411,7 @@ interface DataTableContextValue<TData> {
|
|
|
65
411
|
enablePagination: boolean
|
|
66
412
|
enableRowSelection: boolean
|
|
67
413
|
visibleColumnCount: number
|
|
414
|
+
isLoading: boolean
|
|
68
415
|
}
|
|
69
416
|
|
|
70
417
|
/**
|
|
@@ -80,7 +427,8 @@ export interface DataTableProps<TData, TValue> {
|
|
|
80
427
|
* literal が `string` に拡張され型エラーになる。
|
|
81
428
|
*
|
|
82
429
|
* @example
|
|
83
|
-
* import type
|
|
430
|
+
* import { DataTable, type ColumnDef } from "@/components/data-table"
|
|
431
|
+
* // Vantage: import { DataTable, type ColumnDef } from "@squadbase/vantage/components"
|
|
84
432
|
*
|
|
85
433
|
* const columns: ColumnDef<User>[] = [
|
|
86
434
|
* { accessorKey: "id", header: "ID" },
|
|
@@ -107,7 +455,42 @@ export interface DataTableProps<TData, TValue> {
|
|
|
107
455
|
enablePagination?: boolean
|
|
108
456
|
pagination?: PaginationState
|
|
109
457
|
onPaginationChange?: OnChangeFn<PaginationState>
|
|
458
|
+
/**
|
|
459
|
+
* サーバー側ページング時の総ページ数。指定すると manualPagination になる。
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* <DataTable pageCount={data.pageCount} … />
|
|
463
|
+
*/
|
|
110
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
|
|
111
494
|
|
|
112
495
|
// Row selection
|
|
113
496
|
enableRowSelection?: boolean
|
|
@@ -118,6 +501,13 @@ export interface DataTableProps<TData, TValue> {
|
|
|
118
501
|
columnVisibility?: VisibilityState
|
|
119
502
|
onColumnVisibilityChange?: OnChangeFn<VisibilityState>
|
|
120
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
|
+
*/
|
|
121
511
|
meta?: TableMeta<TData>
|
|
122
512
|
className?: string
|
|
123
513
|
|
|
@@ -133,6 +523,33 @@ export interface DataTableToolbarProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
133
523
|
|
|
134
524
|
export interface DataTableContentProps extends HTMLAttributes<HTMLDivElement> {}
|
|
135
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
|
+
|
|
136
553
|
export interface DataTablePaginationProps extends HTMLAttributes<HTMLDivElement> {
|
|
137
554
|
pageSizeOptions?: number[]
|
|
138
555
|
}
|
|
@@ -150,6 +567,20 @@ export type DataTablePresetProps<TData, TValue> = Omit<
|
|
|
150
567
|
> & {
|
|
151
568
|
enableColumnVisibility?: boolean
|
|
152
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
|
|
153
584
|
|
|
154
585
|
/** Extension slots */
|
|
155
586
|
toolbar?: ReactNode
|
|
@@ -194,9 +625,12 @@ export function useDataTable<TData>(): DataTableContextValue<TData> {
|
|
|
194
625
|
|
|
195
626
|
/* -------------------------------------------------------------------- root */
|
|
196
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
|
+
|
|
197
631
|
export function DataTable<TData, TValue>({
|
|
198
|
-
columns,
|
|
199
|
-
data,
|
|
632
|
+
columns = NO_ROWS,
|
|
633
|
+
data = NO_ROWS,
|
|
200
634
|
enableSorting = true,
|
|
201
635
|
sorting: sortingProp,
|
|
202
636
|
onSortingChange,
|
|
@@ -209,6 +643,10 @@ export function DataTable<TData, TValue>({
|
|
|
209
643
|
pagination: paginationProp,
|
|
210
644
|
onPaginationChange,
|
|
211
645
|
pageCount,
|
|
646
|
+
rowCount,
|
|
647
|
+
manualFiltering,
|
|
648
|
+
manualSorting,
|
|
649
|
+
isLoading = false,
|
|
212
650
|
enableRowSelection = false,
|
|
213
651
|
rowSelection: rowSelectionProp,
|
|
214
652
|
onRowSelectionChange,
|
|
@@ -218,6 +656,8 @@ export function DataTable<TData, TValue>({
|
|
|
218
656
|
className,
|
|
219
657
|
children,
|
|
220
658
|
}: DataTableProps<TData, TValue>) {
|
|
659
|
+
const locale = useLocale()
|
|
660
|
+
const t = TEXT[uiLanguage(locale)]
|
|
221
661
|
const [sorting, setSorting] = useControlledState<SortingState>(sortingProp, [], onSortingChange)
|
|
222
662
|
|
|
223
663
|
const [columnFilters, setColumnFilters] = useControlledState<ColumnFiltersState>(
|
|
@@ -252,7 +692,29 @@ export function DataTable<TData, TValue>({
|
|
|
252
692
|
|
|
253
693
|
// Row selection prepends a checkbox column that the caller never declares.
|
|
254
694
|
const allColumns = useMemo<ColumnDef<TData, TValue>[]>(() => {
|
|
255
|
-
|
|
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
|
|
256
718
|
|
|
257
719
|
const selectionColumn: ColumnDef<TData, TValue> = {
|
|
258
720
|
id: "_select",
|
|
@@ -261,24 +723,40 @@ export function DataTable<TData, TValue>({
|
|
|
261
723
|
checked={table.getIsAllPageRowsSelected()}
|
|
262
724
|
indeterminate={table.getIsSomePageRowsSelected()}
|
|
263
725
|
onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
|
|
264
|
-
aria-label=
|
|
726
|
+
aria-label={t.selectAll}
|
|
265
727
|
/>
|
|
266
728
|
),
|
|
267
729
|
cell: ({ row }) => (
|
|
268
730
|
<Checkbox
|
|
269
731
|
checked={row.getIsSelected()}
|
|
270
732
|
onCheckedChange={(checked) => row.toggleSelected(checked)}
|
|
271
|
-
aria-label=
|
|
733
|
+
aria-label={t.selectRow}
|
|
272
734
|
/>
|
|
273
735
|
),
|
|
274
736
|
enableSorting: false,
|
|
275
737
|
enableHiding: false,
|
|
276
738
|
}
|
|
277
739
|
|
|
278
|
-
return [selectionColumn, ...
|
|
279
|
-
}, [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])
|
|
280
757
|
|
|
281
758
|
const table = useReactTable({
|
|
759
|
+
autoResetPageIndex: false,
|
|
282
760
|
data,
|
|
283
761
|
columns: allColumns,
|
|
284
762
|
state: {
|
|
@@ -297,7 +775,11 @@ export function DataTable<TData, TValue>({
|
|
|
297
775
|
onRowSelectionChange: setRowSelection,
|
|
298
776
|
onColumnVisibilityChange: setColumnVisibility,
|
|
299
777
|
...(enablePagination ? { onPaginationChange: setPagination } : {}),
|
|
300
|
-
...(pageCount !== undefined
|
|
778
|
+
...(pageCount !== undefined || rowCount !== undefined
|
|
779
|
+
? { pageCount, rowCount, manualPagination: true }
|
|
780
|
+
: {}),
|
|
781
|
+
...(manualFiltering ? { manualFiltering: true } : {}),
|
|
782
|
+
...(manualSorting ? { manualSorting: true } : {}),
|
|
301
783
|
getCoreRowModel: getCoreRowModel(),
|
|
302
784
|
getSortedRowModel: getSortedRowModel(),
|
|
303
785
|
getFilteredRowModel: getFilteredRowModel(),
|
|
@@ -325,6 +807,7 @@ export function DataTable<TData, TValue>({
|
|
|
325
807
|
enablePagination,
|
|
326
808
|
enableRowSelection,
|
|
327
809
|
visibleColumnCount: allColumns.length,
|
|
810
|
+
isLoading,
|
|
328
811
|
}),
|
|
329
812
|
// `useReactTable` returns the same object every render, so each piece of
|
|
330
813
|
// table state has to be tracked explicitly to re-render the consumers.
|
|
@@ -339,6 +822,8 @@ export function DataTable<TData, TValue>({
|
|
|
339
822
|
enablePagination,
|
|
340
823
|
enableRowSelection,
|
|
341
824
|
allColumns.length,
|
|
825
|
+
isLoading,
|
|
826
|
+
rowCount,
|
|
342
827
|
sorting,
|
|
343
828
|
columnFilters,
|
|
344
829
|
columnVisibility,
|
|
@@ -349,7 +834,7 @@ export function DataTable<TData, TValue>({
|
|
|
349
834
|
|
|
350
835
|
return (
|
|
351
836
|
<DataTableContext.Provider value={contextValue as DataTableContextValue<unknown>}>
|
|
352
|
-
<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)}>
|
|
353
838
|
{children}
|
|
354
839
|
</div>
|
|
355
840
|
</DataTableContext.Provider>
|
|
@@ -357,6 +842,76 @@ export function DataTable<TData, TValue>({
|
|
|
357
842
|
}
|
|
358
843
|
DataTable.displayName = "DataTable"
|
|
359
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
|
+
|
|
360
915
|
/* ----------------------------------------------------------------- toolbar */
|
|
361
916
|
|
|
362
917
|
export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps>(
|
|
@@ -399,63 +954,200 @@ DataTableToolbar.displayName = "DataTableToolbar"
|
|
|
399
954
|
|
|
400
955
|
/* ----------------------------------------------------------------- content */
|
|
401
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
|
+
|
|
402
1041
|
export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps>(
|
|
403
1042
|
({ className, ...props }, ref) => {
|
|
404
|
-
const { table, visibleColumnCount } = useDataTable()
|
|
1043
|
+
const { table, visibleColumnCount, isLoading, enablePagination } = useDataTable()
|
|
1044
|
+
const t = useText()
|
|
1045
|
+
const { comparisonOf, comparisonIds } = pairComparisons(table.getVisibleLeafColumns())
|
|
405
1046
|
|
|
406
1047
|
return (
|
|
407
1048
|
<div
|
|
408
1049
|
ref={ref}
|
|
409
1050
|
data-slot="data-table-content"
|
|
410
|
-
|
|
1051
|
+
aria-busy={isLoading || undefined}
|
|
1052
|
+
className={cn("@container overflow-x-auto", className)}
|
|
411
1053
|
{...props}
|
|
412
1054
|
>
|
|
413
|
-
<Table>
|
|
1055
|
+
<Table className="[&_th:first-child]:pl-0 [&_td:first-child]:pl-0 [&_th:last-child]:pr-0 [&_td:last-child]:pr-0">
|
|
414
1056
|
<TableHeader>
|
|
415
1057
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
416
1058
|
<TableRow key={headerGroup.id}>
|
|
417
|
-
{headerGroup.headers.map((header) =>
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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"
|
|
426
1075
|
? "ascending"
|
|
427
|
-
:
|
|
1076
|
+
: sorted === "desc"
|
|
428
1077
|
? "descending"
|
|
429
1078
|
: "none"
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
})}
|
|
446
1101
|
</TableRow>
|
|
447
1102
|
))}
|
|
448
1103
|
</TableHeader>
|
|
449
1104
|
|
|
450
1105
|
<TableBody>
|
|
451
|
-
{
|
|
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 ? (
|
|
452
1119
|
table.getRowModel().rows.map((row) => (
|
|
453
1120
|
<TableRow key={row.id} data-state={row.getIsSelected() ? "selected" : undefined}>
|
|
454
|
-
{row.getVisibleCells().map((cell) =>
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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
|
+
})}
|
|
459
1151
|
</TableRow>
|
|
460
1152
|
))
|
|
461
1153
|
) : (
|
|
@@ -464,7 +1156,7 @@ export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps
|
|
|
464
1156
|
colSpan={visibleColumnCount}
|
|
465
1157
|
className="h-24 text-center text-muted-foreground"
|
|
466
1158
|
>
|
|
467
|
-
|
|
1159
|
+
{t.noData}
|
|
468
1160
|
</TableCell>
|
|
469
1161
|
</TableRow>
|
|
470
1162
|
)}
|
|
@@ -480,36 +1172,46 @@ DataTableContent.displayName = "DataTableContent"
|
|
|
480
1172
|
|
|
481
1173
|
export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginationProps>(
|
|
482
1174
|
({ pageSizeOptions = [10, 20, 50], className, ...props }, ref) => {
|
|
483
|
-
const { table } = useDataTable()
|
|
1175
|
+
const { table, isLoading } = useDataTable()
|
|
1176
|
+
const t = useText()
|
|
484
1177
|
|
|
485
|
-
const totalRows =
|
|
1178
|
+
const totalRows =
|
|
1179
|
+
table.options.rowCount !== undefined
|
|
1180
|
+
? table.getRowCount()
|
|
1181
|
+
: table.getFilteredRowModel().rows.length
|
|
486
1182
|
const currentPageSize = table.getState().pagination.pageSize
|
|
487
1183
|
const currentPageIndex = table.getState().pagination.pageIndex
|
|
488
1184
|
const rangeStart = currentPageIndex * currentPageSize + 1
|
|
489
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
|
+
}))
|
|
490
1192
|
|
|
491
1193
|
return (
|
|
492
1194
|
<div
|
|
493
1195
|
ref={ref}
|
|
494
1196
|
data-slot="data-table-pagination"
|
|
1197
|
+
aria-busy={isLoading || undefined}
|
|
495
1198
|
className={cn("flex flex-wrap items-center justify-between gap-4", className)}
|
|
496
1199
|
{...props}
|
|
497
1200
|
>
|
|
498
1201
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
499
|
-
<span>
|
|
500
|
-
{totalRows}件中 {rangeStart} - {rangeEnd} を表示
|
|
501
|
-
</span>
|
|
1202
|
+
<span>{t.range(totalRows, rangeStart, rangeEnd)}</span>
|
|
502
1203
|
<Select
|
|
1204
|
+
items={pageSizeItems}
|
|
503
1205
|
value={String(currentPageSize)}
|
|
504
1206
|
onValueChange={(value) => table.setPageSize(Number(value))}
|
|
505
1207
|
>
|
|
506
|
-
<SelectTrigger size="sm" className="w-[80px]">
|
|
1208
|
+
<SelectTrigger size="sm" className="min-w-[80px]">
|
|
507
1209
|
<SelectValue />
|
|
508
1210
|
</SelectTrigger>
|
|
509
1211
|
<SelectContent>
|
|
510
|
-
{
|
|
511
|
-
<SelectItem key={
|
|
512
|
-
{
|
|
1212
|
+
{pageSizeItems.map((item) => (
|
|
1213
|
+
<SelectItem key={item.value} value={item.value}>
|
|
1214
|
+
{item.label}
|
|
513
1215
|
</SelectItem>
|
|
514
1216
|
))}
|
|
515
1217
|
</SelectContent>
|
|
@@ -521,33 +1223,33 @@ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginatio
|
|
|
521
1223
|
variant="outline"
|
|
522
1224
|
size="sm"
|
|
523
1225
|
onClick={() => table.firstPage()}
|
|
524
|
-
disabled={!table.getCanPreviousPage()}
|
|
1226
|
+
disabled={isLoading || !table.getCanPreviousPage()}
|
|
525
1227
|
>
|
|
526
|
-
|
|
1228
|
+
{t.first}
|
|
527
1229
|
</Button>
|
|
528
1230
|
<Button
|
|
529
1231
|
variant="outline"
|
|
530
1232
|
size="sm"
|
|
531
1233
|
onClick={() => table.previousPage()}
|
|
532
|
-
disabled={!table.getCanPreviousPage()}
|
|
1234
|
+
disabled={isLoading || !table.getCanPreviousPage()}
|
|
533
1235
|
>
|
|
534
|
-
|
|
1236
|
+
{t.previous}
|
|
535
1237
|
</Button>
|
|
536
1238
|
<Button
|
|
537
1239
|
variant="outline"
|
|
538
1240
|
size="sm"
|
|
539
1241
|
onClick={() => table.nextPage()}
|
|
540
|
-
disabled={!table.getCanNextPage()}
|
|
1242
|
+
disabled={isLoading || !table.getCanNextPage()}
|
|
541
1243
|
>
|
|
542
|
-
|
|
1244
|
+
{t.next}
|
|
543
1245
|
</Button>
|
|
544
1246
|
<Button
|
|
545
1247
|
variant="outline"
|
|
546
1248
|
size="sm"
|
|
547
1249
|
onClick={() => table.lastPage()}
|
|
548
|
-
disabled={!table.getCanNextPage()}
|
|
1250
|
+
disabled={isLoading || !table.getCanNextPage()}
|
|
549
1251
|
>
|
|
550
|
-
|
|
1252
|
+
{t.last}
|
|
551
1253
|
</Button>
|
|
552
1254
|
</div>
|
|
553
1255
|
</div>
|
|
@@ -563,6 +1265,7 @@ export const DataTableColumnVisibility = forwardRef<
|
|
|
563
1265
|
DataTableColumnVisibilityProps
|
|
564
1266
|
>(({ columnLabels, className }, ref) => {
|
|
565
1267
|
const { table } = useDataTable()
|
|
1268
|
+
const t = useText()
|
|
566
1269
|
|
|
567
1270
|
const hideableColumns = table.getAllColumns().filter((column) => column.getCanHide())
|
|
568
1271
|
|
|
@@ -574,8 +1277,9 @@ export const DataTableColumnVisibility = forwardRef<
|
|
|
574
1277
|
ref={ref}
|
|
575
1278
|
render={<Button variant="outline" size="sm" className={cn(className)} />}
|
|
576
1279
|
data-slot="data-table-column-visibility"
|
|
1280
|
+
aria-label={t.columns}
|
|
577
1281
|
>
|
|
578
|
-
<Columns3 className="size-4" />
|
|
1282
|
+
<Columns3 className="size-4" aria-hidden="true" />
|
|
579
1283
|
</DropdownMenuTrigger>
|
|
580
1284
|
<DropdownMenuContent align="end" className="min-w-[10rem]">
|
|
581
1285
|
{hideableColumns.map((column) => {
|
|
@@ -598,20 +1302,130 @@ export const DataTableColumnVisibility = forwardRef<
|
|
|
598
1302
|
})
|
|
599
1303
|
DataTableColumnVisibility.displayName = "DataTableColumnVisibility"
|
|
600
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
|
+
|
|
601
1412
|
/* ------------------------------------------------------------------ preset */
|
|
602
1413
|
|
|
603
1414
|
/** The usual arrangement — toolbar, body and pagination — in one component. */
|
|
604
1415
|
export function DataTablePreset<TData, TValue>({
|
|
605
1416
|
enableColumnVisibility = false,
|
|
606
1417
|
columnLabels,
|
|
1418
|
+
exportable = false,
|
|
1419
|
+
exportFileName,
|
|
607
1420
|
toolbar,
|
|
608
1421
|
bulkActions,
|
|
609
1422
|
...tableProps
|
|
610
1423
|
}: DataTablePresetProps<TData, TValue>) {
|
|
611
1424
|
const toolbarSlot =
|
|
612
|
-
toolbar || enableColumnVisibility ? (
|
|
1425
|
+
toolbar || enableColumnVisibility || exportable ? (
|
|
613
1426
|
<>
|
|
614
1427
|
{toolbar}
|
|
1428
|
+
{exportable && <DataTableExport fileName={exportFileName} />}
|
|
615
1429
|
{enableColumnVisibility && <DataTableColumnVisibility columnLabels={columnLabels} />}
|
|
616
1430
|
</>
|
|
617
1431
|
) : undefined
|