@spree/dashboard-ui 1.0.0-rc.2 → 1.0.0-rc.4

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spree/dashboard-ui",
3
3
  "license": "MIT",
4
- "version": "1.0.0-rc.2",
4
+ "version": "1.0.0-rc.4",
5
5
  "description": "Spree Dashboard design system — shadcn primitives, headless composed components, and design tokens. Source-only package; consumers compile via their own Vite/Tailwind setup. Developer Preview.",
6
6
  "author": "Vendo Connect Inc.",
7
7
  "homepage": "https://spreecommerce.org",
@@ -37,7 +37,6 @@
37
37
  "LICENSE"
38
38
  ],
39
39
  "peerDependencies": {
40
- "@tanstack/react-hotkeys": "^0.10.0",
41
40
  "@tanstack/react-router": "^1.169.2",
42
41
  "i18next": "^26.2.0",
43
42
  "react": "^19.2.6",
@@ -60,37 +59,40 @@
60
59
  "@dnd-kit/core": "6.3.1",
61
60
  "@dnd-kit/sortable": "10.0.0",
62
61
  "@dnd-kit/utilities": "3.2.2",
63
- "@fontsource-variable/geist": "5.2.8",
64
- "@fontsource-variable/geist-mono": "5.2.7",
65
- "@tanstack/react-table": "8.21.3",
66
- "@tiptap/core": "3.31.3",
67
- "@tiptap/extension-image": "3.31.3",
68
- "@tiptap/extension-link": "3.31.3",
69
- "@tiptap/extension-placeholder": "3.31.3",
70
- "@tiptap/pm": "3.31.3",
71
- "@tiptap/react": "3.31.3",
72
- "@tiptap/starter-kit": "3.31.3",
62
+ "@fontsource-variable/geist": "5.3.0",
63
+ "@fontsource-variable/geist-mono": "5.3.0",
64
+ "@tanstack/react-hotkeys": "^0.14.3",
65
+ "@tanstack/react-table": "9.3.1",
66
+ "@tiptap/core": "3.32.0",
67
+ "@tiptap/extension-bubble-menu": "3.32.0",
68
+ "@tiptap/extension-floating-menu": "3.32.0",
69
+ "@tiptap/extension-image": "3.32.0",
70
+ "@tiptap/extension-link": "3.32.0",
71
+ "@tiptap/extension-placeholder": "3.32.0",
72
+ "@tiptap/pm": "3.32.0",
73
+ "@tiptap/react": "3.32.0",
74
+ "@tiptap/starter-kit": "3.32.0",
73
75
  "@uiw/react-json-view": "1.12.2",
74
76
  "class-variance-authority": "0.7.1",
75
77
  "clsx": "2.1.1",
76
78
  "cmdk": "1.1.1",
77
- "date-fns": "4.1.0",
79
+ "date-fns": "4.4.0",
78
80
  "date-fns-tz": "3.2.0",
79
- "flag-icons": "7.5.0",
80
- "lucide-react": "1.47.0",
81
- "react-colorful": "5.7.0",
82
- "react-day-picker": "9.14.0",
81
+ "flag-icons": "7.6.0",
82
+ "lucide-react": "1.58.0",
83
+ "react-colorful": "5.8.1",
84
+ "react-day-picker": "10.0.2",
83
85
  "recharts": "3.10.1",
84
- "shadcn": "4.21.0",
85
- "tailwind-merge": "3.5.0",
86
+ "shadcn": "4.22.0",
87
+ "tailwind-merge": "3.7.0",
86
88
  "tw-animate-css": "1.4.0"
87
89
  },
88
90
  "devDependencies": {
89
91
  "@types/react": "^19.2.14",
90
92
  "@types/react-dom": "^19.2.3",
91
93
  "tailwindcss": "^4.2.4",
92
- "typescript": "~6.0.3",
93
- "vite": "^8.0.16"
94
+ "typescript": "~7.0.2",
95
+ "vite": "^8.3.4"
94
96
  },
