@ciromaciel/dashboard-kit 1.9.1 → 1.10.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.9.1",
3
+ "version": "1.10.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,5 +1,5 @@
1
1
  import { ActionIcon, Tooltip, useMantineColorScheme } from '@mantine/core'
2
- import { useMounted } from '@mantine/hooks'
2
+ import { useColorScheme, useMounted } from '@mantine/hooks'
3
3
  import { IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react'
4
4
 
5
5
  /**
@@ -11,6 +11,12 @@ import { IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react'
11
11
  * twice: the tooltip and the accessible name both read "Tema: Claro. Mudar
12
12
  * para Escuro." — what it is now, and what one click does.
13
13
  *
14
+ * The cycle follows the device (14/10/2026): from Automático the first click
15
+ * goes to the theme OPPOSITE the device's, so every click from Automático
16
+ * changes the screen. Before, on a light device, Automático → Claro changed
17
+ * nothing visible. The tooltip opens on focus and touch too, not only on
18
+ * hover — keyboard and touch users see the state as well.
19
+ *
14
20
  * The icon waits for the mount. Mantine reads the stored choice from
15
21
  * localStorage on the client only; a prerendered page would otherwise paint the
16
22
  * server's icon and swap it on hydration.
@@ -18,22 +24,25 @@ import { IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react'
18
24
  * The same component lives in the components-kit for the sites; the two must
19
25
  * move together.
20
26
  */
21
- const NEXT = { auto: 'light', light: 'dark', dark: 'auto' }
22
27
  const NAME = { auto: 'Automático', light: 'Claro', dark: 'Escuro' }
23
28
  const ICON = { auto: IconDeviceDesktop, light: IconSun, dark: IconMoon }
24
29
 
25
30
  export function ColorSchemeToggle({ size = 36, ...props }) {
26
31
  const { colorScheme, setColorScheme } = useMantineColorScheme()
27
32
  const isMounted = useMounted()
33
+ const system = useColorScheme('light')
28
34
  const current = isMounted ? colorScheme : 'auto'
35
+ const opposite = system === 'dark' ? 'light' : 'dark'
36
+ const next = { auto: opposite, [opposite]: system, [system]: 'auto' }[current] ?? 'auto'
29
37
  const Icon = ICON[current]
30
- const label = `Tema: ${NAME[current]}. Mudar para ${NAME[NEXT[current]]}.`
38
+ const label = `Tema: ${NAME[current]}. Mudar para ${NAME[next]}.`
31
39
 
32
40
  return (
33
41
  <Tooltip
34
42
  label={label}
35
43
  position="top"
36
44
  withArrow={false}
45
+ events={{ hover: true, focus: true, touch: true }}
37
46
  >
38
47
  <ActionIcon
39
48
  data-color-scheme-toggle
@@ -42,7 +51,7 @@ export function ColorSchemeToggle({ size = 36, ...props }) {
42
51
  size={size}
43
52
  radius={0}
44
53
  aria-label={label}
45
- onClick={() => setColorScheme(NEXT[current])}
54
+ onClick={() => setColorScheme(next)}
46
55
  {...props}
47
56
  >
48
57
  <Icon
@@ -12,11 +12,12 @@ import { templateFor, ROW_PADDING, HEAD_PADDING } from './table-grid'
12
12
  *
13
13
  * Spec (§Table):
14
14
  * container Card p={0} withBorder · overflow hidden
15
- * header bg gray.1 · padding 12px 24px · borderBottom 1px gray.2
15
+ * header bg --rlg-head (3% of the ink: a step above any surface, in both
16
+ * themes — gray.1 sank below a card in dark mode) · padding 12px 24px
16
17
  * header cell fz 10 · fw 800 · gray.5 · uppercase · ls 1.5px
17
18
  * row padding 14px 24px · borderBottom 1px gray.2 (except the last)
18
19
  * cell fz 13 · gray.7 · lh 1.4 — the "data" role
19
- * hover background gray.1 · transition 120ms
20
+ * hover --rlg-hover (6% of the ink) · transition 120ms
20
21
  * highlight background gray.1 + 2px black hairline on the left (inset)
21
22
  * — `quietSelection` keeps only the background, for when the
22
23
  * highlight is a state of the data and not the row you clicked
@@ -80,7 +81,7 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
80
81
 
81
82
  {!stacked && (
82
83
  <Box
83
- bg="gray.1"
84
+ bg="var(--rlg-head)"
84
85
  style={{
85
86
  display: 'grid',
86
87
  gridTemplateColumns: template,
@@ -125,7 +126,7 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
125
126
  const clickable = typeof onRowClick === 'function' || hasDetail
126
127
  const activate = () => (typeof onRowClick === 'function' ? onRowClick(row) : toggle(key))
127
128
  const last = i === rows.length - 1
128
- const rest = tone === 'broken' ? 'var(--mantine-color-red-0)' : selected ? 'var(--mantine-color-gray-1)' : ''
129
+ const rest = tone === 'broken' ? 'var(--mantine-color-red-0)' : selected ? 'var(--rlg-hover)' : ''
129
130
 
130
131
  return (
131
132
  <Fragment key={key}>
@@ -146,7 +147,7 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
146
147
  }
147
148
  : undefined
148
149
  }
149
- onMouseEnter={clickable ? e => (e.currentTarget.style.backgroundColor = 'var(--mantine-color-gray-1)') : undefined}
150
+ onMouseEnter={clickable ? e => (e.currentTarget.style.backgroundColor = 'var(--rlg-hover)') : undefined}
150
151
  onMouseLeave={clickable ? e => (e.currentTarget.style.backgroundColor = rest) : undefined}
151
152
  tabIndex={clickable ? 0 : undefined}
152
153
  role={clickable ? 'button' : undefined}
@@ -193,7 +194,7 @@ export function DataTable({ columns, rows, getRowKey, onRowClick, isRowSelected,
193
194
  </Box>
194
195
  {open && (
195
196
  <Box
196
- bg="gray.1"
197
+ bg="var(--rlg-head)"
197
198
  style={{ borderBottom: last ? 'none' : 'var(--rlg-border)' }}
198
199
  >
199
200
  {detail}
@@ -24,6 +24,10 @@ function useHold(onConfirm, holdMs) {
24
24
  const [progress, setProgress] = useState(0)
25
25
  const timer = useRef(null)
26
26
  const holding = useRef(false)
27
+ // Marks a real press (mouse, finger, key) so the click that follows it is
28
+ // not mistaken for an assistive-technology activation. A ref: the button
29
+ // re-renders every tick while held, and a plain object would reset.
30
+ const pressed = useRef(false)
27
31
 
28
32
  const stop = useCallback(() => {
29
33
  if (timer.current) clearInterval(timer.current)
@@ -53,22 +57,39 @@ function useHold(onConfirm, holdMs) {
53
57
  // If the component leaves the screen mid-hold, the interval dies with it.
54
58
  useEffect(() => stop, [stop])
55
59
 
56
- return { progress, start, stop }
60
+ return { progress, start, stop, pressed }
57
61
  }
58
62
 
59
- /** Handlers shared by both variants: pointer, touch and keyboard. */
60
- function holdHandlers(start, stop, disabled) {
63
+ /**
64
+ * Handlers shared by both variants: pointer, touch and keyboard — and the
65
+ * assistive-technology path.
66
+ *
67
+ * A screen reader (VoiceOver, TalkBack) or Switch Control does not hold a key:
68
+ * it sends one synthesized `click`, with no keydown or mousedown before it.
69
+ * Until 14/10/2026 that click did nothing, so a person using one could never
70
+ * delete. Now a click that arrives WITHOUT a hold in progress asks a second
71
+ * time, in the browser's own dialog — which every assistive technology reads —
72
+ * and confirms only on yes. Apple's rule for hard-to-recover actions under
73
+ * assistive access: confirm twice. A mouse, finger or key hold never reaches
74
+ * that path, because each of them marks the press before the click.
75
+ */
76
+ function holdHandlers(start, stop, disabled, { pressed, onConfirm, confirmText }) {
61
77
  if (disabled) return {}
78
+ const press = fn => e => {
79
+ pressed.current = true
80
+ fn(e)
81
+ }
62
82
  return {
63
- onMouseDown: start,
83
+ onMouseDown: press(start),
64
84
  onMouseUp: stop,
65
85
  onMouseLeave: stop,
66
- onTouchStart: start,
86
+ onTouchStart: press(start),
67
87
  onTouchEnd: stop,
68
88
  onTouchCancel: stop,
69
89
  onKeyDown: e => {
70
90
  if (e.key === ' ' || e.key === 'Enter') {
71
91
  e.preventDefault()
92
+ pressed.current = true
72
93
  start()
73
94
  }
74
95
  },
@@ -76,6 +97,13 @@ function holdHandlers(start, stop, disabled) {
76
97
  if (e.key === ' ' || e.key === 'Enter') stop()
77
98
  },
78
99
  onBlur: stop,
100
+ onClick: () => {
101
+ if (pressed.current) {
102
+ pressed.current = false
103
+ return
104
+ }
105
+ if (typeof window !== 'undefined' && window.confirm(confirmText)) onConfirm()
106
+ },
79
107
  }
80
108
  }
81
109
 
@@ -84,7 +112,7 @@ function holdHandlers(start, stop, disabled) {
84
112
  * The ring grows over the icon itself.
85
113
  */
86
114
  export function HoldToConfirmIcon({ icon, label, onConfirm, disabled, color = 'red.6', holdMs = HOLD_MS }) {
87
- const { progress, start, stop } = useHold(onConfirm, holdMs)
115
+ const { progress, start, stop, pressed } = useHold(onConfirm, holdMs)
88
116
  const active = progress > 0
89
117
 
90
118
  return (
@@ -109,7 +137,7 @@ export function HoldToConfirmIcon({ icon, label, onConfirm, disabled, color = 'r
109
137
  color={active ? color : 'gray'}
110
138
  aria-label={label}
111
139
  disabled={disabled}
112
- {...holdHandlers(start, stop, disabled)}
140
+ {...holdHandlers(start, stop, disabled, { pressed, onConfirm, confirmText: `${label}? Não dá para desfazer.` })}
113
141
  >
114
142
  {icon}
115
143
  </ActionIcon>
@@ -123,7 +151,7 @@ export function HoldToConfirmIcon({ icon, label, onConfirm, disabled, color = 'r
123
151
  * to say what is going to happen and how much is left.
124
152
  */
125
153
  export function HoldToConfirmButton({ children, holdingLabel, onConfirm, disabled, loading, leftSection, holdMs = HOLD_MS, ...props }) {
126
- const { progress, start, stop } = useHold(onConfirm, holdMs)
154
+ const { progress, start, stop, pressed } = useHold(onConfirm, holdMs)
127
155
  const active = progress > 0
128
156
  const remaining = Math.ceil(((100 - progress) / 100) * (holdMs / 1000))
129
157
 
@@ -134,7 +162,7 @@ export function HoldToConfirmButton({ children, holdingLabel, onConfirm, disable
134
162
  disabled={disabled}
135
163
  loading={loading}
136
164
  leftSection={leftSection}
137
- {...holdHandlers(start, stop, disabled || loading)}
165
+ {...holdHandlers(start, stop, disabled || loading, { pressed, onConfirm, confirmText: `${typeof children === 'string' ? children : 'Confirmar'}? Não dá para desfazer.` })}
138
166
  {...props}
139
167
  >
140
168
  {active ? (
@@ -33,7 +33,7 @@ export function MenuItem({ icon: Icon, label, onClick }) {
33
33
  transition: 'background 120ms ease',
34
34
  }}
35
35
  onMouseEnter={e => {
36
- e.currentTarget.style.background = 'var(--mantine-color-gray-1)'
36
+ e.currentTarget.style.background = 'var(--rlg-hover)'
37
37
  }}
38
38
  onMouseLeave={e => {
39
39
  e.currentTarget.style.background = 'transparent'
@@ -50,7 +50,7 @@ export function ResourceItem({ icon: Icon, title, subtitle, tone = 'muted', mono
50
50
  maxWidth: '100%',
51
51
  }}
52
52
  onMouseEnter={e => {
53
- e.currentTarget.style.background = 'var(--mantine-color-gray-1)'
53
+ e.currentTarget.style.background = 'var(--rlg-hover)'
54
54
  }}
55
55
  onMouseLeave={e => {
56
56
  e.currentTarget.style.background = 'transparent'
@@ -1,3 +1,4 @@
1
+ import { cloneElement, isValidElement, useId } from 'react'
1
2
  import { Box, Card, Divider, Group, Text, Title } from '@mantine/core'
2
3
  import { useMediaQuery } from '@mantine/hooks'
3
4
 
@@ -61,8 +62,10 @@ export function SectionCard({ eyebrow, title, lead, action, children }) {
61
62
  {eyebrow}
62
63
  </Text>
63
64
  )}
65
+ {/* h2: it comes right under the page's h1 (PageHeader). It was h3,
66
+ which skipped a level for screen readers. The size stays. */}
64
67
  <Title
65
- order={3}
68
+ order={2}
66
69
  fz={18}
67
70
  fw={800}
68
71
  lh={1.2}
@@ -94,6 +97,18 @@ export function SectionCard({ eyebrow, title, lead, action, children }) {
94
97
  )
95
98
  }
96
99
 
100
+ /**
101
+ * The control is named by the row's label. The label is a Text next to the
102
+ * control, not a <label>, so without this a screen reader announced the input
103
+ * with no name at all (23 settings screens). A control that already has a
104
+ * label or aria-label keeps it.
105
+ */
106
+ function labelled(control, labelId) {
107
+ if (!labelId || !isValidElement(control)) return control
108
+ if (control.props['aria-label'] || control.props['aria-labelledby'] || control.props.label) return control
109
+ return cloneElement(control, { 'aria-labelledby': labelId })
110
+ }
111
+
97
112
  /**
98
113
  * One row of the block: label on the left, control on the right.
99
114
  *
@@ -102,6 +117,7 @@ export function SectionCard({ eyebrow, title, lead, action, children }) {
102
117
  * hairline would double up with the Card's frame.
103
118
  */
104
119
  export function Field({ label, help, control, last = false, align = 'center' }) {
120
+ const labelId = useId()
105
121
  /*
106
122
  * Two columns become one on phones.
107
123
  *
@@ -125,6 +141,7 @@ export function Field({ label, help, control, last = false, align = 'center' })
125
141
  >
126
142
  <Box miw={0}>
127
143
  <Text
144
+ id={labelId}
128
145
  fz={13}
129
146
  fw={700}
130
147
  c="gray.9"
@@ -145,7 +162,7 @@ export function Field({ label, help, control, last = false, align = 'center' })
145
162
  </Text>
146
163
  )}
147
164
  </Box>
148
- <Box style={{ justifySelf: side ? 'end' : 'stretch' }}>{control}</Box>
165
+ <Box style={{ justifySelf: side ? 'end' : 'stretch' }}>{labelled(control, label && labelId)}</Box>
149
166
  </Box>
150
167
  )
151
168
  }
@@ -155,6 +172,7 @@ export function Field({ label, help, control, last = false, align = 'center' })
155
172
  * doesn't fit on the right: a textarea, a dropzone, a whole table.
156
173
  */
157
174
  export function FieldStacked({ label, help, control, last = false }) {
175
+ const labelId = useId()
158
176
  return (
159
177
  <Box
160
178
  px={{ base: 16, sm: 24 }}
@@ -163,6 +181,7 @@ export function FieldStacked({ label, help, control, last = false }) {
163
181
  >
164
182
  {label && (
165
183
  <Text
184
+ id={labelId}
166
185
  fz={13}
167
186
  fw={700}
168
187
  c="gray.9"
@@ -184,7 +203,7 @@ export function FieldStacked({ label, help, control, last = false }) {
184
203
  {help}
185
204
  </Text>
186
205
  )}
187
- {control}
206
+ {labelled(control, label && labelId)}
188
207
  </Box>
189
208
  )
190
209
  }
@@ -55,7 +55,7 @@ export const notificationTheme = {
55
55
  styles: theme => ({
56
56
  root: {
57
57
  boxShadow: 'none',
58
- border: `1px solid ${theme.colors.gray[2]}`,
58
+ border: '1px solid var(--mantine-color-gray-2)',
59
59
  '--notification-radius': '0',
60
60
  },
61
61
  icon: {
@@ -79,11 +79,11 @@ export const notificationTheme = {
79
79
  fontWeight: 800,
80
80
  fontSize: '13px',
81
81
  letterSpacing: '-0.02em',
82
- color: theme.colors.gray[9],
82
+ color: 'var(--mantine-color-gray-9)',
83
83
  },
84
84
  description: {
85
85
  fontSize: '13px',
86
- color: theme.colors.gray[6],
86
+ color: 'var(--mantine-color-gray-6)',
87
87
  },
88
88
  }),
89
89
  },
package/src/theme.js CHANGED
@@ -19,6 +19,10 @@ import { notificationTheme } from './components/notify-theme'
19
19
  * same as the components-kit's; the two files must move together.
20
20
  */
21
21
  export const panelTheme = createTheme({
22
+ // Mantine's own transitions (Modal, Menu, Popover, Notification) follow
23
+ // "Reduzir movimento" too, not only ours.
24
+ respectReducedMotion: true,
25
+
22
26
  fontFamily: 'Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
23
27
  fontFamilyMonospace: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
24
28
 
@@ -272,7 +276,7 @@ export const panelTheme = createTheme({
272
276
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
273
277
  styles: (theme, props) => ({
274
278
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
275
- input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-2)` },
279
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-3)` },
276
280
  }),
277
281
  },
278
282
  /**
@@ -290,35 +294,35 @@ export const panelTheme = createTheme({
290
294
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
291
295
  styles: (theme, props) => ({
292
296
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
293
- input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-2)` },
297
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-3)` },
294
298
  }),
295
299
  },
296
300
  Textarea: {
297
301
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
298
302
  styles: (theme, props) => ({
299
303
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
300
- input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-2)` },
304
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-3)` },
301
305
  }),
302
306
  },
303
307
  Select: {
304
308
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
305
309
  styles: (theme, props) => ({
306
310
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
307
- input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-2)` },
311
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid var(--mantine-color-gray-3)` },
308
312
  }),
309
313
  },
310
314
  NumberInput: {
311
315
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
312
316
  styles: theme => ({
313
317
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
314
- input: { border: `1px solid var(--mantine-color-gray-2)` },
318
+ input: { border: `1px solid var(--mantine-color-gray-3)` },
315
319
  }),
316
320
  },
317
321
  Autocomplete: {
318
322
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
319
323
  styles: theme => ({
320
324
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
321
- input: { border: `1px solid var(--mantine-color-gray-2)` },
325
+ input: { border: `1px solid var(--mantine-color-gray-3)` },
322
326
  }),
323
327
  },
324
328
  /**
@@ -343,7 +347,7 @@ export const panelTheme = createTheme({
343
347
  styles: theme => ({
344
348
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
345
349
  input: {
346
- border: `1px solid var(--mantine-color-gray-2)`,
350
+ border: `1px solid var(--mantine-color-gray-3)`,
347
351
  minHeight: rem(36),
348
352
  display: 'flex',
349
353
  alignItems: 'center',
@@ -356,7 +360,7 @@ export const panelTheme = createTheme({
356
360
  defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
357
361
  styles: theme => ({
358
362
  label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
359
- input: { border: `1px solid var(--mantine-color-gray-2)` },
363
+ input: { border: `1px solid var(--mantine-color-gray-3)` },
360
364
  }),
361
365
  },
362
366
  SegmentedControl: {
@@ -382,7 +386,7 @@ export const panelTheme = createTheme({
382
386
  fontSize: rem(10),
383
387
  fontWeight: 700,
384
388
  color: "var(--mantine-color-gray-5)",
385
- backgroundColor: theme.white,
389
+ backgroundColor: 'var(--mantine-color-white)',
386
390
  border: `1px solid var(--mantine-color-gray-2)`,
387
391
  borderBottomColor: "var(--mantine-color-gray-2)",
388
392
  boxShadow: 'none',
package/src/tokens.css CHANGED
@@ -90,23 +90,41 @@
90
90
  * runtime, after this sheet.
91
91
  */
92
92
  @media (prefers-contrast: more) {
93
+ /* Contraste aumentado: a rampa inteira desloca e continua em ORDEM — cada
94
+ degrau mais forte que o anterior —, e a borda passa de 3:1 sobre a base,
95
+ o cartão e o menu. Vermelho e verde ganham a sua variante. Até
96
+ 14/10/2026 o corpo ficava mais forte que o dado e a borda seguia abaixo
97
+ de 3:1. */
93
98
  :root:root:root[data-mantine-color-scheme='dark'] {
94
- /* No escuro o deslocamento é para o claro: o filete aparece, o label
95
- e a legenda sobem um degrau. */
96
- --mantine-color-gray-2: #3a444d;
97
- --mantine-color-gray-3: #4b5563;
99
+ --mantine-color-gray-2: #6b7582;
100
+ --mantine-color-gray-3: #7b8591;
98
101
  --mantine-color-gray-4: #b6bec8;
99
- --mantine-color-gray-5: #d3d8de;
100
- --mantine-color-gray-6: #e5e7eb;
102
+ --mantine-color-gray-5: #c4cad2;
103
+ --mantine-color-gray-6: #d3d8de;
104
+ --mantine-color-gray-7: #e5e7eb;
105
+ --mantine-color-gray-8: #f3f4f6;
106
+ --mantine-color-gray-9: #ffffff;
107
+ --mantine-color-red-6: #ffa8a8;
108
+ --mantine-color-red-7: #ffc9c9;
109
+ --mantine-color-red-filled: #ffa8a8;
110
+ --mantine-color-green-6: #4ade80;
111
+ --mantine-color-green-7: #86efac;
101
112
  }
102
113
 
103
114
  :root:root[data-mantine-color-scheme='light'],
104
115
  :root:root:not([data-mantine-color-scheme]) {
105
- --mantine-color-gray-2: #9ca3af;
106
- --mantine-color-gray-3: #9ca3af;
116
+ --mantine-color-gray-2: #878e99;
117
+ --mantine-color-gray-3: #7b838e;
107
118
  --mantine-color-gray-4: #4b5563;
108
- --mantine-color-gray-5: #374151;
119
+ --mantine-color-gray-5: #414a57;
109
120
  --mantine-color-gray-6: #374151;
121
+ --mantine-color-gray-7: #252d38;
122
+ --mantine-color-gray-8: #1a222b;
123
+ --mantine-color-gray-9: #0b0f12;
124
+ --mantine-color-red-6: #c92a2a;
125
+ --mantine-color-red-7: #a61e1e;
126
+ --mantine-color-green-6: #15803d;
127
+ --mantine-color-green-7: #166534;
110
128
  }
111
129
  }
112
130
 
@@ -166,7 +184,7 @@
166
184
  --mantine-color-gray-1: #12181d; /* palco alternado, hover */
167
185
  --mantine-color-gray-2: #222a31; /* filete */
168
186
  --mantine-color-gray-3: #2f3840; /* borda forte, desabilitado */
169
- --mantine-color-gray-4: #7d8794; /* label caixa alta — 5,3:1 */
187
+ --mantine-color-gray-4: #8a94a0; /* label caixa alta — 6,3:1 na base, 4,7:1 no menu */
170
188
  --mantine-color-gray-5: #98a1ac; /* legenda — 7,4:1 */
171
189
  --mantine-color-gray-6: #b6bec8; /* corpo — 10,3:1 */
172
190
  --mantine-color-gray-7: #d3d8de; /* dado — 13,4:1 */
@@ -233,3 +251,58 @@
233
251
  :root[data-mantine-color-scheme='dark'] img[data-invert-dark]:not([data-keep-colors]) {
234
252
  filter: invert(1) hue-rotate(180deg);
235
253
  }
254
+
255
+ /* ===========================================================================
256
+ * Revisão de 14/10/2026 contra as diretrizes da Apple (16-apple.md).
257
+ * =========================================================================== */
258
+
259
+ /* O realce de linha e de item funciona em QUALQUER superfície. Era gray.1 —
260
+ no escuro, um degrau da base: dentro de um cartão ou de um menu, o item
261
+ destacado afundava abaixo da própria superfície. Uma tinta de 6% do preto
262
+ do sistema escurece no claro e clareia no escuro, sobre o que estiver
263
+ embaixo. */
264
+ :root {
265
+ --rlg-hover: color-mix(in srgb, var(--mantine-color-gray-9) 6%, transparent);
266
+ --rlg-head: color-mix(in srgb, var(--mantine-color-gray-9) 3%, transparent);
267
+ }
268
+
269
+ /* Alvo de toque também em notebook com tela de toque (que se declara
270
+ "ponteiro fino") e em mais controles: item de navegação, caixa de seleção,
271
+ chave, abas, paginação, menu. */
272
+ @media (any-pointer: coarse) {
273
+ .mantine-Button-root,
274
+ .mantine-ActionIcon-root,
275
+ .mantine-CloseButton-root,
276
+ .mantine-Burger-root,
277
+ .mantine-NavLink-root,
278
+ .mantine-Tabs-tab,
279
+ .mantine-Pagination-control,
280
+ .mantine-Menu-item,
281
+ .mantine-SegmentedControl-control {
282
+ min-height: 44px;
283
+ }
284
+ .mantine-ActionIcon-root,
285
+ .mantine-CloseButton-root,
286
+ .mantine-Burger-root,
287
+ .mantine-Pagination-control {
288
+ min-width: 44px;
289
+ }
290
+ /* O selo sobe para 11px no toque: 10px fica abaixo do mínimo da Apple
291
+ para texto em tela de celular. */
292
+ [data-tag] {
293
+ font-size: 11px !important;
294
+ }
295
+ }
296
+
297
+ /* Menos movimento, sem perder o esmaecimento. A Apple pede que deslocamento
298
+ vire esmaecimento, não que tudo pare: animações em laço param, a rolagem
299
+ deixa de ser suave, e o que é cor e opacidade continua. */
300
+ @media (prefers-reduced-motion: reduce) {
301
+ *,
302
+ *::before,
303
+ *::after {
304
+ animation-iteration-count: 1 !important;
305
+ animation-duration: 0.01ms !important;
306
+ scroll-behavior: auto !important;
307
+ }
308
+ }