@squadbase/vantage 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +21 -4
  2. package/dist/chunk-A2UUGASH.js +12 -0
  3. package/dist/chunk-A2UUGASH.js.map +1 -0
  4. package/dist/{chunk-ZGDU5YLH.js → chunk-PRFBSQA4.js} +9 -5
  5. package/dist/chunk-PRFBSQA4.js.map +1 -0
  6. package/dist/chunk-WQZYXXQW.js +2249 -0
  7. package/dist/chunk-WQZYXXQW.js.map +1 -0
  8. package/dist/{chunk-73J5ZD4C.js → chunk-YLAB6UQS.js} +17 -3
  9. package/dist/chunk-YLAB6UQS.js.map +1 -0
  10. package/dist/cli.js +607 -36
  11. package/dist/cli.js.map +1 -1
  12. package/dist/components/index.d.ts +554 -0
  13. package/dist/components/index.js +1983 -0
  14. package/dist/components/index.js.map +1 -0
  15. package/dist/markdown/index.d.ts +9 -0
  16. package/dist/markdown/index.js +26 -0
  17. package/dist/markdown/index.js.map +1 -0
  18. package/dist/query/index.d.ts +27 -1
  19. package/dist/query/index.js +18 -0
  20. package/dist/query/index.js.map +1 -1
  21. package/dist/server/node.js +6 -1
  22. package/dist/server/node.js.map +1 -1
  23. package/dist/ui/index.d.ts +316 -200
  24. package/dist/ui/index.js +566 -511
  25. package/dist/ui/index.js.map +1 -1
  26. package/dist/vite/index.js +2 -2
  27. package/docs/en/agent-skills.md +89 -0
  28. package/docs/en/api-and-server.md +84 -0
  29. package/docs/en/build-and-deploy.md +280 -0
  30. package/docs/en/cli-reference.md +311 -0
  31. package/docs/en/components.md +233 -0
  32. package/docs/en/data-fetching.md +78 -0
  33. package/docs/en/environment-variables.md +69 -0
  34. package/docs/en/getting-started.md +95 -0
  35. package/docs/en/index.md +65 -0
  36. package/docs/en/markdown/markdown-renderer.md +44 -0
  37. package/docs/en/pages-and-metadata.md +56 -0
  38. package/docs/en/parts/app-shell.md +54 -0
  39. package/docs/en/parts/dashboard-card.md +64 -0
  40. package/docs/en/parts/data-table.md +86 -0
  41. package/docs/en/parts/date-range-picker.md +41 -0
  42. package/docs/en/parts/echart.md +53 -0
  43. package/docs/en/parts/filter-bar.md +66 -0
  44. package/docs/en/parts/funnel-steps.md +33 -0
  45. package/docs/en/parts/metric-value.md +23 -0
  46. package/docs/en/parts/multi-select.md +27 -0
  47. package/docs/en/parts/page-shell.md +45 -0
  48. package/docs/en/parts/searchable-select.md +47 -0
  49. package/docs/en/parts/section-header.md +20 -0
  50. package/docs/en/parts/segmented-control.md +25 -0
  51. package/docs/en/parts/status-badge.md +44 -0
  52. package/docs/en/parts/trend-indicator.md +26 -0
  53. package/docs/en/routing.md +95 -0
  54. package/docs/en/ui/accordion.md +36 -0
  55. package/docs/en/ui/alert.md +31 -0
  56. package/docs/en/ui/badge.md +23 -0
  57. package/docs/en/ui/breadcrumb.md +38 -0
  58. package/docs/en/ui/button.md +39 -0
  59. package/docs/en/ui/calendar.md +28 -0
  60. package/docs/en/ui/card.md +35 -0
  61. package/docs/en/ui/checkbox.md +34 -0
  62. package/docs/en/ui/cn.md +34 -0
  63. package/docs/en/ui/collapsible.md +21 -0
  64. package/docs/en/ui/command.md +37 -0
  65. package/docs/en/ui/dialog.md +52 -0
  66. package/docs/en/ui/dropdown-menu.md +50 -0
  67. package/docs/en/ui/empty.md +22 -0
  68. package/docs/en/ui/error-state.md +50 -0
  69. package/docs/en/ui/input-group.md +30 -0
  70. package/docs/en/ui/input.md +28 -0
  71. package/docs/en/ui/label.md +19 -0
  72. package/docs/en/ui/loading.md +19 -0
  73. package/docs/en/ui/popover.md +34 -0
  74. package/docs/en/ui/progress.md +28 -0
  75. package/docs/en/ui/scroll-area.md +23 -0
  76. package/docs/en/ui/select.md +63 -0
  77. package/docs/en/ui/separator.md +15 -0
  78. package/docs/en/ui/sheet.md +28 -0
  79. package/docs/en/ui/sidebar.md +92 -0
  80. package/docs/en/ui/skeleton.md +16 -0
  81. package/docs/en/ui/slider.md +21 -0
  82. package/docs/en/ui/spinner.md +14 -0
  83. package/docs/en/ui/switch.md +19 -0
  84. package/docs/en/ui/table.md +24 -0
  85. package/docs/en/ui/tabs.md +23 -0
  86. package/docs/en/ui/textarea.md +15 -0
  87. package/docs/en/ui/toggle-group.md +26 -0
  88. package/docs/en/ui/toggle.md +19 -0
  89. package/docs/en/ui/tooltip.md +22 -0
  90. package/docs/en/ui/use-is-mobile.md +18 -0
  91. package/docs/en/ui-and-theming.md +95 -0
  92. package/docs/index.json +1306 -0
  93. package/docs/ja/agent-skills.md +87 -0
  94. package/docs/ja/api-and-server.md +84 -0
  95. package/docs/ja/build-and-deploy.md +279 -0
  96. package/docs/ja/cli-reference.md +305 -0
  97. package/docs/ja/components.md +229 -0
  98. package/docs/ja/data-fetching.md +78 -0
  99. package/docs/ja/environment-variables.md +68 -0
  100. package/docs/ja/getting-started.md +95 -0
  101. package/docs/ja/index.md +65 -0
  102. package/docs/ja/markdown/markdown-renderer.md +43 -0
  103. package/docs/ja/pages-and-metadata.md +56 -0
  104. package/docs/ja/parts/app-shell.md +54 -0
  105. package/docs/ja/parts/dashboard-card.md +64 -0
  106. package/docs/ja/parts/data-table.md +85 -0
  107. package/docs/ja/parts/date-range-picker.md +41 -0
  108. package/docs/ja/parts/echart.md +52 -0
  109. package/docs/ja/parts/filter-bar.md +64 -0
  110. package/docs/ja/parts/funnel-steps.md +33 -0
  111. package/docs/ja/parts/metric-value.md +25 -0
  112. package/docs/ja/parts/multi-select.md +27 -0
  113. package/docs/ja/parts/page-shell.md +45 -0
  114. package/docs/ja/parts/searchable-select.md +47 -0
  115. package/docs/ja/parts/section-header.md +20 -0
  116. package/docs/ja/parts/segmented-control.md +26 -0
  117. package/docs/ja/parts/status-badge.md +44 -0
  118. package/docs/ja/parts/trend-indicator.md +26 -0
  119. package/docs/ja/routing.md +95 -0
  120. package/docs/ja/ui/accordion.md +37 -0
  121. package/docs/ja/ui/alert.md +31 -0
  122. package/docs/ja/ui/badge.md +23 -0
  123. package/docs/ja/ui/breadcrumb.md +38 -0
  124. package/docs/ja/ui/button.md +39 -0
  125. package/docs/ja/ui/calendar.md +28 -0
  126. package/docs/ja/ui/card.md +35 -0
  127. package/docs/ja/ui/checkbox.md +34 -0
  128. package/docs/ja/ui/cn.md +34 -0
  129. package/docs/ja/ui/collapsible.md +21 -0
  130. package/docs/ja/ui/command.md +36 -0
  131. package/docs/ja/ui/dialog.md +51 -0
  132. package/docs/ja/ui/dropdown-menu.md +50 -0
  133. package/docs/ja/ui/empty.md +22 -0
  134. package/docs/ja/ui/error-state.md +50 -0
  135. package/docs/ja/ui/input-group.md +30 -0
  136. package/docs/ja/ui/input.md +28 -0
  137. package/docs/ja/ui/label.md +19 -0
  138. package/docs/ja/ui/loading.md +19 -0
  139. package/docs/ja/ui/popover.md +34 -0
  140. package/docs/ja/ui/progress.md +28 -0
  141. package/docs/ja/ui/scroll-area.md +24 -0
  142. package/docs/ja/ui/select.md +64 -0
  143. package/docs/ja/ui/separator.md +15 -0
  144. package/docs/ja/ui/sheet.md +28 -0
  145. package/docs/ja/ui/sidebar.md +92 -0
  146. package/docs/ja/ui/skeleton.md +16 -0
  147. package/docs/ja/ui/slider.md +21 -0
  148. package/docs/ja/ui/spinner.md +14 -0
  149. package/docs/ja/ui/switch.md +19 -0
  150. package/docs/ja/ui/table.md +24 -0
  151. package/docs/ja/ui/tabs.md +24 -0
  152. package/docs/ja/ui/textarea.md +15 -0
  153. package/docs/ja/ui/toggle-group.md +27 -0
  154. package/docs/ja/ui/toggle.md +19 -0
  155. package/docs/ja/ui/tooltip.md +22 -0
  156. package/docs/ja/ui/use-is-mobile.md +18 -0
  157. package/docs/ja/ui-and-theming.md +95 -0
  158. package/package.json +29 -4
  159. package/registry/blocks/sales-overview.tsx +43 -19
  160. package/registry/ui/data-table.tsx +702 -102
  161. package/skills/vantage-add-feature/SKILL.md +150 -0
  162. package/skills/vantage-app/SKILL.md +258 -0
  163. package/skills/vantage-pitfalls/SKILL.md +115 -0
  164. package/templates/AGENTS.md +268 -0
  165. package/theme.css +178 -40
  166. package/dist/chunk-73J5ZD4C.js.map +0 -1
  167. package/dist/chunk-ZGDU5YLH.js.map +0 -1
