@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ciromaciel/dashboard-kit",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "The components every dashboard shares: table, empty state, loading state, destructive confirm, danger zone and wordmark.",
@@ -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 clickable = typeof onRowClick === 'function'
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 selected = isRowSelected?.(row) || undefined
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
- <Box
92
- key={getRowKey(row)}
93
- data-selected={selected}
94
- onClick={clickable ? () => onRowClick(row) : undefined}
95
- // The row is the target: it has to announce itself to the
96
- // keyboard and the screen reader as the button it actually is.
97
- onKeyDown={
98
- clickable
99
- ? e => {
100
- if (e.target !== e.currentTarget) return
101
- if (e.key === 'Enter' || e.key === ' ') {
102
- e.preventDefault()
103
- onRowClick(row)
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
- : undefined
107
- }
108
- onMouseEnter={clickable ? e => (e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-1)') : undefined}
109
- onMouseLeave={clickable ? e => (e.currentTarget.style.backgroundColor = selected ? 'var(--mantine-color-gray-1)' : '') : undefined}
110
- tabIndex={clickable ? 0 : undefined}
111
- role={clickable ? 'button' : undefined}
112
- aria-pressed={clickable && isRowSelected ? Boolean(selected) : undefined}
113
- aria-label={clickable ? getRowLabel?.(row) : undefined}
114
- style={{
115
- display: 'grid',
116
- gridTemplateColumns: template,
117
- gap: 16,
118
- alignItems: 'center',
119
- padding: ROW_PADDING,
120
- cursor: clickable ? 'pointer' : undefined,
121
- borderBottom: last ? 'none' : 'var(--rlg-border)',
122
- transition: 'background-color 120ms ease',
123
- /* Selected: black hairline on the left. Not a strong
124
- background color, which would compete with the hover.
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
- `quietSelection` keeps the background and drops the
127
- hairline. The 2px mark was designed for
128
- SELECTION — the row you just clicked — and it
129
- weighs too much when the highlight is a
130
- permanent state of the data (the live version,
131
- which already announces itself with the badge on
132
- the row). */
133
- backgroundColor: selected ? 'var(--mantine-color-gray-1)' : undefined,
134
- boxShadow: selected && !quietSelection ? 'inset 2px 0 0 var(--mantine-color-gray-9)' : undefined,
135
- }}
136
- >
137
- {visible.map(col => (
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
- key={col.key}
140
- miw={0}
141
- style={col.align ? { textAlign: col.align } : undefined}
190
+ bg="gray.1"
191
+ style={{ borderBottom: last ? 'none' : 'var(--rlg-border)' }}
142
192
  >
143
- {col.render(row)}
193
+ {detail}
144
194
  </Box>
145
- ))}
146
- </Box>
195
+ )}
196
+ </Fragment>
147
197
  )
148
198
  })}
149
199