@ciromaciel/dashboard-kit 1.5.0 → 1.6.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.6.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.",
@@ -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
@@ -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'
package/src/theme.js ADDED
@@ -0,0 +1,443 @@
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
+ green: ['#F0FDF4', '#DCFCE7', '#BBF7D0', '#86EFAC', '#4ADE80', '#22C55E', '#16A34A', '#15803D', '#166534', '#14532D'],
102
+
103
+ red: ['#FFF5F5', '#FFE3E3', '#FFC9C9', '#FFA8A8', '#FF8787', '#FF6B6B', '#E03131', '#C92A2A', '#B02525', '#962020'],
104
+ },
105
+
106
+ primaryColor: 'gray',
107
+ primaryShade: 9,
108
+ autoContrast: true,
109
+
110
+ // Zen: shadows exist only for real elevation (Modal, Popover, Menu).
111
+ // Surfaces use a 1px gray.2 border — never a shadow.
112
+ shadows: {
113
+ xs: 'none',
114
+ sm: 'none',
115
+ md: '0 4px 20px rgba(0,0,0,0.05)',
116
+ lg: '0 10px 30px rgba(0,0,0,0.06)',
117
+ xl: '0 20px 40px rgba(0,0,0,0.08)',
118
+ },
119
+
120
+ other: {
121
+ // Small Caps Label — the "announcement" of the canonical pair.
122
+ zenLabel: { fz: 11, fw: 800, tt: 'uppercase', lts: '1.5px', c: 'gray.4' },
123
+ // Panel geometry (spec §Panel geometry).
124
+ panel: { sidebar: 300, sidebarCollapsed: 64, px: 48, pt: 40, pb: 80, maw: 1280, headerMb: 32, blockGap: 20 },
125
+ // Destructive action through persistence, not through a dialog.
126
+ hold: { holdMs: 3000, tickMs: 50 },
127
+ },
128
+
129
+ components: {
130
+ // --- SURFACES ---
131
+ /**
132
+ * Depth comes from surface contrast and from the 1px hairline — never
133
+ * from a shadow.
134
+ *
135
+ * Paper = a declared surface (the block needs to stand out from the
136
+ * stage).
137
+ * Card = an invisible grouping (the space is already enough). When in
138
+ * doubt, Card.
139
+ */
140
+ Paper: {
141
+ defaultProps: { withBorder: true, shadow: 'none', radius: '0' },
142
+ vars: () => ({ root: { '--paper-border-color': 'var(--mantine-color-gray-2)' } }),
143
+ },
144
+ Card: {
145
+ defaultProps: { withBorder: true, shadow: 'none', padding: 'lg' },
146
+ vars: () => ({ root: { '--paper-border-color': 'var(--mantine-color-gray-2)' } }),
147
+ },
148
+ Modal: {
149
+ defaultProps: {
150
+ centered: true,
151
+ size: 'md',
152
+ padding: 'xl',
153
+ radius: 0,
154
+ overlayProps: { backgroundOpacity: 0.45 }, // no blur: glassmorphism is chrome
155
+ },
156
+ styles: {
157
+ title: { fontWeight: 800, fontSize: rem(16), letterSpacing: '-0.04em' },
158
+ },
159
+ },
160
+ Menu: {
161
+ defaultProps: { shadow: 'none', width: 210, withinPortal: true, radius: 0 },
162
+ styles: theme => ({
163
+ dropdown: { border: `1px solid ${theme.colors.gray[2]}` },
164
+ item: { fontSize: rem(13), fontWeight: 500 },
165
+ label: { fontSize: rem(10), fontWeight: 800, textTransform: 'uppercase', letterSpacing: '1.5px' },
166
+ }),
167
+ },
168
+ Popover: {
169
+ defaultProps: { shadow: 'none', radius: 0 },
170
+ styles: theme => ({ dropdown: { border: `1px solid ${theme.colors.gray[2]}` } }),
171
+ },
172
+
173
+ // --- DATA ---
174
+ /**
175
+ * The "data" role: 12-14px, gray.7. A 16px paragraph is comfortable; a
176
+ * 200-row table at 16px is impossible to scan.
177
+ *
178
+ * The app's main table is CSS Grid (see components/zen/DataTable) — fr
179
+ * for content, fixed px for actions, the whole row clickable. These
180
+ * defaults apply to the simple tables that are left.
181
+ */
182
+ Table: {
183
+ defaultProps: { verticalSpacing: 'sm', horizontalSpacing: 'md', withRowBorders: true },
184
+ styles: theme => ({
185
+ th: {
186
+ fontSize: rem(10),
187
+ fontWeight: 800,
188
+ textTransform: 'uppercase',
189
+ letterSpacing: '1.5px',
190
+ color: theme.colors.gray[5],
191
+ borderBottom: `1px solid ${theme.colors.gray[2]}`,
192
+ whiteSpace: 'nowrap',
193
+ },
194
+ td: { borderBottom: `1px solid ${theme.colors.gray[2]}`, fontSize: rem(13), color: theme.colors.gray[7] },
195
+ }),
196
+ },
197
+ Badge: {
198
+ defaultProps: { size: 'sm', variant: 'outline', color: 'gray', fw: 800, radius: 0 },
199
+ styles: { label: { textTransform: 'uppercase', letterSpacing: '1.5px', fontSize: rem(10) } },
200
+ },
201
+ Code: {
202
+ styles: { root: { fontSize: rem(12) } },
203
+ },
204
+ Skeleton: {
205
+ // The Skeleton reproduces the content's real grid — and square
206
+ // corners, like everything else.
207
+ defaultProps: { radius: 0 },
208
+ },
209
+
210
+ // --- CONTROLS ---
211
+ /**
212
+ * Hierarchy without color — filled (primary) → outline (secondary) →
213
+ * subtle (tertiary).
214
+ */
215
+ /*
216
+ * This panel follows the design system here where the theme inherited from
217
+ * Analytics did not: a button is a LABEL in a rectangle — uppercase,
218
+ * 800, 1px tracking (02-tipografia), as the prototype draws it.
219
+ */
220
+ Button: {
221
+ defaultProps: { size: 'sm', radius: '0' },
222
+ styles: { root: { textTransform: 'uppercase', fontWeight: 800, letterSpacing: '1px', fontSize: rem(12) } },
223
+ },
224
+ /**
225
+ * `loaderProps` exists because `bars` and `oval` do not occupy the same
226
+ * space with the same number. The ActionIcon sizes its loader at
227
+ * `calc(var(--ai-size) * 0.55)` — ~15px at the `md` used here. For the
228
+ * `oval` that is a thin-stroked ring; for the `bars` it is a FULL 15px
229
+ * block, three bars painted from end to end.
230
+ *
231
+ * Next to 16px glyphs with `stroke={1.5}`, the two numbers are equal and
232
+ * the two visual masses are not: the loader became the heaviest element
233
+ * of the row, and the whole row seemed to flicker in size when an audit
234
+ * started.
235
+ *
236
+ * 12px gives the loader back the mass of an outline icon. Here and not
237
+ * in the screen because the mismatch comes from the geometry of `bars`
238
+ * with the `ActionIcon` — it applies to every icon button in the panel,
239
+ * and defined case by case it diverges again on the next one.
240
+ */
241
+ ActionIcon: {
242
+ defaultProps: { size: 'md', variant: 'subtle', color: 'gray', radius: 0, loaderProps: { size: 12 } },
243
+ },
244
+ /**
245
+ * The input's frame is the gray.2 hairline at rest and gray.9 on focus.
246
+ * The `unstyled` variant (used on the stage) is left out: there only the
247
+ * bottom border exists.
248
+ *
249
+ * THE DESCRIPTION GOES BELOW THE FIELD, not between the label and it.
250
+ *
251
+ * Mantine's default order (label → description → input) misaligns any
252
+ * row of side-by-side fields: a two-line description pushes its own
253
+ * input 34px down, and the neighbor without a description is left
254
+ * hanging at the top. That is what happened with "Cor de marca / Fonte /
255
+ * Tema" on the status page screen — three inputs at three heights.
256
+ *
257
+ * Below the field, the top of every single-line input coincides, and the
258
+ * alignment stops depending on how many lines each text takes up. The
259
+ * reading also improves: the label says what it is, the field shows the
260
+ * value, and the note explains — in the order in which one reads and
261
+ * fills it in.
262
+ *
263
+ * It applies to the whole panel, not field by field: defined here, it is
264
+ * invariant; done in the screen, it diverges again on the next form.
265
+ */
266
+ TextInput: {
267
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
268
+ styles: (theme, props) => ({
269
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
270
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
271
+ }),
272
+ },
273
+ /**
274
+ * The PasswordInput was left out when this block was written, and so it
275
+ * fell back to Mantine's default (`size: "md"`): on the sign-in screen,
276
+ * the password field was visibly larger than the e-mail one right above
277
+ * it — font, height and rounded corners, all different.
278
+ *
279
+ * It is not a simple input: it carries the eye button
280
+ * (`visibilityToggle`) and that is why it has its own component in the
281
+ * theme. Inheriting from `TextInput` does not happen — in Mantine each
282
+ * component reads its own key.
283
+ */
284
+ PasswordInput: {
285
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
286
+ styles: (theme, props) => ({
287
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
288
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
289
+ }),
290
+ },
291
+ Textarea: {
292
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
293
+ styles: (theme, props) => ({
294
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
295
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
296
+ }),
297
+ },
298
+ Select: {
299
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
300
+ styles: (theme, props) => ({
301
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
302
+ input: props.variant === 'unstyled' ? {} : { border: `1px solid ${theme.colors.gray[2]}` },
303
+ }),
304
+ },
305
+ NumberInput: {
306
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
307
+ styles: theme => ({
308
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
309
+ input: { border: `1px solid ${theme.colors.gray[2]}` },
310
+ }),
311
+ },
312
+ Autocomplete: {
313
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
314
+ styles: theme => ({
315
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
316
+ input: { border: `1px solid ${theme.colors.gray[2]}` },
317
+ }),
318
+ },
319
+ /**
320
+ * The TagsInput is a PILL BOX, not a simple input — and that is why it
321
+ * misaligned with the TextInput next to it.
322
+ *
323
+ * Mantine sizes that box by its content: empty, it measured 22px against
324
+ * the TextInput's 36px, and it also got larger internal breathing room.
325
+ * On a "Nome | Tags" row the two fields started with 6px of difference —
326
+ * too little to name, enough to make the row look crooked.
327
+ *
328
+ * The target of `minHeight` is the FRAME (`.mantine-PillsInput-root`),
329
+ * not the `input`: in this component `input` names the typing field
330
+ * INSIDE the pill box, and sizing it does not move the frame. That is
331
+ * what made the first attempt fix nothing.
332
+ *
333
+ * The box keeps growing as the tags come in, which is the right behavior
334
+ * — the minimum only guarantees the alignment at rest.
335
+ */
336
+ TagsInput: {
337
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
338
+ styles: theme => ({
339
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
340
+ input: {
341
+ border: `1px solid ${theme.colors.gray[2]}`,
342
+ minHeight: rem(36),
343
+ display: 'flex',
344
+ alignItems: 'center',
345
+ paddingTop: 0,
346
+ paddingBottom: 0,
347
+ },
348
+ }),
349
+ },
350
+ ColorInput: {
351
+ defaultProps: { size: 'sm', radius: '0', inputWrapperOrder: ['label', 'input', 'description', 'error'] },
352
+ styles: theme => ({
353
+ label: { marginBottom: rem(6), fontWeight: 700, fontSize: rem(13) },
354
+ input: { border: `1px solid ${theme.colors.gray[2]}` },
355
+ }),
356
+ },
357
+ SegmentedControl: {
358
+ defaultProps: { size: 'xs', radius: 0, color: 'gray.9' },
359
+ styles: { label: { fontWeight: 700, fontSize: rem(12) } },
360
+ },
361
+ Switch: {
362
+ defaultProps: { size: 'sm', color: 'gray.9', radius: 0 },
363
+ styles: { label: { fontSize: rem(13), fontWeight: 500 } },
364
+ },
365
+ Checkbox: {
366
+ defaultProps: { size: 'sm', radius: 0, color: 'gray.9' },
367
+ },
368
+ // The prototype's tabs: 11px labels, uppercase, 800, 1px tracking.
369
+ Tabs: {
370
+ styles: { tab: { fontSize: rem(11), fontWeight: 800, textTransform: 'uppercase', letterSpacing: '1px', borderRadius: 0 } },
371
+ },
372
+ Kbd: {
373
+ defaultProps: { size: 'xs' },
374
+ styles: theme => ({
375
+ root: {
376
+ fontFamily: 'var(--mantine-font-family-monospace)',
377
+ fontSize: rem(10),
378
+ fontWeight: 700,
379
+ color: theme.colors.gray[5],
380
+ backgroundColor: theme.white,
381
+ border: `1px solid ${theme.colors.gray[2]}`,
382
+ borderBottomColor: theme.colors.gray[2],
383
+ boxShadow: 'none',
384
+ borderRadius: 0,
385
+ },
386
+ }),
387
+ },
388
+ ThemeIcon: {
389
+ // The ThemeIcon is always square, never a circle.
390
+ defaultProps: { size: 'lg', variant: 'light', color: 'gray', radius: 0 },
391
+ },
392
+ Avatar: {
393
+ // The system's portrait is square, like the ThemeIcon above.
394
+ defaultProps: { radius: 0, color: 'gray' },
395
+ },
396
+ Alert: {
397
+ defaultProps: { variant: 'light', color: 'gray', radius: 0 },
398
+ styles: { title: { fontWeight: 800, fontSize: rem(12) }, message: { fontSize: rem(13) } },
399
+ },
400
+ Tooltip: {
401
+ defaultProps: { fz: 'xs', withArrow: false, openDelay: 300, color: 'gray.9', radius: 0 },
402
+ },
403
+ Progress: {
404
+ defaultProps: { size: 'sm', color: 'gray.9', radius: 0 },
405
+ },
406
+ /**
407
+ * `bars` as the system default, and not `oval`.
408
+ *
409
+ * This `type` applies to EVERY loader in the panel — including the one
410
+ * that Button and ActionIcon build on their own when they receive
411
+ * `loading`. And that is exactly where the `oval` failed: a ring
412
+ * spinning inside a 16px button is indistinguishable from a still ring,
413
+ * and the defect showed up side by side on the same table row — the
414
+ * status indicator (which already used `bars`) moving, and the one on the
415
+ * button next to it not.
416
+ *
417
+ * The three bars change HEIGHT at different times: the silhouette
418
+ * changes on every frame, so the movement is legible even at 10px and
419
+ * even at a glance. It is the difference between an equalizer and a
420
+ * spinning disc — both spin, only one communicates.
421
+ */
422
+ Loader: {
423
+ defaultProps: { color: 'gray', type: 'bars' },
424
+ },
425
+ // The notification comes from the kit: one shape across the apps and the SDK.
426
+ ...notificationTheme,
427
+
428
+ // --- TYPOGRAPHY ---
429
+ Title: { defaultProps: { lh: 1, c: 'gray.9' } },
430
+ /**
431
+ * A Text with no colour inherits — and what it inherits from the page is
432
+ * body gray.6 (`--mantine-color-text`, set in tokens.css). Until
433
+ * 14/10/2026 that was #000: a paragraph left without a colour came out
434
+ * darker than the title. A fixed `c: 'gray.6'` here would break
435
+ * inheritance instead — the label inside a black button, the line inside
436
+ * the terminal. Title says gray.9; values and emphasis say their colour.
437
+ */
438
+ Text: { defaultProps: { lh: 1.6 } },
439
+ Anchor: { defaultProps: { underline: 'never', fw: 500, c: 'gray.9' } },
440
+ List: { defaultProps: { spacing: 'xs' } },
441
+ Divider: { defaultProps: { color: 'gray.2' } },
442
+ },
443
+ })
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
  /*
@@ -114,3 +111,14 @@
114
111
  transition: none !important;
115
112
  }
116
113
  }
114
+
115
+ /*
116
+ * The page's colour is body. `--mantine-color-text` is what every text without
117
+ * a colour of its own inherits — body, a Text without `c`, a table cell, a list
118
+ * item. It was #000 (Mantine's default `black`), darker than the title itself.
119
+ * `:root:root[...]` beats the `:root[...]` Mantine injects after this sheet.
120
+ * Same rule as the components-kit's global.css.
121
+ */
122
+ :root:root[data-mantine-color-scheme='light'] {
123
+ --mantine-color-text: var(--mantine-color-gray-6);
124
+ }