@ciromaciel/dashboard-kit 1.5.0 → 1.7.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.5.0",
3
+ "version": "1.7.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.",
@@ -0,0 +1,56 @@
1
+ import { ActionIcon, Tooltip, useMantineColorScheme } from '@mantine/core'
2
+ import { useMounted } from '@mantine/hooks'
3
+ import { IconDeviceDesktop, IconMoon, IconSun } from '@tabler/icons-react'
4
+
5
+ /**
6
+ * The theme button — one icon that cycles Automático → Claro → Escuro
7
+ * (decided 14/10/2026).
8
+ *
9
+ * Automático follows the device and is the default for anyone who never chose.
10
+ * A single button hides its state in the icon, so the state is said in words
11
+ * twice: the tooltip and the accessible name both read "Tema: Claro. Mudar
12
+ * para Escuro." — what it is now, and what one click does.
13
+ *
14
+ * The icon waits for the mount. Mantine reads the stored choice from
15
+ * localStorage on the client only; a prerendered page would otherwise paint the
16
+ * server's icon and swap it on hydration.
17
+ *
18
+ * The same component lives in the components-kit for the sites; the two must
19
+ * move together.
20
+ */
21
+ const NEXT = { auto: 'light', light: 'dark', dark: 'auto' }
22
+ const NAME = { auto: 'Automático', light: 'Claro', dark: 'Escuro' }
23
+ const ICON = { auto: IconDeviceDesktop, light: IconSun, dark: IconMoon }
24
+
25
+ export function ColorSchemeToggle({ size = 36, ...props }) {
26
+ const { colorScheme, setColorScheme } = useMantineColorScheme()
27
+ const isMounted = useMounted()
28
+ const current = isMounted ? colorScheme : 'auto'
29
+ const Icon = ICON[current]
30
+ const label = `Tema: ${NAME[current]}. Mudar para ${NAME[NEXT[current]]}.`
31
+
32
+ return (
33
+ <Tooltip
34
+ label={label}
35
+ position="top"
36
+ withArrow={false}
37
+ >
38
+ <ActionIcon
39
+ data-color-scheme-toggle
40
+ variant="subtle"
41
+ color="gray.9"
42
+ size={size}
43
+ radius={0}
44
+ aria-label={label}
45
+ onClick={() => setColorScheme(NEXT[current])}
46
+ {...props}
47
+ >
48
+ <Icon
49
+ size={18}
50
+ stroke={1.5}
51
+ aria-hidden
52
+ />
53
+ </ActionIcon>
54
+ </Tooltip>
55
+ )
56
+ }
@@ -1,9 +1,10 @@
1
1
  import { ActionIcon, Box, Group, ScrollArea, Stack, Text, TextInput, Tooltip } from '@mantine/core'
2
- import { IconSearch, IconX } from '@tabler/icons-react'
2
+ import { IconPlus, IconSearch, IconX } from '@tabler/icons-react'
3
3
 
4
4
  import { GroupLabel } from './GroupLabel'
5
5
  import { ListSkeleton } from './ListSkeleton'
6
6
  import { NavItem } from './NavItem'
7
+ import { ColorSchemeToggle } from './ColorSchemeToggle'
7
8
 
8
9
  /**
9
10
  * A coluna lateral de um painel, por CONFIGURAÇÃO em vez de por cópia.
@@ -39,8 +40,13 @@ import { NavItem } from './NavItem'
39
40
  * Nada de quota entra aqui. O painel resolve o gate e passa `createAllowed`
40
41
  * booleano — a coluna não sabe o que é um plano.
41
42
  */
