@ciromaciel/dashboard-kit 1.8.1 → 1.9.1

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.1",
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,37 @@ 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
+ * stackOnPhone below 48em the head goes and each row stacks its cells in
41
+ * one column — for lists where every column carries
42
+ * something the row needs (Posts, the People strategies)
43
+ * The row body is the spec's "data" role — 13px, gray.7, line-height 1.4 —
44
+ * which until then only the copies applied.
26
45
  */
27
46
 
28
47
  const HEAD = { fz: 10, fw: 800, c: 'gray.5', tt: 'uppercase', lts: '1.5px', lh: 1.4 }
29
48
 
30
- export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected, quietSelection, getRowLabel, toolbar, footer, emptyState, loading, skeleton }) {
31
- const clickable = typeof onRowClick === 'function'
49
+ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected, quietSelection, getRowLabel, getRowTone, renderDetail, className, bare, stackOnPhone, toolbar, footer, emptyState, loading, skeleton }) {
50
+ const [openKeys, setOpenKeys] = useState(() => new Set())
51
+ const expandable = typeof renderDetail === 'function' && typeof onRowClick !== 'function'
52
+ const toggle = key =>
53
+ setOpenKeys(prev => {
54
+ const next = new Set(prev)
55
+ next.has(key) ? next.delete(key) : next.add(key)
56
+ return next
57
+ })
32
58
 
33
59
  /*
34
60
  * A column with `hideBelow` disappears on phones — and really disappears:
@@ -38,16 +64,21 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
38
64
  * them.
39
65
  */
40
66
  const wide = useMediaQuery('(min-width: 48em)', true)
41
- const visible = wide ? columns : columns.filter(col => !col.hideBelow)
42
- const template = templateFor(visible)
67
+ const visible = wide ? columns : columns.filter(col => !col.hideBelow && !col.hideSm)
68
+ const stacked = stackOnPhone && !wide
69
+ const template = stacked ? 'minmax(0, 1fr)' : templateFor(visible)
43
70
 
44
71
  return (
45
72
  <Card
46
73
  p={0}
74
+ className={className}
75
+ withBorder={bare ? false : undefined}
76
+ bg={bare ? 'transparent' : undefined}
47
77
  style={{ overflow: 'hidden' }}
48
78
  >
49
79
  {toolbar}
50
80
 
81
+ {!stacked && (
51
82
  <Box
52
83
  bg="gray.1"
53
84
  style={{
@@ -73,6 +104,7 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
73
104
  ),
74
105
  )}
75
106
  </Box>
107
+ )}
76
108
 
77
109
  {/* First load: the skeleton has the SAME grid as the table, so the
78
110
  layout doesn't jump when the data arrives (Law 3). A refetch with
@@ -84,66 +116,90 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
84
116
 
85
117
  {!loading &&
86
118
  rows.map((row, i) => {
87
- const selected = isRowSelected?.(row) || undefined
119
+ const key = getRowKey(row)
120
+ const tone = getRowTone?.(row)
121
+ const selected = isRowSelected?.(row) || tone === 'lead' || undefined
122
+ const detail = expandable ? renderDetail(row) : null
123
+ const hasDetail = Boolean(detail)
124
+ const open = hasDetail && openKeys.has(key)
125
+ const clickable = typeof onRowClick === 'function' || hasDetail
126
+ const activate = () => (typeof onRowClick === 'function' ? onRowClick(row) : toggle(key))
88
127
  const last = i === rows.length - 1
128
+ const rest = tone === 'broken' ? 'var(--mantine-color-red-0)' : selected ? 'var(--mantine-color-gray-1)' : ''
89
129
 
90
130
  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)
131
+ <Fragment key={key}>
132
+ <Box
133
+ data-selected={selected}
134
+ data-tone={tone}
135
+ onClick={clickable ? activate : undefined}
136
+ // The row is the target: it has to announce itself to the
137
+ // keyboard and the screen reader as the button it actually is.
138
+ onKeyDown={
139
+ clickable
140
+ ? e => {
141
+ if (e.target !== e.currentTarget) return
142
+ if (e.key === 'Enter' || e.key === ' ') {
143
+ e.preventDefault()
144
+ activate()
145
+ }
104
146
  }
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.
147
+ : undefined
148
+ }
149
+ onMouseEnter={clickable ? e => (e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-1)') : undefined}
150
+ onMouseLeave={clickable ? e => (e.currentTarget.style.backgroundColor = rest) : undefined}
151
+ tabIndex={clickable ? 0 : undefined}
152
+ role={clickable ? 'button' : undefined}
153
+ aria-pressed={typeof onRowClick === 'function' && isRowSelected ? Boolean(selected) : undefined}
154
+ aria-expanded={hasDetail ? open : undefined}
155
+ aria-label={clickable ? getRowLabel?.(row) : undefined}
156
+ style={{
157
+ display: 'grid',
158
+ gridTemplateColumns: template,
159
+ gap: stacked ? 6 : 16,
160
+ alignItems: stacked ? 'start' : 'center',
161
+ padding: stacked ? '14px 16px' : ROW_PADDING,
162
+ cursor: clickable ? 'pointer' : undefined,
163
+ borderBottom: last && !open ? 'none' : 'var(--rlg-border)',
164
+ transition: 'background-color 120ms ease',
165
+ // The "data" role: what a cell shows when it brings no type of its own.
166
+ fontSize: 13,
167
+ color: 'var(--mantine-color-gray-7)',
168
+ lineHeight: 1.4,
169
+ /* Selected: black hairline on the left. Not a strong
170
+ background color, which would compete with the hover.
125
171
 
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 => (
172
+ `quietSelection` keeps the background and drops the
173
+ hairline. The 2px mark was designed for
174
+ SELECTION — the row you just clicked — and it
175
+ weighs too much when the highlight is a
176
+ permanent state of the data (the live version,
177
+ which already announces itself with the badge on
178
+ the row). A broken row is red.0: the state of the
179
+ data, said by the row itself. */
180
+ backgroundColor: rest || undefined,
181
+ boxShadow: selected && !quietSelection ? 'inset 2px 0 0 var(--mantine-color-gray-9)' : undefined,
182
+ }}
183
+ >
184
+ {visible.map(col => (
185
+ <Box
186
+ key={col.key}
187
+ miw={0}
188
+ style={col.align && !stacked ? { textAlign: col.align } : undefined}
189
+ >
190
+ {col.render(row, { open })}
191
+ </Box>
192
+ ))}
193
+ </Box>
194
+ {open && (
138
195
  <Box
139
- key={col.key}
140
- miw={0}
141
- style={col.align ? { textAlign: col.align } : undefined}
196
+ bg="gray.1"
197
+ style={{ borderBottom: last ? 'none' : 'var(--rlg-border)' }}
142
198
  >
143
- {col.render(row)}
199
+ {detail}
144
200
  </Box>
145
- ))}
146
- </Box>
201
+ )}
202
+ </Fragment>
147
203
  )
148
204
  })}
149
205