95
97
  "scripts": {
96
98
  "lint": "biome check src",
package/src/index.ts CHANGED
@@ -26,7 +26,7 @@ export {
26
26
  } from './lib/date-range-presets'
27
27
  export { sameRichText, shouldEmitRichTextChange } from './lib/same-rich-text'
28
28
  export { cn } from './lib/utils'
29
- export { requiredMessage } from './lib/validation-messages'
29
+ export { decimalPlacesMessage, requiredMessage } from './lib/validation-messages'
30
30
  // ---------------------------------------------------------------------------
31
31
  // Spree composed components — headless, accept data via props
32
32
  // ---------------------------------------------------------------------------
@@ -117,6 +117,7 @@ export * from './ui/empty'
117
117
  export * from './ui/field'
118
118
  export * from './ui/input'
119
119
  export * from './ui/input-group'
120
+ export * from './ui/item'
120
121
  export * from './ui/label'
121
122
  export * from './ui/media-preview'
122
123
  export * from './ui/pagination'
@@ -17,3 +17,15 @@ import i18n from 'i18next'
17
17
  export function requiredMessage(field: string): () => string {
18
18
  return () => i18n.t('admin.validation.required', { field: i18n.t(`admin.fields.${field}.label`) })
19
19
  }
20
+
21
+ /**
22
+ * Lazy "<Field> can have at most <count> decimal places" message, for amounts
23
+ * and rates the server keeps to a fixed number of decimals.
24
+ */
25
+ export function decimalPlacesMessage(field: string, count: number): () => string {
26
+ return () =>
27
+ i18n.t('admin.validation.too_many_decimals', {
28
+ field: i18n.t(`admin.fields.${field}.label`),
29
+ count,
30
+ })
31
+ }
@@ -1,14 +1,8 @@
1
1
  import { useTranslation } from 'react-i18next'
2
2
  import { Badge } from '../ui/badge'
3
- import { Button } from '../ui/button'
4
- import {
5
- DropdownMenu,
6
- DropdownMenuContent,
7
- DropdownMenuItem,
8
- DropdownMenuTrigger,
9
- } from '../ui/dropdown-menu'
3
+ import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
10
4
  import { AddressBlock, type AddressBlockValue } from './address-block'
11
- import { EllipsisVerticalIcon, PencilIcon, TrashIcon } from './icons'
5
+ import { RowActions } from './row-actions'
12
6
 
13
7
  /** What every address book row needs, whoever owns the book. */
14
8
  export interface AddressBookEntry extends AddressBlockValue {
@@ -45,52 +39,44 @@ export function AddressBookRow({
45
39
  const { t } = useTranslation()
46
40
 
47
41
  return (
48
- <div className="flex items-start justify-between gap-2 rounded-md border p-3">
49
- <div className="flex min-w-0 flex-col gap-1">
50
- <span className="flex flex-wrap items-center gap-2">
51
- {address.label && (
52
- <span className="font-medium text-foreground text-sm">{address.label}</span>
53
- )}
54
- {address.is_default_billing && (
55
- <Badge variant="outline">{t('admin.address_book.default_billing')}</Badge>
56
- )}
57
- {address.is_default_shipping && (
58
- <Badge variant="outline">{t('admin.address_book.default_shipping')}</Badge>
59
- )}
60
- </span>
42
+ <Item variant="outline" className="items-start">
43
+ <ItemContent className="min-w-0">
44
+ {(address.label || address.is_default_billing || address.is_default_shipping) && (
45
+ <ItemTitle className="flex-wrap">
46
+ {address.label}
47
+ {address.is_default_billing && (
48
+ <Badge variant="outline">{t('admin.address_book.default_billing')}</Badge>
49
+ )}
50
+ {address.is_default_shipping && (
51
+ <Badge variant="outline">{t('admin.address_book.default_shipping')}</Badge>
52
+ )}
53
+ </ItemTitle>
54
+ )}
61
55
  <AddressBlock address={address} />
62
- </div>
56
+ </ItemContent>
63
57
 
64
58
  {canEdit && (
65
- <DropdownMenu>
66
- <DropdownMenuTrigger asChild>
67
- <Button variant="ghost" size="icon-xs" className="shrink-0">
68
- <EllipsisVerticalIcon className="size-4" />
69
- <span className="sr-only">{t('admin.actions.actions_menu')}</span>
70
- </Button>
71
- </DropdownMenuTrigger>
72
- <DropdownMenuContent align="end">
73
- <DropdownMenuItem onClick={onEdit}>
74
- <PencilIcon className="size-4" />
75
- {t('admin.actions.edit')}
76
- </DropdownMenuItem>
77
- {onSetDefaultBilling && !address.is_default_billing && (
78
- <DropdownMenuItem onClick={onSetDefaultBilling}>
79
- {t('admin.address_book.set_default_billing')}
80
- </DropdownMenuItem>
81
- )}
82
- {onSetDefaultShipping && !address.is_default_shipping && (
83
- <DropdownMenuItem onClick={onSetDefaultShipping}>
84
- {t('admin.address_book.set_default_shipping')}
85
- </DropdownMenuItem>
86
- )}
87
- <DropdownMenuItem variant="destructive" onClick={onRemove}>
88
- <TrashIcon className="size-4" />
89
- {t('admin.actions.delete')}
90
- </DropdownMenuItem>
91
- </DropdownMenuContent>
92
- </DropdownMenu>
59
+ <ItemActions>
60
+ <RowActions
61
+ actions={[
62
+ { key: 'edit', onSelect: onEdit },
63
+ {
64
+ key: 'set_default_billing',
65
+ label: t('admin.address_book.set_default_billing'),
66
+ visible: !!onSetDefaultBilling && !address.is_default_billing,
67
+ onSelect: () => onSetDefaultBilling?.(),
68
+ },
69
+ {
70
+ key: 'set_default_shipping',
71
+ label: t('admin.address_book.set_default_shipping'),
72
+ visible: !!onSetDefaultShipping && !address.is_default_shipping,
73
+ onSelect: () => onSetDefaultShipping?.(),
74
+ },
75
+ { key: 'delete', destructive: true, onSelect: onRemove },
76
+ ]}
77
+ />
78
+ </ItemActions>
93
79
  )}
94
- </div>
80
+ </Item>
95
81
  )
96
82
  }
@@ -1,8 +1,16 @@
1
- import type { ColumnDef } from '@tanstack/react-table'
2
1
  import { PlusIcon, XIcon } from 'lucide-react'
3
2
  import { useMemo } from 'react'
3
+ import { Alert, AlertDescription } from '../ui/alert'
4
4
  import { Button } from '../ui/button'
5
- import { DataGrid, DecimalCell, editableRowIndex, MoneyCell, ReadOnlyCell } from './data-grid'
5
+ import {
6
+ DataGrid,
7
+ type DataGridColumn,
8
+ DecimalCell,
9
+ editableRowIndex,
10
+ MoneyCell,
11
+ ReadOnlyCell,
12
+ } from './data-grid'
13
+ import { CircleAlertIcon } from './icons'
6
14
  import { SearchInput } from './search-input'
7
15
 
8
16
  export interface BulkPriceRow {
@@ -131,7 +139,7 @@ export function BulkPriceTable({
131
139
  isLoading,
132
140
  error,
133
141
  }: BulkPriceTableProps) {
134
- const columns = useMemo<ColumnDef<BulkPriceRow>[]>(
142
+ const columns = useMemo<DataGridColumn<BulkPriceRow>[]>(
135
143
  () => [
136
144
  {
137
145
  id: 'variant',
@@ -203,7 +211,7 @@ export function BulkPriceTable({
203
211
  />
204
212
  )
205
213
  },
206
- } satisfies ColumnDef<BulkPriceRow>,
214
+ } satisfies DataGridColumn<BulkPriceRow>,
207
215
  ]
208
216
  : []),
209
217
  {
@@ -291,7 +299,7 @@ export function BulkPriceTable({
291
299
  </Button>
292
300
  )
293
301
  },
294
- } satisfies ColumnDef<BulkPriceRow>,
302
+ } satisfies DataGridColumn<BulkPriceRow>,
295
303
  ]
296
304
  : []),
297
305
  ],
@@ -312,12 +320,10 @@ export function BulkPriceTable({
312
320
  // table in a card does.
313
321
  <div className="flex h-full flex-col gap-3">
314
322
  {error && (
315
- <div
316
- role="alert"
317
- className="mx-3 shrink-0 rounded-md border border-destructive/50 bg-destructive/10 px-3 py-2 text-destructive text-sm"
318
- >
319
- {error}
320
- </div>
323
+ <Alert variant="destructive" className="mx-3 w-auto shrink-0">
324
+ <CircleAlertIcon />
325
+ <AlertDescription>{error}</AlertDescription>
326
+ </Alert>
321
327
  )}
322
328
  {showToolbar && (
323
329
  // Always-mounted toolbar. Conditionally rendering the search input
@@ -1,6 +1,13 @@
1
- import type { ColumnDef } from '@tanstack/react-table'
2
1
  import { useMemo } from 'react'
3
- import { DataGrid, DecimalCell, ReadOnlyCell, SelectCell, SwitchCell, TextCell } from './data-grid'
2
+ import {
3
+ DataGrid,
4
+ type DataGridColumn,
5
+ DecimalCell,
6
+ ReadOnlyCell,
7
+ SelectCell,
8
+ SwitchCell,
9
+ TextCell,
10
+ } from './data-grid'
4
11
 
5
12
  export interface BulkVariantsRow {
6
13
  id: string
@@ -139,7 +146,7 @@ export function BulkVariantsTable({
139
146
  cartonOptions,
140
147
  onChange,
141
148
  }: BulkVariantsTableProps) {
142
- const columns = useMemo<ColumnDef<BulkVariantsRow>[]>(() => {
149
+ const columns = useMemo<DataGridColumn<BulkVariantsRow>[]>(() => {
143
150
  // Keyboard column coordinates, fixed per column: variant label is
144
151
  // read-only (unregistered), then sku=1, barcode=2, weight=3, unit=4,
145
152
  // height=5, width=6, depth=7, unit=8, preorderable=9, backorder=10,
@@ -148,7 +155,7 @@ export function BulkVariantsTable({
148
155
  const numericHeader = (label: string) => () => <span className="block text-right">{label}</span>
149
156
  const rowLabel = (r: BulkVariantsRow) => r.variantLabel ?? labels.variantDefault
150
157
 
151
- const defs: ColumnDef<BulkVariantsRow>[] = [
158
+ const defs: DataGridColumn<BulkVariantsRow>[] = [
152
159
  {
153
160
  id: 'variant',
154
161
  header: labels.variant,
@@ -182,7 +189,7 @@ export function BulkVariantsTable({
182
189
  />
183
190
  ),
184
191
  },
185
- ...DECIMAL_COLUMNS.map<ColumnDef<BulkVariantsRow>>(({ field, col }) => ({
192
+ ...DECIMAL_COLUMNS.map<DataGridColumn<BulkVariantsRow>>(({ field, col }) => ({
186
193
  id: field,
187
194
  header: numericHeader(labels[field]),
188
195
  cell: ({ row }) => (
@@ -5,7 +5,8 @@
5
5
  * model (delivery methods, tax rates, …).
6
6
  *
7
7
  * The expected payload mirrors the admin API's calculator embed: a
8
- * `{ label, preferences, preference_schema }` triple. The component is
8
+ * `{ label, preferences, schema }` triple, `schema` being the JSON Schema of
9
+ * the calculator's preferences. The component is
9
10
  * tolerant of partial input — missing schema or preferences just fall
10
11
  * back to the label alone.
11
12
  *
@@ -22,7 +23,14 @@ export interface CalculatorPayload {
22
23
  /** STI class name (e.g. "Spree::Calculator::FlatRate"). Used as a label fallback. */
23
24
  type?: string | null
24
25
  preferences?: Record<string, unknown> | null
25
- preference_schema?: ReadonlyArray<{ key: string; type: string }> | null
26
+ schema?: { properties?: Record<string, CalculatorPropertySchema> } | null
27
+ }
28
+
29
+ /** The part of a preference's JSON Schema the summary reads. */
30
+ interface CalculatorPropertySchema {
31
+ type?: string | string[]
32
+ format?: string
33
+ pattern?: string
26
34
  }
27
35
 
28
36
  interface CalculatorSummaryProps {
@@ -60,14 +68,14 @@ export function formatCalculatorSummary(
60
68
  calculator.label?.trim() ||
61
69
  (calculator.type ? (calculator.type.split('::').pop() ?? calculator.type) : '')
62
70
 
63
- const schema = calculator.preference_schema
71
+ const properties = Object.entries(calculator.schema?.properties ?? {})
64
72
  const prefs = calculator.preferences
65
73
 
66
- if (!schema?.length || !prefs) return label || null
74
+ if (!properties.length || !prefs) return label || null
67
75
 
68
76
  const currency = (typeof prefs.currency === 'string' && prefs.currency) || 'USD'
69
- const details = schema
70
- .map((field) => formatField(field, prefs[field.key], currency))
77
+ const details = properties
78
+ .map(([key, property]) => formatField(key, property, prefs[key], currency))
71
79
  .filter((s): s is string => s !== null)
72
80
  .join(', ')
73
81
 
@@ -76,50 +84,47 @@ export function formatCalculatorSummary(
76
84
  }
77
85
 
78
86
  /**
79
- * Formats one preference value by naming convention. Calculators don't
80
- * carry richer field metadata (no `display_format`, no `unit`), so we
81
- * fall back to key-name heuristics that match the Ruby preference
82
- * declarations in `Spree::Calculator::*`.
87
+ * Formats one preference value from its schema.
83
88
  *
84
- * - `amount` / `*_amount` decimals → currency-formatted.
85
- * - `*_percent` decimals → `N%`.
86
- * - `currency` strings → omitted (folded into the money formatting).
89
+ * - money → currency-formatted.
90
+ * - `*_percent` values → `N%`.
91
+ * - currencies → omitted (folded into the money formatting).
87
92
  * - booleans → humanized key when true; skipped when false.
88
- * - lists (tiers) → their length; other objects → omitted.
93
+ * - lists (tiers) → their length; objects → omitted.
89
94
  * - everything else → `key: value`.
90
95
  */
91
96
  function formatField(
92
- field: { key: string; type: string },
97
+ key: string,
98
+ property: CalculatorPropertySchema,
93
99
  value: unknown,
94
100
  currency: string,
95
101
  ): string | null {
96
102
  if (value === null || value === undefined || value === '') return null
97
- if (field.key === 'currency') return null
103
+ if (property.format === 'currency') return null
104
+ if (property.format === 'money') return formatMoney(value, currency)
105
+ if (/(?:^|_)percent$/.test(key)) return `${value}%`
98
106
 
99
- if (field.type === 'decimal') {
100
- if (/(?:^|_)percent$/.test(field.key)) return `${value}%`
101
- if (field.key === 'amount' || field.key.endsWith('_amount')) {
102
- return formatMoney(value, currency)
103
- }
104
- return `${humanize(field.key)}: ${value}`
105
- }
106
- if (field.type === 'boolean') {
107
- return value ? humanize(field.key) : null
108
- }
109
- if (field.type === 'array') {
110
- return Array.isArray(value) && value.length ? `${humanize(field.key)}: ${value.length}` : null
107
+ const type = (Array.isArray(property.type) ? property.type : [property.type]).find(
108
+ (t) => t !== 'null',
109
+ )
110
+ if (type === 'boolean') return value ? humanize(key) : null
111
+ if (type === 'array') {
112
+ return Array.isArray(value) && value.length ? `${humanize(key)}: ${value.length}` : null
111
113
  }
112
- if (field.type === 'hash') return null
113
- return `${humanize(field.key)}: ${value}`
114
+ if (type === 'object') return null
115
+ return `${humanize(key)}: ${value}`
114
116
  }
115
117
 
116
118
  function formatMoney(value: unknown, currency: string): string {
117
- const n = typeof value === 'number' ? value : Number(value)
118
- if (Number.isNaN(n)) return String(value)
119
+ const amount = String(value)
120
+ if (!/^-?\d+(\.\d+)?$/.test(amount)) return amount
119
121
  try {
120
- return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(n)
122
+ // `Intl` reads a numeric string exactly; its ES2022 typings only list numbers.
123
+ return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(
124
+ amount as unknown as number,
125
+ )
121
126
  } catch {
122
- return `${n} ${currency}`
127
+ return `${amount} ${currency}`
123
128
  }
124
129
  }
125
130
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '../ui/dialog'
12
12
  import { Field, FieldLabel } from '../ui/field'
13
13
  import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from '../ui/input-group'
14
+ import { Item, ItemActions, ItemContent, ItemTitle } from '../ui/item'
14
15
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'
15
16
  import { Switch } from '../ui/switch'
16
17
  import type { RefundMethod } from './return-dialogs'
@@ -93,8 +94,11 @@ export function ClaimResolveDialog({
93
94
 
94
95
  // Refunding nothing, or replacing nothing, is what the server rejects —
95
96
  // say so here instead of letting the request fail.
97
+ // The amount is the canonical decimal string a number input yields, so
98
+ // "above zero" is unsigned digits with a non-zero digit among them.
99
+ const positiveAmount = /^(\d+(\.\d+)?|\.\d+)$/.test(amount) && /[1-9]/.test(amount)
96
100
  const ready =
97
- (!refunding || Number(amount) > 0) && (!sendingReplacement || chosenReplacements.length > 0)
101
+ (!refunding || positiveAmount) && (!sendingReplacement || chosenReplacements.length > 0)
98
102
 
99
103
  return (
100
104
  <Dialog open onOpenChange={(open) => !open && onClose()}>
@@ -138,7 +142,7 @@ export function ClaimResolveDialog({
138
142
  <InputGroupInput
139
143
  id="claim-resolve-amount"
140
144
  type="number"
141
- step="0.01"
145
+ step="any"
142
146
  min="0"
143
147
  value={amount}
144
148
  onChange={(event) => setAmount(event.target.value)}
@@ -175,21 +179,23 @@ export function ClaimResolveDialog({
175
179
  {t('admin.pages.orders.detail.claims.replacement_items')}
176
180
  </span>
177
181
  {lines.map((line) => (
178
- <div
179
- key={line.id}
180
- className="flex items-center justify-between gap-4 rounded-lg border border-border-subtle p-3"
181
- >
182
- <span className="truncate text-sm">
183
- {line.label}
184
- <span className="text-muted-foreground"> ×{line.quantity}</span>
185
- </span>
186
- <Switch
187
- checked={replacing[line.id] ?? false}
188
- onCheckedChange={(checked) =>
189
- setReplacing({ ...replacing, [line.id]: checked })
190
- }
191
- />
192
- </div>
182
+ <Item key={line.id} variant="outline" className="flex-nowrap border-border-subtle">
183
+ <ItemContent className="min-w-0">
184
+ <ItemTitle className="block truncate font-normal">
185
+ {line.label}
186
+ <span className="text-muted-foreground"> ×{line.quantity}</span>
187
+ </ItemTitle>
188
+ </ItemContent>
189
+ <ItemActions>
190
+ <Switch
191
+ aria-label={line.label}
192
+ checked={replacing[line.id] ?? false}
193
+ onCheckedChange={(checked) =>
194
+ setReplacing({ ...replacing, [line.id]: checked })
195
+ }
196
+ />
197
+ </ItemActions>
198
+ </Item>
193
199
  ))}
194
200
  </div>
195
201
  )}
@@ -1,10 +1,11 @@
1
1
  import {
2
+ type ColumnDef,
2
3
  flexRender,
3
- getCoreRowModel,
4
4
  type Row,
5
+ type RowData,
5
6
  type Table,
6
- type TableOptions,
7
- useReactTable,
7
+ tableFeatures,
8
+ useTable,
8
9
  } from '@tanstack/react-table'
9
10
  import { useCallback, useMemo, useRef, useState } from 'react'
10
11
  import { cn } from '../../lib/utils'
@@ -14,9 +15,18 @@ import type { CellCoords, CellKey, CellRegistration, RenderSectionHeader } from
14
15
  import { cellKey } from './types'
15
16
  import { useDataGridKeyboard } from './use-data-grid-keyboard'
16
17
 
17
- interface DataGridProps<T> {
18
+ // The grid renders rows as given — no sorting, filtering or column state — so
19
+ // it needs only the table's core features.
20
+ const dataGridFeatures = tableFeatures({})
21
+ type DataGridFeatures = typeof dataGridFeatures
22
+
23
+ /** A column of a `DataGrid`. */
24
+ // biome-ignore lint/suspicious/noExplicitAny: each column reads a value of its own type
25
+ export type DataGridColumn<T extends RowData> = ColumnDef<DataGridFeatures, T, any>
26
+
27
+ interface DataGridProps<T extends RowData> {
18
28
  rows: T[]
19
- columns: TableOptions<T>['columns']
29
+ columns: DataGridColumn<T>[]
20
30
  getRowId: (row: T) => string
21
31
  /** Optional grouping. Return a node spanning all columns to render this row
22
32
  * as a (non-editable) section header. Return null to render the row as
@@ -39,7 +49,7 @@ interface DataGridProps<T> {
39
49
  scrollable?: boolean
40
50
  }
41
51
 
42
- export function DataGrid<T>({
52
+ export function DataGrid<T extends RowData>({
43
53
  rows,
44
54
  columns,
45
55
  getRowId,
@@ -48,11 +58,11 @@ export function DataGrid<T>({
48
58
  'aria-label': ariaLabel,
49
59
  scrollable = false,
50
60
  }: DataGridProps<T>) {
51
- const table = useReactTable<T>({
61
+ const table = useTable({
62
+ features: dataGridFeatures,
52
63
  data: rows,
53
64
  columns,
54
65
  getRowId,
55
- getCoreRowModel: getCoreRowModel(),
56
66
  })
57
67
 
58
68
  return (
@@ -66,14 +76,14 @@ export function DataGrid<T>({
66
76
  )
67
77
  }
68
78
 
69
- function DataGridShell<T>({
79
+ function DataGridShell<T extends RowData>({
70
80
  table,
71
81
  renderSectionHeader,
72
82
  className,
73
83
  ariaLabel,
74
84
  scrollable,
75
85
  }: {
76
- table: Table<T>
86
+ table: Table<DataGridFeatures, T>
77
87
  renderSectionHeader?: RenderSectionHeader<T>
78
88
  className?: string
79
89
  ariaLabel?: string
@@ -324,12 +334,12 @@ function DataGridShell<T>({
324
334
  )
325
335
  }
326
336
 
327
- function DataGridRow<T>({
337
+ function DataGridRow<T extends RowData>({
328
338
  row,
329
339
  renderSectionHeader,
330
340
  columnCount,
331
341
  }: {
332
- row: Row<T>
342
+ row: Row<DataGridFeatures, T>
333
343
  renderSectionHeader?: RenderSectionHeader<T>
334
344
  columnCount: number
335
345
  }) {
@@ -345,7 +355,7 @@ function DataGridRow<T>({
345
355
  }
346
356
  return (
347
357
  <tr>
348
- {row.getVisibleCells().map((cell) => (
358
+ {row.getAllCells().map((cell) => (
349
359
  <td key={cell.id} className="h-9 p-0 align-middle">
350
360
  {flexRender(cell.column.columnDef.cell, cell.getContext())}
351
361
  </td>
@@ -8,6 +8,6 @@ export {
8
8
  TextCell,
9
9
  } from './cells'
10
10
  export { useDataGridContext } from './context'
11
- export { DataGrid } from './data-grid'
11
+ export { DataGrid, type DataGridColumn } from './data-grid'
12
12
  export { editableRowIndex } from './helpers'
13
13
  export type { CellCoords, RenderSectionHeader } from './types'