42
- /** O botão de criar na linha da busca: quadrado de 36px, preto; em contorno cinza no limite do plano. */
43
- function CreateSquare({ icon: Icon, label, isAllowed, onClick }) {
43
+ /**
44
+ * O botão de criar na linha da busca: quadrado de 36px, preto, colado à busca;
45
+ * em contorno cinza quando o plano chegou ao limite. É a forma única do "+" em
46
+ * todo painel — exportado como `CreateButton` para as colunas que ainda não
47
+ * são o SidebarShell. Nunca um item de lista "+ Novo …" abaixo da busca.
48
+ */
49
+ export function CreateButton({ icon: Icon = IconPlus, label, isAllowed = true, onClick }) {
44
50
  return (
45
51
  <Tooltip
46
52
  label={label}
@@ -196,7 +202,7 @@ export function SidebarShell({
196
202
  }
197
203
  styles={{ input: { height: 36, minHeight: 36, fontSize: 13, background: 'var(--mantine-color-gray-1)' } }}
198
204
  />
199
- {onCreate && <CreateSquare {...criar} />}
205
+ {onCreate && <CreateButton {...criar} />}
200
206
  </Group>
201
207
  )}
202
208
 
@@ -301,15 +307,21 @@ export function SidebarShell({
301
307
  menu dele, junto de Conta e Assinatura. Na coluna, competiriam com
302
308
  a lista — que é o assunto da coluna.
303
309
  */}
304
- {footer && (
305
- <Box
306
- pt={16}
307
- w="100%"
308
- style={{ borderTop: 'var(--rlg-border)' }}
309
- >
310
- {footer}
311
- </Box>
312
- )}
310
+ {/*
311
+ O botão de tema mora ao lado do cartão: é preferência da pessoa,
312
+ como Conta, e não compete com a lista. Sem cartão, ele fica só.
313
+ */}
314
+ <Group
315
+ pt={16}
316
+ w="100%"
317
+ gap={8}
318
+ wrap="nowrap"
319
+ align="center"
320
+ style={{ borderTop: 'var(--rlg-border)' }}
321
+ >
322
+ {footer && <Box style={{ flex: 1, minWidth: 0 }}>{footer}</Box>}
323
+ <ColorSchemeToggle style={footer ? undefined : { marginLeft: 'auto' }} />
324
+ </Group>
313
325
 
314
326
  {children}
315
327
  </Stack>
@@ -81,14 +81,13 @@ export function useHealthCheck(enabled, apiUrl) {
81
81
  /**
82
82
  * The status light.
83
83
  *
84
- * `--rlg-status-up` is the system's only green, and it exists because this is
85
- * NOT the success of an action (that is gray.9 + IconCheck): it is the measured
86
- * state of an external system, and it never appears without the word next to
87
- * it.
84
+ * Green is the system's success colour (`--rlg-success`), the same one the
85
+ * notification and the status bars use. It never appears without the word
86
+ * next to it.
88
87
  */
89
88
  export function HealthRow({ health, host }) {
90
89
  const label = health.status === 'up' ? 'No ar' : health.status === 'down' ? 'Sem resposta' : 'Verificando…'
91
- const color = health.status === 'up' ? 'var(--rlg-status-up)' : health.status === 'down' ? 'var(--mantine-color-red-6)' : 'var(--mantine-color-gray-4)'
90
+ const color = health.status === 'up' ? 'var(--rlg-success)' : health.status === 'down' ? 'var(--mantine-color-red-6)' : 'var(--mantine-color-gray-4)'
92
91
 
93
92
  return (
94
93
  <Group
@@ -17,13 +17,18 @@ import { IconCopy, IconCheck } from '@tabler/icons-react'
17
17
  * usa em FeaturesGrid layout="split"), para que painel e site mostrem código
18
18
  * com a mesma cara. Os valores vieram de lá, não foram escolhidos aqui.
19
19
  *
20
- * O fundo é gray.9 — a única superfície escura do sistema. Ela se justifica
20
+ * O fundo é #11181C — a única superfície escura do sistema. Ela se justifica
21
21
  * porque código é conteúdo citado, não interface: o contraste invertido marca
22
22
  * "isto é a máquina falando", e é o mesmo tratamento de qualquer terminal que
23
23
  * o desenvolvedor já tem aberto ao lado.
24
+ *
25
+ * É LITERAL, não `var(--mantine-color-gray-9)`, de propósito: o terminal é
26
+ * escuro nos dois temas. No modo escuro o gray.9 vira claro, e o texto branco
27
+ * daqui ficaria branco sobre branco. A borda é `--rlg-shell-edge`, que no
28
+ * escuro clareia para o terminal não se fundir com a página.
24
29
  */
25
30
  const DARK = {
26
- shell: 'var(--mantine-color-gray-9)',
31
+ shell: '#11181C',
27
32
  bar: 'rgba(255, 255, 255, 0.06)',
28
33
  /*
29
34
  * O token vem de `tokens.css` do kit, com fallback ao literal que as
@@ -51,7 +56,7 @@ export function Terminal({ children, label, copy = true, actions, style }) {
51
56
  return (
52
57
  <Box
53
58
  style={{
54
- border: '1px solid var(--mantine-color-gray-9)',
59
+ border: '1px solid var(--rlg-shell-edge, #11181C)',
55
60
  background: DARK.shell,
56
61
  overflow: 'hidden',
57
62
  ...style,
@@ -168,7 +173,7 @@ export function TerminalOutput({ children, label = 'output', tone = 'default' })
168
173
  return (
169
174
  <Box
170
175
  style={{
171
- border: '1px solid var(--mantine-color-gray-9)',
176
+ border: '1px solid var(--rlg-shell-edge, #11181C)',
172
177
  borderTop: 'none',
173
178
  background: DARK.shell,
174
179
  overflow: 'hidden',
@@ -10,7 +10,7 @@ import { createElement } from 'react'
10
10
  * isn't to invent a color: it's to use the two dimensions that remain, color AND
11
11
  * glyph.
12
12
  *
13
- * success teal + IconCheck (a confirmation mark)
13
+ * success green.6 + IconCheck (a confirmation mark)
14
14
  * error red + IconAlertCircle (the system's chromatic exception)
15
15
  * info gray + IconInfoCircle (neutral, asks for no action)
16
16
  *
@@ -63,7 +63,7 @@ const STICKY = { autoClose: false }
63
63
 
64
64
  export const notify = {
65
65
  success(title, message) {
66
- show('teal', icon(IconCheck), title, message)
66
+ show('green.6', icon(IconCheck), title, message)
67
67
  },
68
68
 
69
69
  error(title, message) {
package/src/index.js CHANGED
@@ -64,6 +64,7 @@ export { DangerZone } from './components/DangerZone'
64
64
  export { ZenLabel } from './components/ZenLabel'
65
65
  export { notify } from './components/notify'
66
66
  export { NOTIFICATIONS_PROPS, NOTIFY_DESCRIPTION_CSS, notificationTheme } from './components/notify-theme'
67
+ export { panelTheme } from './theme'
67
68
  export { Wordmark } from './components/Wordmark'
68
69
  export { SupportModal, HealthRow, useHealthCheck } from './components/SupportModal'
69
70
  export { AuthLoader } from './components/AuthLoader'
@@ -83,7 +84,8 @@ export { ActiveResource } from './components/ActiveResource'
83
84
  export { ColumnBrand } from './components/ColumnBrand'
84
85
  export { ListSkeleton } from './components/ListSkeleton'
85
86
  export { DashboardFrame, SIDEBAR_WIDTH } from './components/DashboardFrame'
86
- export { SidebarShell } from './components/SidebarShell'
87
+ export { SidebarShell, CreateButton } from './components/SidebarShell'
88
+ export { ColorSchemeToggle } from './components/ColorSchemeToggle'
87
89
  export { AUTH_LABELS, AUTH_TEXTS } from './auth-labels'
88
90
  export { CardSkeleton } from './components/CardSkeleton'
89
91
  export { MoreResults } from './components/MoreResults'
package/src/theme.js ADDED
@@ -0,0 +1,448 @@
1
+ import { createTheme, rem } from '@mantine/core'
2
+ import { notificationTheme } from './components/notify-theme'
3
+
4
+ /**
5
+ * The panel theme — one for every dashboard (14/10/2026).
6
+ *
7
+ * Until then each panel carried its own `theme.js`: eighteen files, seventeen of
8
+ * them different, copied from one another at different moments. None held a
9
+ * real exception — the differences were versions that stopped evolving. They
10
+ * all agreed on radius and font and diverged on the rest: buttons in two case
11
+ * styles, h2 bigger than h1, inputs at three sizes.
12
+ *
13
+ * A panel now does `export { panelTheme as theme } from '@ciromaciel/dashboard-kit'`,
14
+ * and an exception that is truly the product's own goes through
15
+ * `mergeThemeOverrides` with a comment saying why.
16
+ *
17
+ * The values come from the Zen design system (ciromaciel.click/design-system),
18
+ * at the PANEL density: data at 12-14px, screen title at 36px. The ramp is the
19
+ * same as the components-kit's; the two files must move together.
20
+ */
21
+ export const panelTheme = createTheme({
22
+ fontFamily: 'Montserrat, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
23
+ fontFamilyMonospace: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
24
+
25
+ /**
26
+ * Radius 0 is an invariant, not a preference. A rounded corner is pure
27
+ * chrome — it communicates nothing, it only softens, and softening is the
28
+ * opposite of what massive hierarchy needs.
29
+ */
30
+ defaultRadius: 0,
31
+
32
+ /**
33
+ * h1 is 36px because in a panel the title shares the line with the actions
34
+ * and the width is controlled — it does not need to scale like a landing
35
+ * page's.
36
+ *
37
+ * Allowed tracking: -0.04em (display), 1px (control), 1.5px and 2px (label).
38
+ * Display needs to regroup; a label needs to breathe.
39
+ */
40
+ /**
41
+ * A ladder, each step clearly below the one above. Until 14/10/2026 every
42
+ * panel shipped h2 at 48px and h3 at 32px — both BIGGER than the 36px h1,
43
+ * the opposite of Law 2. It did not show because 29 of the 30 `order={2}`
44
+ * calls passed `fz` by hand; the one that trusted the theme got 48px.
45
+ */
46
+ headings: {
47
+ fontFamily: 'Montserrat, sans-serif',
48
+ fontWeight: '900',
49
+ sizes: {
50
+ h1: { fontSize: rem(36), lineHeight: '1', letterSpacing: '-0.04em' },
51
+ h2: { fontSize: rem(24), lineHeight: '1.1', letterSpacing: '-0.04em' },
52
+ h3: { fontSize: rem(18), lineHeight: '1.2', letterSpacing: '-0.02em', fontWeight: '800' },
53
+ h4: { fontSize: rem(15), lineHeight: '1.3', letterSpacing: '-0.02em', fontWeight: '800' },
54
+ h5: { fontSize: rem(13), lineHeight: '1.3', letterSpacing: '-0.02em', fontWeight: '800' },
55
+ },
56
+ },
57
+
58
+ /**
59
+ * Mantine's `--mantine-color-text` is `theme.black`, and no panel set it:
60
+ * every input, table cell and unstyled text came out #000, a black the ramp
61
+ * does not contain. gray.9 is the system's black.
62
+ */
63
+ black: '#11181C',
64
+
65
+ colors: {
66
+ /**
67
+ * The ramp. Steps 0-3 are surface and structure; 4-9 are ink. None of
68
+ * them crosses the boundary.
69
+ *
70
+ * gray.9 (#11181C) is not #000: pure black on a large screen vibrates and
71
+ * tires the eye. gray.4 fails AA for body text (2.6:1) — which is why it
72
+ * only lives in uppercase labels.
73
+ */
74
+ gray: [
75
+ '#FFFFFF', // 0 · App base
76
+ '#F9FAFB', // 1 · Alternate surface — table header, hover
77
+ '#F3F4F6', // 2 · Default hairline — EVERY border in the system
78
+ '#E5E7EB', // 3 · Mute border — quotation, disabled
79
+ '#9CA3AF', // 4 · Uppercase label ONLY
80
+ '#6B7280', // 5 · Labels, captions
81
+ '#4B5563', // 6 · Body — the most used color
82
+ '#374151', // 7 · Emphatic body, tabular data
83
+ '#1F2937', // 8 · Rare
84
+ '#11181C', // 9 · RiLiGar black — titles, metrics, filled button
85
+ ],
86
+
87
+ /**
88
+ * The system's chromatic exception. The semantics of LOSS and of error —
89
+ * never decoration, never the same color as success (Law 5).
90
+ *
91
+ * Success is green (below). The distinction is colour AND glyph,
92
+ * legible without reading the text.
93
+ */
94
+ /**
95
+ * Success — the measured "it worked" (14/10/2026). Green, not teal: it
96
+ * is the green "No ar" already used in every panel, and it separates
97
+ * from red better for red-green colour blindness. green.6 fills (dot,
98
+ * disc, bar); green.7 is the ink for text on white (5.0:1). Like red,
99
+ * it never appears without the word or the glyph beside it.
100
+ */
101
+ /* A rampa que o Mantine usa para as próprias superfícies no escuro
102
+ (corpo, campo, botão default, texto apagado). São os mesmos valores
103
+ do bloco escuro do CSS: dark.7 é a base, dark.6 a superfície
104
+ elevada, dark.4 a borda, dark.0–3 as tintas. */
105
+ dark: ['#D3D8DE', '#B6BEC8', '#98A1AC', '#7D8794', '#2F3840', '#2A333B', '#1A2127', '#0B0F12', '#080B0D', '#050708'],
106
+ green: ['#F0FDF4', '#DCFCE7', '#BBF7D0', '#86EFAC', '#4ADE80', '#22C55E', '#16A34A', '#15803D', '#166534', '#14532D'],
107
+
108
+ red: ['#FFF5F5', '#FFE3E3', '#FFC9C9', '#FFA8A8', '#FF8787', '#FF6B6B', '#E03131', '#C92A2A', '#B02525', '#962020'],
109
+ },
110
+
111
+ primaryColor: 'gray',
112
+ primaryShade: 9,
113
+ autoContrast: true,
114
+
115
+ // Zen: shadows exist only for real elevation (Modal, Popover, Menu).
116
+ // Surfaces use a 1px gray.2 border — never a shadow.
117
+ shadows: {
118
+ xs: 'none',
119
+ sm: 'none',
120
+ md: '0 4px 20px rgba(0,0,0,0.05)',
121
+ lg: '0 10px 30px rgba(0,0,0,0.06)',
122
+ xl: '0 20px 40px rgba(0,0,0,0.08)',
123
+ },
124
+
125
+ other: {
126
+ // Small Caps Label — the "announcement" of the canonical pair.
127
+ zenLabel: { fz: 11, fw: 800, tt: 'uppercase', lts: '1.5px', c: 'gray.4' },
128
+ // Panel geometry (spec §Panel geometry).
129
+ panel: { sidebar: 300, sidebarCollapsed: 64, px: 48, pt: 40, pb: 80, maw: 1280, headerMb: 32, blockGap: 20 },
130
+ // Destructive action through persistence, not through a dialog.
131
+ hold: { holdMs: 3000, tickMs: 50 },
132
+ },
133
+
134
+ components: {
135
+ // --- SURFACES ---
136
+ /**
137
+ * Depth comes from surface contrast and from the 1px hairline — never
138
+ * from a shadow.
139
+ *
140
+ * Paper = a declared surface (the block needs to stand out from the
141
+ * stage).
142
+ * Card = an invisible grouping (the space is already enough). When in
143
+ * doubt, Card.
144
+ */
145
+ Paper: {
146
+ defaultProps: { withBorder: true, shadow: 'none', radius: '0' },
147
+ vars: () => ({ root: { '--paper-border-color': 'var(--mantine-color-gray-2)' } }),
148
+ },
149
+ Card: {
150
+ defaultProps: { withBorder: true, shadow: 'none', padding: 'lg' },
151
+ vars: () => ({ root: { '--paper-border-color': 'var(--mantine-color-gray-2)' } }),
152
+ },
153
+ Modal: {
154
+ defaultProps: {
155
+ centered: true,
156
+ size: 'md',
157
+ padding: 'xl',
158
+ radius: 0,
159
+ overlayProps: { backgroundOpacity: 0.45 }, // no blur: glassmorphism is chrome
160
+ },
161
+ styles: {
162
+ title: { fontWeight: 800, fontSize: rem(16), letterSpacing: '-0.04em' },
163
+ },
164
+ },
165
+ Menu: {
166
+ defaultProps: { shadow: 'none', width: 210, withinPortal: true, radius: 0 },
167
+ styles: theme => ({
168
+ dropdown: { border: `1px solid ${theme.colors.gray[2]}` },
169
+ item: { fontSize: rem(13), fontWeight: 500 },
170
+ label: { fontSize: rem(10), fontWeight: 800, textTransform: 'uppercase', letterSpacing: '1.5px' },
171
+ }),
172
+ },
173
+ Popover: {
174
+ defaultProps: { shadow: 'none', radius: 0 },
175
+ styles: theme => ({ dropdown: { border: `1px solid ${theme.colors.gray[2]}` } }),
176
+ },
177
+
178
+ // --- DATA ---
179
+ /**
180
+ * The "data" role: 12-14px, gray.7. A 16px paragraph is comfortable; a
181
+ * 200-row table at 16px is impossible to scan.
182
+ *
183
+ * The app's main table is CSS Grid (see components/zen/DataTable) — fr
184
+ * for content, fixed px for actions, the whole row clickable. These
185
+ * defaults apply to the simple tables that are left.
186
+ */
187
+ Table: {
188
+ defaultProps: { verticalSpacing: 'sm', horizontalSpacing: 'md', withRowBorders: true },
189
+ styles: theme => ({
190
+ th: {
191
+ fontSize: rem(10),
192
+ fontWeight: 800,
193
+ textTransform: 'uppercase',
194
+ letterSpacing: '1.5px',
195
+ color: theme.colors.gray[5],
196
+ borderBottom: `1px solid ${theme.colors.gray[2]}`,
197
+ whiteSpace: 'nowrap',
198
+ },
199
+ td: { borderBottom: `1px solid ${theme.colors.gray[2]}`, fontSize: rem(13), color: theme.colors.gray[7] },
200
+ }),
201
+ },
202
+ Badge: {
203
+ defaultProps: { size: 'sm', variant: 'outline', color: 'gray', fw: 800, radius: 0 },
204
+ styles: { label: { textTransform: 'uppercase', letterSpacing: '1.5px', fontSize: rem(10) } },
205
+ },
206
+ Code: {
207
+ styles: { root: { fontSize: rem(12) } },
208
+ },
209
+ Skeleton: {
210
+ // The Skeleton reproduces the content's real grid — and square
211
+ // corners, like everything else.
212
+ defaultProps: { radius: 0 },
213
+ },
214
+
215
+ // --- CONTROLS ---
216
+ /**
217
+ * Hierarchy without color — filled (primary) → outline (secondary) →
218
+ * subtle (tertiary).
219
+ */
220
+ /*
221
+ * This panel follows the design system here where the theme inherited from
222
+ * Analytics did not: a button is a LABEL in a rectangle — uppercase,
223
+ * 800, 1px tracking (02-tipografia), as the prototype draws it.
224
+ */
225
+ Button: {
226
+ defaultProps: { size: 'sm', radius: '0' },
227
+ styles: { root: { textTransform: 'uppercase', fontWeight: 800, letterSpacing: '1px', fontSize: rem(12) } },
228
+ },
229
+ /**
230
+ * `loaderProps` exists because `bars` and `oval` do not occupy the same
231
+ * space with the same number. The ActionIcon sizes its loader at
232
+ * `calc(var(--ai-size) * 0.55)` — ~15px at the `md` used here. For the
233
+ * `oval` that is a thin-stroked ring; for the `bars` it is a FULL 15px
234
+ * block, three bars painted from end to end.
235
+ *
236
+ * Next to 16px glyphs with `stroke={1.5}`, the two numbers are equal and
237
+ * the two visual masses are not: the loader became the heaviest element
238
+ * of the row, and the whole row seemed to flicker in size when an audit
239
+ * started.
240
+ *
241
+ * 12px gives the loader back the mass of an outline icon. Here and not
242
+ * in the screen because the mismatch comes from the geometry of `bars`
243
+ * with the `ActionIcon` — it applies to every icon button in the panel,
244
+ * and defined case by case it diverges again on the next one.
245
+ */
246
+ ActionIcon: {
247
+ defaultProps: { size: 'md', variant: 'subtle', color: 'gray', radius: 0, loaderProps: { size: 12 } },
248
+ },
249
+ /**
250
+ * The input's frame is the gray.2 hairline at rest and gray.9 on focus.
251
+ * The `unstyled` variant (used on the stage) is left out: there only the
252
+ * bottom border exists.
253
+ *
254
+ * THE DESCRIPTION GOES BELOW THE FIELD, not between the label and it.
255
+ *
256
+ * Mantine's default order (label → description → input) misaligns any
257
+ * row of side-by-side fields: a two-line description pushes its own
258
+ * input 34px down, and the neighbor without a description is left
259
+ * hanging at the top. That is what happened with "Cor de marca / Fonte /
260
+ * Tema" on the status page screen — three inputs at three heights.
261
+ *
262
+ * Below the field, the top of every single-line input coincides, and the
263
+ * alignment stops depending on how many lines each text takes up. The
264
+ * reading also improves: the label says what it is, the field shows the
265
+ * value, and the note explains — in the order in which one reads and
266
+ * fills it in.
267
+ *
268
+ * It applies to the whole panel, not field by field: defined here, it is
269
+ * invariant; done in the screen, it diverges again on the next form.
270
+ */
271
+ TextInput: {
272
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
273
+ styles: (theme, props) => ({
274
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
275
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
276
+ }),
277
+ },
278
+ /**
279
+ * The PasswordInput was left out when this block was written, and so it
280
+ * fell back to Mantine's default (`size: "md"`): on the sign-in screen,
281
+ * the password field was visibly larger than the e-mail one right above
282
+ * it — font, height and rounded corners, all different.
283
+ *
284
+ * It is not a simple input: it carries the eye button
285
+ * (`visibilityToggle`) and that is why it has its own component in the
286
+ * theme. Inheriting from `TextInput` does not happen — in Mantine each
287
+ * component reads its own key.
288
+ */
289
+ PasswordInput: {
290
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
291
+ styles: (theme, props) => ({
292
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
293
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
294
+ }),
295
+ },
296
+ Textarea: {
297
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
298
+ styles: (theme, props) => ({
299
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
300
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
301
+ }),
302
+ },
303
+ Select: {
304
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
305
+ styles: (theme, props) => ({
306
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
307
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
308
+ }),
309
+ },
310
+ NumberInput: {
311
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
312
+ styles: theme => ({
313
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
314
+ input: { border: `1px solid ${theme.colors.gray[2]}` },
315
+ }),
316
+ },
317
+ Autocomplete: {
318
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
319
+ styles: theme => ({
320
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
321
+ input: { border: `1px solid ${theme.colors.gray[2]}` },
322
+ }),
323
+ },
324
+ /**
325
+ * The TagsInput is a PILL BOX, not a simple input — and that is why it
326
+ * misaligned with the TextInput next to it.
327
+ *
328
+ * Mantine sizes that box by its content: empty, it measured 22px against
329
+ * the TextInput's 36px, and it also got larger internal breathing room.
330
+ * On a "Nome | Tags" row the two fields started with 6px of difference —
331
+ * too little to name, enough to make the row look crooked.
332
+ *
333
+ * The target of `minHeight` is the FRAME (`.mantine-PillsInput-root`),
334
+ * not the `input`: in this component `input` names the typing field
335
+ * INSIDE the pill box, and sizing it does not move the frame. That is
336
+ * what made the first attempt fix nothing.
337
+ *
338
+ * The box keeps growing as the tags come in, which is the right behavior
339
+ * — the minimum only guarantees the alignment at rest.
340
+ */
341
+ TagsInput: {
342
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
343
+ styles: theme => ({
344
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
345
+ input: {
346
+ border: `1px solid ${theme.colors.gray[2]}`,
347
+ minHeight: rem(36),
348
+ display: 'flex',
349
+ alignItems: 'center',
350
+ paddingTop: 0,
351
+ paddingBottom: 0,
352
+ },
353
+ }),
354
+ },
355
+ ColorInput: {
356
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
357
+ styles: theme => ({
358
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
359
+ input: { border: `1px solid ${theme.colors.gray[2]}` },
360
+ }),
361
+ },
362
+ SegmentedControl: {
363
+ defaultProps: { size: 'xs', radius: 0, color: 'gray.9' },
364
+ styles: { label: { fontWeight: 700, fontSize: rem(12) } },
365
+ },
366
+ Switch: {
367
+ defaultProps: { size: 'sm', color: 'gray.9', radius: 0 },
368
+ styles: { label: { fontSize: rem(13), fontWeight: 500 } },
369
+ },
370
+ Checkbox: {
371
+ defaultProps: { size: 'sm', radius: 0, color: 'gray.9' },
372
+ },
373
+ // The prototype's tabs: 11px labels, uppercase, 800, 1px tracking.
374
+ Tabs: {
375
+ styles: { tab: { fontSize: rem(11), fontWeight: 800, textTransform: 'uppercase', letterSpacing: '1px', borderRadius: 0 } },
376
+ },
377
+ Kbd: {
378
+ defaultProps: { size: 'xs' },
379
+ styles: theme => ({
380
+ root: {
381
+ fontFamily: 'var(--mantine-font-family-monospace)',
382
+ fontSize: rem(10),
383
+ fontWeight: 700,
384
+ color: theme.colors.gray[5],
385
+ backgroundColor: theme.white,
386
+ border: `1px solid ${theme.colors.gray[2]}`,
387
+ borderBottomColor: theme.colors.gray[2],
388
+ boxShadow: 'none',
389
+ borderRadius: 0,
390
+ },
391
+ }),
392
+ },
393
+ ThemeIcon: {
394
+ // The ThemeIcon is always square, never a circle.
395
+ defaultProps: { size: 'lg', variant: 'light', color: 'gray', radius: 0 },
396
+ },
397
+ Avatar: {
398
+ // The system's portrait is square, like the ThemeIcon above.
399
+ defaultProps: { radius: 0, color: 'gray' },
400
+ },
401
+ Alert: {
402
+ defaultProps: { variant: 'light', color: 'gray', radius: 0 },
403
+ styles: { title: { fontWeight: 800, fontSize: rem(12) }, message: { fontSize: rem(13) } },
404
+ },
405
+ Tooltip: {
406
+ defaultProps: { fz: 'xs', withArrow: false, openDelay: 300, color: 'gray.9', radius: 0 },
407
+ },
408
+ Progress: {
409
+ defaultProps: { size: 'sm', color: 'gray.9', radius: 0 },
410
+ },
411
+ /**
412
+ * `bars` as the system default, and not `oval`.
413
+ *
414
+ * This `type` applies to EVERY loader in the panel — including the one
415
+ * that Button and ActionIcon build on their own when they receive
416
+ * `loading`. And that is exactly where the `oval` failed: a ring
417
+ * spinning inside a 16px button is indistinguishable from a still ring,
418
+ * and the defect showed up side by side on the same table row — the
419
+ * status indicator (which already used `bars`) moving, and the one on the
420
+ * button next to it not.
421
+ *
422
+ * The three bars change HEIGHT at different times: the silhouette
423
+ * changes on every frame, so the movement is legible even at 10px and
424
+ * even at a glance. It is the difference between an equalizer and a
425
+ * spinning disc — both spin, only one communicates.
426
+ */
427
+ Loader: {
428
+ defaultProps: { color: 'gray', type: 'bars' },
429
+ },
430
+ // The notification comes from the kit: one shape across the apps and the SDK.
431
+ ...notificationTheme,
432
+
433
+ // --- TYPOGRAPHY ---
434
+ Title: { defaultProps: { lh: 1, c: 'gray.9' } },
435
+ /**
436
+ * A Text with no colour inherits — and what it inherits from the page is
437
+ * body gray.6 (`--mantine-color-text`, set in tokens.css). Until
438
+ * 14/10/2026 that was #000: a paragraph left without a colour came out
439
+ * darker than the title. A fixed `c: 'gray.6'` here would break
440
+ * inheritance instead — the label inside a black button, the line inside
441
+ * the terminal. Title says gray.9; values and emphasis say their colour.
442
+ */
443
+ Text: { defaultProps: { lh: 1.6 } },
444
+ Anchor: { defaultProps: { underline: 'never', fw: 500, c: 'gray.9' } },
445
+ List: { defaultProps: { spacing: 'xs' } },
446
+ Divider: { defaultProps: { color: 'gray.2' } },
447
+ },
448
+ })
package/src/tokens.css CHANGED
@@ -29,21 +29,18 @@
29
29
  --rlg-terminal-rule: rgba(255, 255, 255, 0.08);
30
30
 
31
31
  /*
32
- * The system's only green, used by the SupportModal's status light.
32
+ * Success — one green for every "it worked" (14/10/2026): the notification
33
+ * disc, the "No ar" light, the status bars, the inline "Salvo". It used to
34
+ * be three (teal in notify, teal.7 in the status bars, this green in the
35
+ * light). `--rlg-success` fills; `--rlg-success-ink` is for text on white.
33
36
  *
34
- * Five panels declared it with this same value; Auth and Storage did not —
35
- * and those are exactly the two that did not yet have the modal. Without
36
- * this floor the "No ar" dot would render WITH NO COLOUR at all in those
37
- * two: a `var()` with no value and no fallback does not apply the property,
38
- * and there is no build error and no console warning. It is the same defect
39
- * as the hairline described above.
40
- *
41
- * It lives outside the gray ramp because this is not the success of an
42
- * ACTION (that is gray.9 + IconCheck): it is the measured state of an
43
- * external system. The rule that keeps the exception honest is that the
44
- * colour never appears without the word beside it.
37
+ * `--rlg-status-up` is the old name, kept so that panels still reading it
38
+ * get the same green. As with red, the colour never appears without the
39
+ * word or the glyph beside it.
45
40
  */
46
- --rlg-status-up: #16a34a;
41
+ --rlg-success: var(--mantine-color-green-6, #16a34a);
42
+ --rlg-success-ink: var(--mantine-color-green-7, #15803d);
43
+ --rlg-status-up: var(--rlg-success);
47
44
  }
48
45
 
49
46
  /*
@@ -93,7 +90,18 @@
93
90
  * runtime, after this sheet.
94
91
  */
95
92
  @media (prefers-contrast: more) {
96
- :root:root {
93
+ :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;
98
+ --mantine-color-gray-4: #b6bec8;
99
+ --mantine-color-gray-5: #d3d8de;
100
+ --mantine-color-gray-6: #e5e7eb;
101
+ }
102
+
103
+ :root:root[data-mantine-color-scheme='light'],
104
+ :root:root:not([data-mantine-color-scheme]) {
97
105
  --mantine-color-gray-2: #9ca3af;
98
106
  --mantine-color-gray-3: #9ca3af;
99
107
  --mantine-color-gray-4: #4b5563;
@@ -114,3 +122,107 @@
114
122
  transition: none !important;
115
123
  }
116
124
  }
125
+
126
+ /*
127
+ * The page's colour is body. `--mantine-color-text` is what every text without
128
+ * a colour of its own inherits — body, a Text without `c`, a table cell, a list
129
+ * item. It was #000 (Mantine's default `black`), darker than the title itself.
130
+ * `:root:root[...]` beats the `:root[...]` Mantine injects after this sheet.
131
+ * Same rule as the components-kit's global.css.
132
+ */
133
+ :root:root[data-mantine-color-scheme='light'] {
134
+ --mantine-color-text: var(--mantine-color-gray-6);
135
+ }
136
+
137
+ /* ===========================================================================
138
+ * MODO ESCURO (14/10/2026) — a rampa por elevação.
139
+ *
140
+ * A regra da Apple: a base recua e cada camada que sobe clareia — página,
141
+ * cartão e modal, menu aberto. Não é a rampa invertida (a proposta antiga
142
+ * espelhava gray.0 ↔ gray.9, e o modal herdava a cor exata da página).
143
+ *
144
+ * Os NOMES não mudam: gray.0 continua sendo "base", gray.9 continua sendo
145
+ * "título". Muda o valor que cada papel recebe, e todo `c="gray.6"` e
146
+ * `var(--mantine-color-gray-2)` já escrito acompanha sozinho. O Mantine monta
147
+ * `--mantine-color-gray-filled` e afins como `var(--mantine-color-gray-9)`,
148
+ * então o botão preenchido também vira.
149
+ *
150
+ * "white" vira a superfície elevada (#1A2127), e isso resolve duas coisas
151
+ * de uma vez: o Paper `bg="white"` sobe um degrau acima da página, e o texto
152
+ * que o autoContrast pôs em "white" sobre um preenchimento gray.9 — agora
153
+ * claro — fica escuro e legível.
154
+ *
155
+ * O que NÃO acompanha é o hexadecimal escrito à mão. Superfícies que são
156
+ * escuras nos dois temas (terminal, bloco de código, player) escrevem as
157
+ * suas cores literais de propósito; todo o resto usa o token.
158
+ *
159
+ * Mesmo bloco no global.css do components-kit e no tokens.css do
160
+ * dashboard-kit — os dois andam juntos.
161
+ * =========================================================================== */
162
+ :root:root[data-mantine-color-scheme='dark'] {
163
+ color-scheme: dark;
164
+
165
+ --mantine-color-gray-0: #0b0f12; /* base da página */
166
+ --mantine-color-gray-1: #12181d; /* palco alternado, hover */
167
+ --mantine-color-gray-2: #222a31; /* filete */
168
+ --mantine-color-gray-3: #2f3840; /* borda forte, desabilitado */
169
+ --mantine-color-gray-4: #7d8794; /* label caixa alta — 5,3:1 */
170
+ --mantine-color-gray-5: #98a1ac; /* legenda — 7,4:1 */
171
+ --mantine-color-gray-6: #b6bec8; /* corpo — 10,3:1 */
172
+ --mantine-color-gray-7: #d3d8de; /* dado — 13,4:1 */
173
+ --mantine-color-gray-8: #e5e7eb;
174
+ --mantine-color-gray-9: #f3f4f6; /* título, botão — 17,5:1 */
175
+
176
+ --mantine-color-white: #1a2127; /* superfície elevada; tinta sobre preenchimento claro */
177
+ --mantine-color-body: #0b0f12;
178
+ --mantine-color-text: var(--mantine-color-gray-6);
179
+ --mantine-color-dimmed: var(--mantine-color-gray-5);
180
+ --mantine-color-placeholder: var(--mantine-color-gray-4);
181
+ --mantine-color-default: #1a2127;
182
+ --mantine-color-default-hover: #232b32;
183
+ --mantine-color-default-color: var(--mantine-color-gray-9);
184
+ --mantine-color-default-border: var(--mantine-color-gray-3);
185
+
186
+ /* Vermelho e verde clareiam: sobre a base escura, o red.6 (#E03131) e o
187
+ green.6 (#16A34A) do claro ficam abaixo de 4,5:1 como texto. */
188
+ --mantine-color-red-0: #2a1416;
189
+ --mantine-color-red-1: #3a1a1d;
190
+ --mantine-color-red-2: #5a2427;
191
+ --mantine-color-red-6: #ff6b6b;
192
+ --mantine-color-red-7: #ff8787;
193
+ --mantine-color-red-filled: #ff6b6b;
194
+ --mantine-color-red-filled-hover: #ff8787;
195
+ --mantine-color-green-6: #22c55e;
196
+ --mantine-color-green-7: #4ade80;
197
+
198
+ --rlg-elevated: #1a2127; /* modal, gaveta */
199
+ --rlg-elevated-2: #232b32; /* menu, popover, lista suspensa, notificação */
200
+ --rlg-shell-edge: #2f3840; /* borda do terminal, escuro nos dois temas */
201
+ }
202
+
203
+ /* As camadas que sobem. No claro todas são brancas e a borda separa; no
204
+ escuro cada uma clareia um degrau. */
205
+ :root[data-mantine-color-scheme='dark'] .mantine-Modal-content,
206
+ :root[data-mantine-color-scheme='dark'] .mantine-Modal-header,
207
+ :root[data-mantine-color-scheme='dark'] .mantine-Drawer-content,
208
+ :root[data-mantine-color-scheme='dark'] .mantine-Drawer-header {
209
+ background-color: var(--rlg-elevated);
210
+ }
211
+ :root[data-mantine-color-scheme='dark'] .mantine-Popover-dropdown,
212
+ :root[data-mantine-color-scheme='dark'] .mantine-Menu-dropdown,
213
+ :root[data-mantine-color-scheme='dark'] .mantine-Combobox-dropdown,
214
+ :root[data-mantine-color-scheme='dark'] .mantine-HoverCard-dropdown,
215
+ :root[data-mantine-color-scheme='dark'] .mantine-Notification-root {
216
+ background-color: var(--rlg-elevated-2);
217
+ }
218
+
219
+ /* Imagens. A ilustração da casa é monocromática por regra (traço na rampa
220
+ cinza), então no escuro ela inverte: o traço preto vira claro e o fundo
221
+ branco some na página. Foto e captura só baixam o brilho, como a Apple
222
+ pede para imagem de fundo branco. `data-keep-colors` desliga as duas. */
223
+ :root[data-mantine-color-scheme='dark'] img[src$='.svg']:not([data-keep-colors]) {
224
+ filter: invert(1) hue-rotate(180deg);
225
+ }
226
+ :root[data-mantine-color-scheme='dark'] img:not([src$='.svg']):not([data-keep-colors]) {
227
+ filter: brightness(0.88);
228
+ }