@@ -1,115 +1,715 @@
1
1
  // Copied by `vantage add ui data-table`. This file is yours to edit.
2
- import { useMemo, useState, type ReactNode } from "react"
3
-
4
- export interface Column<T> {
5
- key: string
6
- header: string
7
- render?: (row: T) => ReactNode
8
- sortable?: boolean
9
- align?: "left" | "right" | "center"
10
- }
2
+ /**
3
+ * DataTable — a compound table over TanStack Table. `<DataTable>` owns the
4
+ * table instance and publishes it on context; the toolbar, body, pagination and
5
+ * column-visibility parts read from that context, so a page composes only the
6
+ * pieces it wants. `DataTablePreset` assembles the usual arrangement.
7
+ *
8
+ * Every piece of state (sorting, filters, pagination, selection, visibility) is
9
+ * optionally controlled: pass the value plus its `on…Change` to drive it from
10
+ * outside, or omit both and the table keeps it internally.
11
+ */
12
+ import {
13
+ createContext,
14
+ forwardRef,
15
+ useCallback,
16
+ useContext,
17
+ useMemo,
18
+ useState,
19
+ type HTMLAttributes,
20
+ type ReactNode,
21
+ type SetStateAction,
22
+ } from "react"
23
+ import {
24
+ flexRender,
25
+ getCoreRowModel,
26
+ getFilteredRowModel,
27
+ getPaginationRowModel,
28
+ getSortedRowModel,
29
+ useReactTable,
30
+ type ColumnDef,
31
+ type ColumnFiltersState,
32
+ type OnChangeFn,
33
+ type PaginationState,
34
+ type RowSelectionState,
35
+ type SortingState,
36
+ type Table as TanstackTable,
37
+ type TableMeta,
38
+ type VisibilityState,
39
+ } from "@tanstack/react-table"
40
+ import { ArrowDown, ArrowUp, ArrowUpDown, Columns3, Search } from "lucide-react"
11
41
 
