@ciromaciel/dashboard-kit 1.3.1 → 1.5.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.3.1",
3
+ "version": "1.5.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.",
@@ -52,7 +52,7 @@ export function ActiveResource({ title, subtitle, tone = 'muted', mono = false }
52
52
 
53
53
  {subtitle && (
54
54
  <Text
55
- fz={9}
55
+ fz={10}
56
56
  fw={800}
57
57
  c={tone === 'alert' ? 'red.6' : 'gray.4'}
58
58
  tt="uppercase"
@@ -0,0 +1,56 @@
1
+ import { Box, Text, UnstyledButton } from '@mantine/core'
2
+
3
+ import { Wordmark } from './Wordmark'
4
+
5
+ /**
6
+ * O topo da coluna: o nome do produto e, embaixo, o que ele cuida.
7
+ *
8
+ * VEIO DE SETE CÓPIAS IGUAIS (Blogs, Forms, Links, Newsletter, Pages, Tasks,
9
+ * Videos) e uma quase igual (People, com cinza e espaçamento próprios). A
10
+ * linha de baixo virou padrão em 10/10/2026 (Ciro): todo painel diz, em duas
11
+ * palavras, o que cabe nele — "Artigos · Publicação", "Edições · Assinantes".
12
+ * Ela orienta quem chega e separa produtos de nome parecido na mesma linha.
13
+ *
14
+ * O nome é a volta para `/` (a lista de recursos, ou o Início no painel sem
15
+ * recurso): o mesmo destino do item "Todos os …" da coluna.
16
+ *
17
+ * @param {string} name O nome do produto ("Blogs").
18
+ * @param {string} tagline Os dois termos, separados por " · ". Some quando
19
+ * não há: uma linha vazia ocuparia espaço sem dizer nada.
20
+ * @param {Function} onClick Ir para `/`.
21
+ * @param {string} label O rótulo de acessibilidade do nome ("Todos os blogs").
22
+ */
23
+ export function ColumnBrand({ name, tagline, onClick, label }) {
24
+ return (
25
+ <Box
26
+ px={12}
27
+ pb={8}
28
+ >
29
+ <UnstyledButton
30
+ onClick={onClick}
31
+ aria-label={label ?? name}
32
+ w="100%"
33
+ style={{ display: 'block', textAlign: 'center' }}
34
+ >
35
+ <Wordmark
36
+ name={name}
37
+ size={28}
38
+ />
39
+ </UnstyledButton>
40
+
41
+ {tagline && (
42
+ <Text
43
+ ta="center"
44
+ fz={10}
45
+ fw={800}
46
+ c="gray.4"
47
+ tt="uppercase"
48
+ lts="1.5px"
49
+ mt={6}
50
+ >
51
+ {tagline}
52
+ </Text>
53
+ )}
54
+ </Box>
55
+ )
56
+ }
@@ -25,7 +25,7 @@ import { templateFor, ROW_PADDING, HEAD_PADDING } from './table-grid'
25
25
  * never injects the table's structure — it only says what each cell shows.
26
26
  */
27
27
 
28
- const HEAD = { fz: 10, fw: 800, c: 'gray.5', tt: 'uppercase', ls: '1.5px', lh: 1.4 }
28
+ const HEAD = { fz: 10, fw: 800, c: 'gray.5', tt: 'uppercase', lts: '1.5px', lh: 1.4 }
29
29
 
30
30
  export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected, quietSelection, getRowLabel, toolbar, footer, emptyState, loading, skeleton }) {
31
31
  const clickable = typeof onRowClick === 'function'
@@ -162,7 +162,7 @@ export function DeleteModal({
162
162
  fw={800}
163
163
  c="red.6"
164
164
  tt="uppercase"
165
- ls="1.5px"
165
+ lts="1.5px"
166
166
  >
167
167
  {eyebrow}
168
168
  </Text>
@@ -182,7 +182,7 @@ export function DeleteModal({
182
182
  fz={scale.fz}
183
183
  lh={1.15}
184
184
  mt={12}
185
- ls="-0.04em"
185
+ lts="-0.04em"
186
186
  c="gray.9"
187
187
  /*
188
188
  * `break-all` only where the title IS the identifier. On the
@@ -23,7 +23,7 @@ export function EmptyState({ title, description, actionLabel, onAction }) {
23
23
  fw={800}
24
24
  c="gray.4"
25
25
  tt="uppercase"
26
- ls="1.5px"
26
+ lts="1.5px"
27
27
  ta="center"
28
28
  >
29
29
  {title}
@@ -138,8 +138,13 @@ export function HoldToConfirmButton({ children, holdingLabel, onConfirm, disable
138
138
  {...props}
139
139
  >
140
140
  {active ? (
141
+ /* The countdown inherits the button's colour. It was gray.6,
142
+ which on the filled red.6 variant measured about 1.3:1 —
143
+ the number that says how long is left was unreadable.
144
+ Explicit `inherit`, because the panel themes give Text a
145
+ default colour that would otherwise win. */
141
146
  <Text
142
- c="gray.6"
147
+ c="inherit"
143
148
  span
144
149
  inherit
145
150
  >
@@ -110,7 +110,7 @@ export function Metric({ label, value, unit, note, hint, delta, deltaSuffix = '%
110
110
  fw={900}
111
111
  c="gray.9"
112
112
  lh={1}
113
- ls="-0.04em"
113
+ lts="-0.04em"
114
114
  style={{ overflowWrap: 'anywhere' }}
115
115
  >
116
116
  {value ?? '—'}
@@ -134,7 +134,7 @@ export function Metric({ label, value, unit, note, hint, delta, deltaSuffix = '%
134
134
  fw={800}
135
135
  c="gray.4"
136
136
  tt="uppercase"
137
- ls="1.5px"
137
+ lts="1.5px"
138
138
  lh={1.4}
139
139
  mt={12}
140
140
  >
@@ -77,7 +77,7 @@ export function MoreResults({ loading, hasMore, onLoadMore, shown, total, noun =
77
77
  fw={800}
78
78
  c="gray.4"
79
79
  tt="uppercase"
80
- ls="1.5px"
80
+ lts="1.5px"
81
81
  >
82
82
  {total} {total === 1 ? noun : plural} · fim da lista
83
83
  </Text>
@@ -103,7 +103,7 @@ export function MoreResults({ loading, hasMore, onLoadMore, shown, total, noun =
103
103
  fw={800}
104
104
  c="gray.4"
105
105
  tt="uppercase"
106
- ls="1.5px"
106
+ lts="1.5px"
107
107
  /* `aria-live` because a scroll-triggered load has no
108
108
  focus: without this, a screen reader would not know
109
109
  the list had grown. */
@@ -127,7 +127,7 @@ export function MoreResults({ loading, hasMore, onLoadMore, shown, total, noun =
127
127
  fw={800}
128
128
  c="gray.4"
129
129
  tt="uppercase"
130
- ls="1.5px"
130
+ lts="1.5px"
131
131
  >
132
132
  {shown} de {total}
133
133
  </Text>
@@ -64,7 +64,7 @@ export function PageHeader({ crumb, title, subtitle, actions, order = 1, mb = 32
64
64
  fw={800}
65
65
  c="gray.4"
66
66
  tt="uppercase"
67
- ls="1.5px"
67
+ lts="1.5px"
68
68
  lh={1.4}
69
69
  >
70
70
  {trilha}
@@ -91,7 +91,7 @@ export function PageHeader({ crumb, title, subtitle, actions, order = 1, mb = 32
91
91
  */
92
92
  fw={900}
93
93
  lh={1}
94
- ls="-0.04em"
94
+ lts="-0.04em"
95
95
  c="gray.9"
96
96
  style={{ overflowWrap: 'anywhere' }}
97
97
  >
@@ -75,7 +75,7 @@ export function ResourceItem({ icon: Icon, title, subtitle, tone = 'muted', mono
75
75
 
76
76
  {subtitle && (
77
77
  <Text
78
- fz={9}
78
+ fz={10}
79
79
  fw={800}
80
80
  c={tone === 'alert' ? 'red.6' : 'gray.4'}
81
81
  tt="uppercase"
@@ -56,7 +56,7 @@ export function SectionCard({ eyebrow, title, lead, action, children }) {
56
56
  c="gray.4"
57
57
  tt="uppercase"
58
58
  mb={6}
59
- ls="1.5px"
59
+ lts="1.5px"
60
60
  >
61
61
  {eyebrow}
62
62
  </Text>
@@ -66,7 +66,7 @@ export function SectionCard({ eyebrow, title, lead, action, children }) {
66
66
  fz={18}
67
67
  fw={800}
68
68
  lh={1.2}
69
- ls="-0.02em"
69
+ lts="-0.02em"
70
70
  c="gray.9"
71
71
  >
72
72
  {title}
@@ -111,7 +111,7 @@ export function HealthRow({ health, host }) {
111
111
  />
112
112
  <Text
113
113
  fz={12}
114
- fw={600}
114
+ fw={500}
115
115
  c="gray.9"
116
116
  lineClamp={1}
117
117
  >
@@ -198,7 +198,7 @@ export function SupportModal({
198
198
  fz={{ base: 26, sm: 36 }}
199
199
  lh={1.05}
200
200
  mt={12}
201
- ls="-0.04em"
201
+ lts="-0.04em"
202
202
  c="gray.9"
203
203
  >
204
204
  {title}{' '}
@@ -124,7 +124,7 @@ export function TableToolbar({ value = '', onChange, placeholder = 'Buscar…',
124
124
  fw={800}
125
125
  c="gray.4"
126
126
  tt="uppercase"
127
- ls="1.5px"
127
+ lts="1.5px"
128
128
  style={{ flexShrink: 0 }}
129
129
  visibleFrom="sm"
130
130
  >
@@ -40,7 +40,7 @@ export function Wordmark({ name, variant = 'mark', size, ...props }) {
40
40
  fz={size ?? (isLabel ? 11 : 28)}
41
41
  fw={isLabel ? 800 : 900}
42
42
  tt={isLabel ? 'uppercase' : undefined}
43
- ls={isLabel ? '1.5px' : '-0.04em'}
43
+ lts={isLabel ? '1.5px' : '-0.04em'}
44
44
  lh={isLabel ? 1.4 : 1}
45
45
  c={isLabel ? 'gray.4' : 'gray.9'}
46
46
  ta="center"
@@ -19,7 +19,7 @@ export function ZenLabel({ children, ...props }) {
19
19
  fz={11}
20
20
  fw={800}
21
21
  tt="uppercase"
22
- ls="1.5px"
22
+ lts="1.5px"
23
23
  c="gray.4"
24
24
  lh={1.4}
25
25
  {...props}
@@ -43,20 +43,31 @@ const icon = Component => createElement(Component, { size: 18, stroke: 2, color:
43
43
  * rule — `--mantine-color-black`, a pure black the system's ramp does not
44
44
  * contain (gray.9 is #11181C precisely to avoid it).
45
45
  */
46
- const show = (color, glyph, title, message) =>
46
+ const show = (color, glyph, title, message, options) =>
47
47
  notifications.show({
48
48
  ...(message === undefined ? { message: title } : { title, message }),
49
49
  color,
50
50
  icon: glyph,
51
+ ...options,
51
52
  })
52
53
 
54
+ /**
55
+ * An error stays until it is dismissed.
56
+ *
57
+ * Apple's rule is to prefer explicit dismissal over views that vanish on a
58
+ * timer, and WCAG 2.2.1 says the same: an error read in five seconds is an
59
+ * error missed by whoever looked away. Success and info still leave on their
60
+ * own — they ask for nothing.
61
+ */
62
+ const STICKY = { autoClose: false }
63
+
53
64
  export const notify = {
54
65
  success(title, message) {
55
66
  show('teal', icon(IconCheck), title, message)
56
67
  },
57
68
 
58
69
  error(title, message) {
59
- show('red', icon(IconAlertCircle), title, message)
70
+ show('red', icon(IconAlertCircle), title, message, STICKY)
60
71
  },
61
72
 
62
73
  info(title, message) {
package/src/index.js CHANGED
@@ -80,6 +80,7 @@ export { NavItem } from './components/NavItem'
80
80
  export { GroupLabel } from './components/GroupLabel'
81
81
  export { ResourceItem } from './components/ResourceItem'
82
82
  export { ActiveResource } from './components/ActiveResource'
83
+ export { ColumnBrand } from './components/ColumnBrand'
83
84
  export { ListSkeleton } from './components/ListSkeleton'
84
85
  export { DashboardFrame, SIDEBAR_WIDTH } from './components/DashboardFrame'
85
86
  export { SidebarShell } from './components/SidebarShell'
package/src/tokens.css CHANGED
@@ -56,3 +56,61 @@
56
56
  .rlg-danger:focus-visible {
57
57
  outline-color: var(--mantine-color-red-7);
58
58
  }
59
+
60
+ /*
61
+ * The rules below are the same floor the components-kit ships in its global
62
+ * stylesheet. The panels don't load that sheet, so they are repeated here — the
63
+ * two files must move together.
64
+ */
65
+
66
+ /*
67
+ * One focus ring everywhere: gray.9, 2px, 2px away, square like the element it
68
+ * surrounds. `.rlg-danger` above keeps the shape and changes only the colour.
69
+ */
70
+ :focus-visible {
71
+ outline: 2px solid var(--mantine-color-gray-9);
72
+ outline-offset: 2px;
73
+ }
74
+
75
+ /*
76
+ * 44px under a finger. A panel is dense on purpose — 36px search, 36px create
77
+ * button, `sm` buttons — and that is right for a mouse. A thumb needs Apple's
78
+ * 44px (WCAG 2.5.5), so only coarse pointers get the floor.
79
+ */
80
+ @media (pointer: coarse) {
81
+ .mantine-Button-root,
82
+ .mantine-ActionIcon-root,
83
+ .mantine-CloseButton-root {
84
+ min-height: 44px;
85
+ min-width: 44px;
86
+ }
87
+ }
88
+
89
+ /*
90
+ * Increased contrast moves the ramp one step darker where it is weakest: the
91
+ * gray.4 label (2.6:1) becomes gray.6, gray.5 becomes gray.7, and the gray.2
92
+ * hairline becomes gray.4. `:root:root` beats the `:root` Mantine injects at
93
+ * runtime, after this sheet.
94
+ */
95
+ @media (prefers-contrast: more) {
96
+ :root:root {
97
+ --mantine-color-gray-2: #9ca3af;
98
+ --mantine-color-gray-3: #9ca3af;
99
+ --mantine-color-gray-4: #4b5563;
100
+ --mantine-color-gray-5: #374151;
101
+ --mantine-color-gray-6: #374151;
102
+ }
103
+ }
104
+
105
+ /*
106
+ * Reduced motion: the sidebar appears instead of sliding in. Background and
107
+ * colour fades on rows and menu items stay — a fade is what Apple asks motion
108
+ * to become, not something to remove.
109
+ */
110
+ @media (prefers-reduced-motion: reduce) {
111
+ .mantine-AppShell-navbar,
112
+ .mantine-AppShell-main,
113
+ .mantine-AppShell-header {
114
+ transition: none !important;
115
+ }
116
+ }