@ciromaciel/dashboard-kit 1.8.1 → 1.9.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.
- package/package.json +1 -1
- package/src/components/DataTable.jsx +105 -55
package/package.json
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Fragment, useState } from 'react'
|
|
1
2
|
import { Box, Card, Text } from '@mantine/core'
|
|
2
3
|
import { useMediaQuery } from '@mantine/hooks'
|
|
3
4
|
import { templateFor, ROW_PADDING, HEAD_PADDING } from './table-grid'
|
|
@@ -23,12 +24,34 @@ import { templateFor, ROW_PADDING, HEAD_PADDING } from './table-grid'
|
|
|
23
24
|
* Config-driven per Law 4: `columns` describes the columns with data
|
|
24
25
|
* (key/label/width), and each column brings its own `render`. The consumer
|
|
25
26
|
* never injects the table's structure — it only says what each cell shows.
|
|
27
|
+
*
|
|
28
|
+
* Since 14/10/2026 it is the ONLY table. Blogs, Pages and the People
|
|
29
|
+
* strategies drew the same grid with their own `GridTable` (identical measures,
|
|
30
|
+
* a second copy), and the Functions invocation log used Mantine's <Table>.
|
|
31
|
+
* What they needed came here:
|
|
32
|
+
* getRowTone(row) 'broken' → red.0 background · 'lead' → the selected look
|
|
33
|
+
* renderDetail(row) a row that expands: the click toggles a full-width panel
|
|
34
|
+
* under it (the invocation console). Ignored when
|
|
35
|
+
* `onRowClick` is set — a row does one thing.
|
|
36
|
+
* className so a panel's cell helpers (`.pg-tbl .cell-t`) still apply
|
|
37
|
+
* bare no border and no background of its own: the table lives
|
|
38
|
+
* inside a card or panel that already draws them
|
|
39
|
+
* hideSm the GridTable name for `hideBelow`
|
|
40
|
+
* The row body is the spec's "data" role — 13px, gray.7, line-height 1.4 —
|
|
41
|
+
* which until then only the copies applied.
|
|
26
42
|
*/
|
|
27
43
|
|
|
28
44
|
const HEAD = { fz: 10, fw: 800, c: 'gray.5', tt: 'uppercase', lts: '1.5px', lh: 1.4 }
|
|
29
45
|
|
|
30
|
-
export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected, quietSelection, getRowLabel, toolbar, footer, emptyState, loading, skeleton }) {
|
|
31
|
-
const
|
|
46
|
+
export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected, quietSelection, getRowLabel, getRowTone, renderDetail, className, bare, toolbar, footer, emptyState, loading, skeleton }) {
|
|
47
|
+
const [openKeys, setOpenKeys] = useState(() => new Set())
|
|
48
|
+
const expandable = typeof renderDetail === 'function' && typeof onRowClick !== 'function'
|
|
49
|
+
const toggle = key =>
|
|
50
|
+
setOpenKeys(prev => {
|
|
51
|
+
const next = new Set(prev)
|
|
52
|
+
next.has(key) ? next.delete(key) : next.add(key)
|
|
53
|
+
return next
|
|
54
|
+
})
|
|
32
55
|
|
|
33
56
|
/*
|
|
34
57
|
* A column with `hideBelow` disappears on phones — and really disappears:
|
|
@@ -38,12 +61,15 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
|
|
|
38
61
|
* them.
|
|
39
62
|
*/
|
|
40
63
|
const wide = useMediaQuery('(min-width: 48em)', true)
|
|
41
|
-
const visible = wide ? columns : columns.filter(col => !col.hideBelow)
|
|
64
|
+
const visible = wide ? columns : columns.filter(col => !col.hideBelow && !col.hideSm)
|
|
42
65
|
const template = templateFor(visible)
|
|
43
66
|
|
|
44
67
|
return (
|
|
45
68
|
<Card
|
|
46
69
|
p={0}
|
|
70
|
+
className={className}
|
|
71
|
+
withBorder={bare ? false : undefined}
|
|
72
|
+
bg={bare ? 'transparent' : undefined}
|
|
47
73
|
style={{ overflow: 'hidden' }}
|
|
48
74
|
>
|
|
49
75
|
{toolbar}
|
|
@@ -84,66 +110,90 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
|
|
|
84
110
|
|
|
85
111
|
{!loading &&
|
|
86
112
|
rows.map((row, i) => {
|
|
87
|
-
const
|
|
113
|
+
const key = getRowKey(row)
|
|
114
|
+
const tone = getRowTone?.(row)
|
|
115
|
+
const selected = isRowSelected?.(row) || tone === 'lead' || undefined
|
|
116
|
+
const detail = expandable ? renderDetail(row) : null
|
|
117
|
+
const hasDetail = Boolean(detail)
|
|
118
|
+
const open = hasDetail && openKeys.has(key)
|
|
119
|
+
const clickable = typeof onRowClick === 'function' || hasDetail
|
|
120
|
+
const activate = () => (typeof onRowClick === 'function' ? onRowClick(row) : toggle(key))
|
|
88
121
|
const last = i === rows.length - 1
|
|
122
|
+
const rest = tone === 'broken' ? 'var(--mantine-color-red-0)' : selected ? 'var(--mantine-color-gray-1)' : ''
|
|
89
123
|
|
|
90
124
|
return (
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
e.
|
|
103
|
-
|
|
125
|
+
<Fragment key={key}>
|
|
126
|
+
<Box
|
|
127
|
+
data-selected={selected}
|
|
128
|
+
data-tone={tone}
|
|
129
|
+
onClick={clickable ? activate : undefined}
|
|
130
|
+
// The row is the target: it has to announce itself to the
|
|
131
|
+
// keyboard and the screen reader as the button it actually is.
|
|
132
|
+
onKeyDown={
|
|
133
|
+
clickable
|
|
134
|
+
? e => {
|
|
135
|
+
if (e.target !== e.currentTarget) return
|
|
136
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
137
|
+
e.preventDefault()
|
|
138
|
+
activate()
|
|
139
|
+
}
|
|
104
140
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
: undefined
|
|
142
|
+
}
|
|
143
|
+
onMouseEnter={clickable ? e => (e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-1)') : undefined}
|
|
144
|
+
onMouseLeave={clickable ? e => (e.currentTarget.style.backgroundColor = rest) : undefined}
|
|
145
|
+
tabIndex={clickable ? 0 : undefined}
|
|
146
|
+
role={clickable ? 'button' : undefined}
|
|
147
|
+
aria-pressed={typeof onRowClick === 'function' && isRowSelected ? Boolean(selected) : undefined}
|
|
148
|
+
aria-expanded={hasDetail ? open : undefined}
|
|
149
|
+
aria-label={clickable ? getRowLabel?.(row) : undefined}
|
|
150
|
+
style={{
|
|
151
|
+
display: 'grid',
|
|
152
|
+
gridTemplateColumns: template,
|
|
153
|
+
gap: 16,
|
|
154
|
+
alignItems: 'center',
|
|
155
|
+
padding: ROW_PADDING,
|
|
156
|
+
cursor: clickable ? 'pointer' : undefined,
|
|
157
|
+
borderBottom: last && !open ? 'none' : 'var(--rlg-border)',
|
|
158
|
+
transition: 'background-color 120ms ease',
|
|
159
|
+
// The "data" role: what a cell shows when it brings no type of its own.
|
|
160
|
+
fontSize: 13,
|
|
161
|
+
color: 'var(--mantine-color-gray-7)',
|
|
162
|
+
lineHeight: 1.4,
|
|
163
|
+
/* Selected: black hairline on the left. Not a strong
|
|
164
|
+
background color, which would compete with the hover.
|
|
125
165
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
166
|
+
`quietSelection` keeps the background and drops the
|
|
167
|
+
hairline. The 2px mark was designed for
|
|
168
|
+
SELECTION — the row you just clicked — and it
|
|
169
|
+
weighs too much when the highlight is a
|
|
170
|
+
permanent state of the data (the live version,
|
|
171
|
+
which already announces itself with the badge on
|
|
172
|
+
the row). A broken row is red.0: the state of the
|
|
173
|
+
data, said by the row itself. */
|
|
174
|
+
backgroundColor: rest || undefined,
|
|
175
|
+
boxShadow: selected && !quietSelection ? 'inset 2px 0 0 var(--mantine-color-gray-9)' : undefined,
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
{visible.map(col => (
|
|
179
|
+
<Box
|
|
180
|
+
key={col.key}
|
|
181
|
+
miw={0}
|
|
182
|
+
style={col.align ? { textAlign: col.align } : undefined}
|
|
183
|
+
>
|
|
184
|
+
{col.render(row, { open })}
|
|
185
|
+
</Box>
|
|
186
|
+
))}
|
|
187
|
+
</Box>
|
|
188
|
+
{open && (
|
|
138
189
|
<Box
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
style={col.align ? { textAlign: col.align } : undefined}
|
|
190
|
+
bg="gray.1"
|
|
191
|
+
style={{ borderBottom: last ? 'none' : 'var(--rlg-border)' }}
|
|
142
192
|
>
|
|
143
|
-
{
|
|
193
|
+
{detail}
|
|
144
194
|
</Box>
|
|
145
|
-
)
|
|
146
|
-
</
|
|
195
|
+
)}
|
|
196
|
+
</Fragment>
|
|
147
197
|
)
|
|
148
198
|
})}
|
|
149
199
|
|