12
- export interface DataTableProps<T> {
13
- columns: Column<T>[]
14
- data: T[]
15
- pageSize?: number
16
- }
42
+ import {
43
+ Button,
44
+ Checkbox,
45
+ DropdownMenu,
46
+ DropdownMenuCheckboxItem,
47
+ DropdownMenuContent,
48
+ 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
+ } from "@squadbase/vantage/ui"
17
63
 
18
- type Sort = { key: string; dir: "asc" | "desc" } | null
64
+
65
+ /* ------------------------------------------------------------------- types */
66
+
67
+ interface DataTableContextValue<TData> {
68
+ table: TanstackTable<TData>
69
+ selectedRows: TData[]
70
+ hasSelection: boolean
71
+ globalFilter: string
72
+ setGlobalFilter: OnChangeFn<string>
73
+ enableFiltering: boolean
74
+ enablePagination: boolean
75
+ enableRowSelection: boolean
76
+ visibleColumnCount: number
77
+ }
19
78
 
20
79
  /**
21
- * A fully-customizable data table. Unlike `DataTable` from
22
- * `@squadbase/vantage/ui`, this copy lives in your project so you can change
23
- * markup, styling and behavior freely.
80
+ * @template TData 行データ型
81
+ * @template TValue セル値の型 (省略時は任意)
24
82
  */
25
- export function DataTable<T extends Record<string, unknown>>({ columns, data, pageSize = 10 }: DataTableProps<T>) {
26
- const [sort, setSort] = useState<Sort>(null)
27
- const [page, setPage] = useState(0)
28
-
29
- const sorted = useMemo(() => {
30
- if (!sort) return data
31
- const copy = [...data]
32
- copy.sort((a, b) => {
33
- const av = a[sort.key]
34
- const bv = b[sort.key]
35
- const cmp =
36
- typeof av === "number" && typeof bv === "number"
37
- ? av - bv
38
- : String(av ?? "").localeCompare(String(bv ?? ""))
39
- return sort.dir === "asc" ? cmp : -cmp
40
- })
41
- return copy
42
- }, [data, sort])
43
-
44
- const pageCount = Math.max(1, Math.ceil(sorted.length / pageSize))
45
- const rows = sorted.slice(page * pageSize, page * pageSize + pageSize)
83
+ export interface DataTableProps<TData, TValue> {
84
+ /**
85
+ * TanStack Table の列定義配列。
86
+ *
87
+ * `ColumnDef<TData, TValue>[]` annotation するか配列リテラルに
88
+ * `satisfies ColumnDef<TData>[]` を付けないと、`accessorKey` などの
89
+ * literal `string` に拡張され型エラーになる。
90
+ *
91
+ * @example
92
+ * import type { ColumnDef } from "@squadbase/vantage/components"
93
+ *
94
+ * const columns: ColumnDef<User>[] = [
95
+ * { accessorKey: "id", header: "ID" },
96
+ * { accessorKey: "name", header: "名前" },
97
+ * ]
98
+ * <DataTable columns={columns} data={users}>...</DataTable>
99
+ */
100
+ columns: ColumnDef<TData, TValue>[]
101
+ data: TData[]
102
+
103
+ // Sorting
104
+ enableSorting?: boolean
105
+ sorting?: SortingState
106
+ onSortingChange?: OnChangeFn<SortingState>
107
+
108
+ // Filtering
109
+ enableFiltering?: boolean
110
+ columnFilters?: ColumnFiltersState
111
+ onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>
112
+ globalFilter?: string
113
+ onGlobalFilterChange?: OnChangeFn<string>
114
+
115
+ // Pagination
116
+ enablePagination?: boolean
117
+ pagination?: PaginationState
118
+ onPaginationChange?: OnChangeFn<PaginationState>
119
+ pageCount?: number
120
+
121
+ // Row selection
122
+ enableRowSelection?: boolean
123
+ rowSelection?: RowSelectionState
124
+ onRowSelectionChange?: OnChangeFn<RowSelectionState>
125
+
126
+ // Column visibility
127
+ columnVisibility?: VisibilityState
128
+ onColumnVisibilityChange?: OnChangeFn<VisibilityState>
129
+
130
+ meta?: TableMeta<TData>
131
+ className?: string
132
+
133
+ /** Required — the parts to render (toolbar, content, pagination). */
134
+ children: ReactNode
135
+ }
136
+
137
+ export interface DataTableToolbarProps extends HTMLAttributes<HTMLDivElement> {
138
+ showSearch?: boolean
139
+ toolbar?: ReactNode
140
+ bulkActions?: (selectedRows: unknown[]) => ReactNode
141
+ }
142
+
143
+ export interface DataTableContentProps extends HTMLAttributes<HTMLDivElement> {}
144
+
145
+ export interface DataTablePaginationProps extends HTMLAttributes<HTMLDivElement> {
146
+ pageSizeOptions?: number[]
147
+ }
148
+
149
+ export interface DataTableColumnVisibilityProps {
150
+ /** 列 ID をキーとしたラベルマップ。ヘッダーが文字列でない列のラベルを指定する */
151
+ columnLabels?: Record<string, string>
152
+ className?: string
153
+ }
154
+
155
+ export interface DataTablePresetProps<TData, TValue> {
156
+ columns: ColumnDef<TData, TValue>[]
157
+ data: TData[]
158
+
159
+ enableSorting?: boolean
160
+ sorting?: SortingState
161
+ onSortingChange?: OnChangeFn<SortingState>
162
+
163
+ enableFiltering?: boolean
164
+ columnFilters?: ColumnFiltersState
165
+ onColumnFiltersChange?: OnChangeFn<ColumnFiltersState>
166
+ globalFilter?: string
167
+ onGlobalFilterChange?: OnChangeFn<string>
168
+
169
+ enablePagination?: boolean
170
+ pagination?: PaginationState
171
+ onPaginationChange?: OnChangeFn<PaginationState>
172
+ pageCount?: number
173
+
174
+ enableRowSelection?: boolean
175
+ rowSelection?: RowSelectionState
176
+ onRowSelectionChange?: OnChangeFn<RowSelectionState>
177
+
178
+ enableColumnVisibility?: boolean
179
+ columnVisibility?: VisibilityState
180
+ onColumnVisibilityChange?: OnChangeFn<VisibilityState>
181
+ columnLabels?: Record<string, string>
182
+
183
+ /** Extension slots */
184
+ toolbar?: ReactNode
185
+ bulkActions?: (selectedRows: TData[]) => ReactNode
186
+
187
+ meta?: TableMeta<TData>
188
+ className?: string
189
+ }
190
+
191
+ /* ------------------------------------------------------------------ helper */
192
+
193
+ /** Use the controlled value when one is given, else keep it in local state. */
194
+ function useControlledState<T>(
195
+ controlledValue: T | undefined,
196
+ defaultValue: T,
197
+ onChange?: OnChangeFn<T>,
198
+ ): [T, OnChangeFn<T>] {
199
+ const [internalValue, setInternalValue] = useState<T>(defaultValue)
200
+
201
+ const isControlled = controlledValue !== undefined
202
+ const value = isControlled ? controlledValue : internalValue
203
+
204
+ const setValue: OnChangeFn<T> = useCallback(
205
+ (updaterOrValue) => {
206
+ onChange?.(updaterOrValue)
207
+ if (!isControlled) {
208
+ setInternalValue(updaterOrValue as SetStateAction<T>)
209
+ }
210
+ },
211
+ [onChange, isControlled],
212
+ )
213
+
214
+ return [value, setValue]
215
+ }
216
+
217
+ /* ----------------------------------------------------------------- context */
218
+
219
+ const DataTableContext = createContext<DataTableContextValue<unknown> | null>(null)
220
+
221
+ export function useDataTable<TData>(): DataTableContextValue<TData> {
222
+ const ctx = useContext(DataTableContext)
223
+ if (!ctx) throw new Error("useDataTable must be used inside <DataTable>")
224
+ return ctx as DataTableContextValue<TData>
225
+ }
226
+
227
+ /* -------------------------------------------------------------------- root */
228
+
229
+ export function DataTable<TData, TValue>({
230
+ columns,
231
+ data,
232
+ enableSorting = true,
233
+ sorting: sortingProp,
234
+ onSortingChange,
235
+ enableFiltering = false,
236
+ columnFilters: columnFiltersProp,
237
+ onColumnFiltersChange,
238
+ globalFilter: globalFilterProp,
239
+ onGlobalFilterChange,
240
+ enablePagination = false,
241
+ pagination: paginationProp,
242
+ onPaginationChange,
243
+ pageCount,
244
+ enableRowSelection = false,
245
+ rowSelection: rowSelectionProp,
246
+ onRowSelectionChange,
247
+ columnVisibility: columnVisibilityProp,
248
+ onColumnVisibilityChange,
249
+ meta,
250
+ className,
251
+ children,
252
+ }: DataTableProps<TData, TValue>) {
253
+ const [sorting, setSorting] = useControlledState<SortingState>(sortingProp, [], onSortingChange)
254
+
255
+ const [columnFilters, setColumnFilters] = useControlledState<ColumnFiltersState>(
256
+ columnFiltersProp,
257
+ [],
258
+ onColumnFiltersChange,
259
+ )
260
+
261
+ const [globalFilter, setGlobalFilter] = useControlledState<string>(
262
+ globalFilterProp,
263
+ "",
264
+ onGlobalFilterChange,
265
+ )
266
+
267
+ const [rowSelection, setRowSelection] = useControlledState<RowSelectionState>(
268
+ rowSelectionProp,
269
+ {},
270
+ onRowSelectionChange,
271
+ )
272
+
273
+ const [columnVisibility, setColumnVisibility] = useControlledState<VisibilityState>(
274
+ columnVisibilityProp,
275
+ {},
276
+ onColumnVisibilityChange,
277
+ )
278
+
279
+ const [pagination, setPagination] = useControlledState<PaginationState>(
280
+ paginationProp,
281
+ { pageIndex: 0, pageSize: 10 },
282
+ onPaginationChange,
283
+ )
284
+
285
+ // Row selection prepends a checkbox column that the caller never declares.
286
+ const allColumns = useMemo<ColumnDef<TData, TValue>[]>(() => {
287
+ if (!enableRowSelection) return columns
288
+
289
+ const selectionColumn: ColumnDef<TData, TValue> = {
290
+ id: "_select",
291
+ header: ({ table }) => (
292
+ <Checkbox
293
+ checked={table.getIsAllPageRowsSelected()}
294
+ indeterminate={table.getIsSomePageRowsSelected()}
295
+ onCheckedChange={(checked) => table.toggleAllPageRowsSelected(checked)}
296
+ aria-label="全て選択"
297
+ />
298
+ ),
299
+ cell: ({ row }) => (
300
+ <Checkbox
301
+ checked={row.getIsSelected()}
302
+ onCheckedChange={(checked) => row.toggleSelected(checked)}
303
+ aria-label="行を選択"
304
+ />
305
+ ),
306
+ enableSorting: false,
307
+ enableHiding: false,
308
+ }
309
+
310
+ return [selectionColumn, ...columns]
311
+ }, [columns, enableRowSelection])
312
+
313
+ const table = useReactTable({
314
+ data,
315
+ columns: allColumns,
316
+ state: {
317
+ sorting,
318
+ columnFilters,
319
+ globalFilter,
320
+ rowSelection,
321
+ columnVisibility,
322
+ ...(enablePagination ? { pagination } : {}),
323
+ },
324
+ enableSorting,
325
+ enableRowSelection,
326
+ onSortingChange: setSorting,
327
+ onColumnFiltersChange: setColumnFilters,
328
+ onGlobalFilterChange: setGlobalFilter,
329
+ onRowSelectionChange: setRowSelection,
330
+ onColumnVisibilityChange: setColumnVisibility,
331
+ ...(enablePagination ? { onPaginationChange: setPagination } : {}),
332
+ ...(pageCount !== undefined ? { pageCount, manualPagination: true } : {}),
333
+ getCoreRowModel: getCoreRowModel(),
334
+ getSortedRowModel: getSortedRowModel(),
335
+ getFilteredRowModel: getFilteredRowModel(),
336
+ ...(enablePagination ? { getPaginationRowModel: getPaginationRowModel() } : {}),
337
+ meta,
338
+ })
339
+
340
+ const selectedRows = useMemo(
341
+ () => table.getFilteredSelectedRowModel().rows.map((row) => row.original),
342
+ // `table` is a stable reference, so the selection state is the real input.
343
+ // eslint-disable-next-line react-hooks/exhaustive-deps
344
+ [rowSelection, data],
345
+ )
346
+
347
+ const hasSelection = selectedRows.length > 0
348
+
349
+ const contextValue = useMemo<DataTableContextValue<TData>>(
350
+ () => ({
351
+ table,
352
+ selectedRows,
353
+ hasSelection,
354
+ globalFilter,
355
+ setGlobalFilter,
356
+ enableFiltering,
357
+ enablePagination,
358
+ enableRowSelection,
359
+ visibleColumnCount: allColumns.length,
360
+ }),
361
+ // `useReactTable` returns the same object every render, so each piece of
362
+ // table state has to be tracked explicitly to re-render the consumers.
363
+ // eslint-disable-next-line react-hooks/exhaustive-deps
364
+ [
365
+ table,
366
+ selectedRows,
367
+ hasSelection,
368
+ globalFilter,
369
+ setGlobalFilter,
370
+ enableFiltering,
371
+ enablePagination,
372
+ enableRowSelection,
373
+ allColumns.length,
374
+ sorting,
375
+ columnFilters,
376
+ columnVisibility,
377
+ pagination,
378
+ rowSelection,
379
+ ],
380
+ )
46
381
 
47
382
  return (
48
- <div className="rounded-xl border border-border bg-card">
49
- <table className="w-full text-sm">
50
- <thead className="border-b border-border">
51
- <tr>
52
- {columns.map((c) => (
53
- <th key={c.key} className="h-10 px-3 text-left text-xs font-medium text-muted-foreground">
54
- {c.sortable ? (
55
- <button
56
- type="button"
57
- onClick={() =>
58
- setSort((p) =>
59
- p?.key === c.key
60
- ? p.dir === "asc"
61
- ? { key: c.key, dir: "desc" }
62
- : null
63
- : { key: c.key, dir: "asc" },
64
- )
65
- }
66
- >
67
- {c.header} {sort?.key === c.key ? (sort.dir === "asc" ? "▲" : "▼") : "↕"}
68
- </button>
69
- ) : (
70
- c.header
71
- )}
72
- </th>
383
+ <DataTableContext.Provider value={contextValue as DataTableContextValue<unknown>}>
384
+ <div data-slot="data-table" className={cn("space-y-4", className)}>
385
+ {children}
386
+ </div>
387
+ </DataTableContext.Provider>
388
+ )
389
+ }
390
+ DataTable.displayName = "DataTable"
391
+
392
+ /* ----------------------------------------------------------------- toolbar */
393
+
394
+ export const DataTableToolbar = forwardRef<HTMLDivElement, DataTableToolbarProps>(
395
+ ({ showSearch, toolbar, bulkActions, className, ...props }, ref) => {
396
+ const { globalFilter, setGlobalFilter, hasSelection, selectedRows, enableFiltering } =
397
+ useDataTable()
398
+
399
+ const showSearchEffective = showSearch ?? enableFiltering
400
+
401
+ const hasContent = showSearchEffective || toolbar != null || (hasSelection && bulkActions != null)
402
+
403
+ if (!hasContent) return null
404
+
405
+ return (
406
+ <div
407
+ ref={ref}
408
+ data-slot="data-table-toolbar"
409
+ className={cn("flex flex-wrap items-center gap-2", className)}
410
+ {...props}
411
+ >
412
+ {showSearchEffective && (
413
+ <div className="relative">
414
+ <Search className="absolute top-2.5 left-2.5 size-4 text-muted-foreground" />
415
+ <Input
416
+ value={globalFilter}
417
+ onChange={(e) => setGlobalFilter(e.target.value)}
418
+ className="max-w-sm pl-8"
419
+ />
420
+ </div>
421
+ )}
422
+ {toolbar}
423
+ {hasSelection && bulkActions && (
424
+ <div className="ml-auto flex items-center gap-2">{bulkActions(selectedRows)}</div>
425
+ )}
426
+ </div>
427
+ )
428
+ },
429
+ )
430
+ DataTableToolbar.displayName = "DataTableToolbar"
431
+
432
+ /* ----------------------------------------------------------------- content */
433
+
434
+ export const DataTableContent = forwardRef<HTMLDivElement, DataTableContentProps>(
435
+ ({ className, ...props }, ref) => {
436
+ const { table, visibleColumnCount } = useDataTable()
437
+
438
+ return (
439
+ <div
440
+ ref={ref}
441
+ data-slot="data-table-content"
442
+ className={cn("overflow-x-auto", className)}
443
+ {...props}
444
+ >
445
+ <Table>
446
+ <TableHeader>
447
+ {table.getHeaderGroups().map((headerGroup) => (
448
+ <TableRow key={headerGroup.id}>
449
+ {headerGroup.headers.map((header) => (
450
+ <TableHead key={header.id}>
451
+ {header.isPlaceholder ? null : header.column.getCanSort() ? (
452
+ <button
453
+ type="button"
454
+ className="inline-flex items-center gap-1 rounded hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:outline-none"
455
+ onClick={header.column.getToggleSortingHandler()}
456
+ aria-sort={
457
+ header.column.getIsSorted() === "asc"
458
+ ? "ascending"
459
+ : header.column.getIsSorted() === "desc"
460
+ ? "descending"
461
+ : "none"
462
+ }
463
+ >
464
+ {flexRender(header.column.columnDef.header, header.getContext())}
465
+ {header.column.getIsSorted() === "asc" ? (
466
+ <ArrowUp className="size-4" />
467
+ ) : header.column.getIsSorted() === "desc" ? (
468
+ <ArrowDown className="size-4" />
469
+ ) : (
470
+ <ArrowUpDown className="size-4 text-muted-foreground" />
471
+ )}
472
+ </button>
473
+ ) : (
474
+ flexRender(header.column.columnDef.header, header.getContext())
475
+ )}
476
+ </TableHead>
477
+ ))}
478
+ </TableRow>
73
479
  ))}
74
- </tr>
75
- </thead>
76
- <tbody>
77
- {rows.map((row, i) => (
78
- <tr key={i} className="border-b border-border last:border-0 hover:bg-muted/50">
79
- {columns.map((c) => (
80
- <td key={c.key} className="px-3 py-2.5">
81
- {c.render ? c.render(row) : String(row[c.key] ?? "")}
82
- </td>
83
- ))}
84
- </tr>
85
- ))}
86
- </tbody>
87
- </table>
88
- {pageCount > 1 ? (
89
- <div className="flex items-center justify-between border-t border-border px-3 py-2 text-sm">
90
- <span className="text-muted-foreground">
91
- Page {page + 1} of {pageCount}
480
+ </TableHeader>
481
+
482
+ <TableBody>
483
+ {table.getRowModel().rows.length ? (
484
+ table.getRowModel().rows.map((row) => (
485
+ <TableRow key={row.id} data-state={row.getIsSelected() ? "selected" : undefined}>
486
+ {row.getVisibleCells().map((cell) => (
487
+ <TableCell key={cell.id}>
488
+ {flexRender(cell.column.columnDef.cell, cell.getContext())}
489
+ </TableCell>
490
+ ))}
491
+ </TableRow>
492
+ ))
493
+ ) : (
494
+ <TableRow>
495
+ <TableCell
496
+ colSpan={visibleColumnCount}
497
+ className="h-24 text-center text-muted-foreground"
498
+ >
499
+ データがありません
500
+ </TableCell>
501
+ </TableRow>
502
+ )}
503
+ </TableBody>
504
+ </Table>
505
+ </div>
506
+ )
507
+ },
508
+ )
509
+ DataTableContent.displayName = "DataTableContent"
510
+
511
+ /* -------------------------------------------------------------- pagination */
512
+
513
+ export const DataTablePagination = forwardRef<HTMLDivElement, DataTablePaginationProps>(
514
+ ({ pageSizeOptions = [10, 20, 50], className, ...props }, ref) => {
515
+ const { table } = useDataTable()
516
+
517
+ const totalRows = table.getFilteredRowModel().rows.length
518
+ const currentPageSize = table.getState().pagination.pageSize
519
+ const currentPageIndex = table.getState().pagination.pageIndex
520
+ const rangeStart = currentPageIndex * currentPageSize + 1
521
+ const rangeEnd = Math.min((currentPageIndex + 1) * currentPageSize, totalRows)
522
+
523
+ return (
524
+ <div
525
+ ref={ref}
526
+ data-slot="data-table-pagination"
527
+ className={cn("flex flex-wrap items-center justify-between gap-4", className)}
528
+ {...props}
529
+ >
530
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
531
+ <span>
532
+ {totalRows}件中 {rangeStart} - {rangeEnd} を表示
92
533
  </span>
93
- <div className="flex gap-1">
94
- <button
95
- type="button"
96
- className="rounded-md border border-border px-3 py-1 disabled:opacity-50"
97
- disabled={page === 0}
98
- onClick={() => setPage((p) => p - 1)}
99
- >
100
- Previous
101
- </button>
102
- <button
103
- type="button"
104
- className="rounded-md border border-border px-3 py-1 disabled:opacity-50"
105
- disabled={page >= pageCount - 1}
106
- onClick={() => setPage((p) => p + 1)}
107
- >
108
- Next
109
- </button>
110
- </div>
534
+ <Select
535
+ items={Object.fromEntries(pageSizeOptions.map((size) => [String(size), `${size}件`]))}
536
+ value={String(currentPageSize)}
537
+ onValueChange={(value) => table.setPageSize(Number(value))}
538
+ >
539
+ <SelectTrigger size="sm" className="w-[80px]">
540
+ <SelectValue />
541
+ </SelectTrigger>
542
+ <SelectContent>
543
+ {pageSizeOptions.map((size) => (
544
+ <SelectItem key={size} value={String(size)}>
545
+ {size}件
546
+ </SelectItem>
547
+ ))}
548
+ </SelectContent>
549
+ </Select>
111
550
  </div>
112
- ) : null}
113
- </div>
551
+
552
+ <div className="flex items-center gap-1">
553
+ <Button
554
+ variant="outline"
555
+ size="sm"
556
+ onClick={() => table.firstPage()}
557
+ disabled={!table.getCanPreviousPage()}
558
+ >
559
+ 最初
560
+ </Button>
561
+ <Button
562
+ variant="outline"
563
+ size="sm"
564
+ onClick={() => table.previousPage()}
565
+ disabled={!table.getCanPreviousPage()}
566
+ >
567
+ 前へ
568
+ </Button>
569
+ <Button
570
+ variant="outline"
571
+ size="sm"
572
+ onClick={() => table.nextPage()}
573
+ disabled={!table.getCanNextPage()}
574
+ >
575
+ 次へ
576
+ </Button>
577
+ <Button
578
+ variant="outline"
579
+ size="sm"
580
+ onClick={() => table.lastPage()}
581
+ disabled={!table.getCanNextPage()}
582
+ >
583
+ 最後
584
+ </Button>
585
+ </div>
586
+ </div>
587
+ )
588
+ },
589
+ )
590
+ DataTablePagination.displayName = "DataTablePagination"
591
+
592
+ /* ------------------------------------------------------- column visibility */
593
+
594
+ export const DataTableColumnVisibility = forwardRef<
595
+ HTMLButtonElement,
596
+ DataTableColumnVisibilityProps
597
+ >(({ columnLabels, className }, ref) => {
598
+ const { table } = useDataTable()
599
+
600
+ const hideableColumns = table.getAllColumns().filter((column) => column.getCanHide())
601
+
602
+ if (hideableColumns.length === 0) return null
603
+
604
+ return (
605
+ <DropdownMenu>
606
+ <DropdownMenuTrigger
607
+ ref={ref}
608
+ render={<Button variant="outline" size="sm" className={cn(className)} />}
609
+ data-slot="data-table-column-visibility"
610
+ >
611
+ <Columns3 className="size-4" />
612
+ </DropdownMenuTrigger>
613
+ <DropdownMenuContent align="end" className="min-w-[10rem]">
614
+ {hideableColumns.map((column) => {
615
+ const label =
616
+ columnLabels?.[column.id] ??
617
+ (typeof column.columnDef.header === "string" ? column.columnDef.header : column.id)
618
+ return (
619
+ <DropdownMenuCheckboxItem
620
+ key={column.id}
621
+ checked={column.getIsVisible()}
622
+ onCheckedChange={(checked) => column.toggleVisibility(checked)}
623
+ >
624
+ {label}
625
+ </DropdownMenuCheckboxItem>
626
+ )
627
+ })}
628
+ </DropdownMenuContent>
629
+ </DropdownMenu>
630
+ )
631
+ })
632
+ DataTableColumnVisibility.displayName = "DataTableColumnVisibility"
633
+
634
+ /* ------------------------------------------------------------------ preset */
635
+
636
+ /** The usual arrangement — toolbar, body and pagination — in one component. */
637
+ 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
+ enableColumnVisibility = false,
656
+ columnVisibility,
657
+ onColumnVisibilityChange,
658
+ columnLabels,
659
+ toolbar,
660
+ bulkActions,
661
+ meta,
662
+ className,
663
+ }: DataTablePresetProps<TData, TValue>) {
664
+ const toolbarSlot =
665
+ toolbar || enableColumnVisibility ? (
666
+ <>
667
+ {toolbar}
668
+ {enableColumnVisibility && <DataTableColumnVisibility columnLabels={columnLabels} />}
669
+ </>
670
+ ) : undefined
671
+
672
+ 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
+ >
696
+ <DataTableToolbar
697
+ showSearch={enableFiltering}
698
+ toolbar={toolbarSlot}
699
+ bulkActions={bulkActions as ((selectedRows: unknown[]) => ReactNode) | undefined}
700
+ />
701
+ <DataTableContent />
702
+ {enablePagination && <DataTablePagination />}
703
+ </DataTable>
114
704
  )
115
705
  }
706
+ DataTablePreset.displayName = "DataTablePreset"
707
+
708
+ export type {
709
+ ColumnDef,
710
+ ColumnFiltersState,
711
+ PaginationState,
712
+ RowSelectionState,
713
+ SortingState,
714
+ VisibilityState,
715
+ } from "@tanstack/react-table"