@ciromaciel/dashboard-kit 1.4.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 +1 -1
- package/src/components/ActiveResource.jsx +1 -1
- package/src/components/DataTable.jsx +1 -1
- package/src/components/DeleteModal.jsx +2 -2
- package/src/components/EmptyState.jsx +1 -1
- package/src/components/HoldToConfirm.jsx +6 -1
- package/src/components/Metric.jsx +2 -2
- package/src/components/MoreResults.jsx +3 -3
- package/src/components/PageHeader.jsx +2 -2
- package/src/components/ResourceItem.jsx +1 -1
- package/src/components/SectionCard.jsx +2 -2
- package/src/components/SupportModal.jsx +6 -7
- package/src/components/TableToolbar.jsx +1 -1
- package/src/components/Wordmark.jsx +1 -1
- package/src/components/ZenLabel.jsx +1 -1
- package/src/components/notify.js +15 -4
- package/src/index.js +1 -0
- package/src/theme.js +443 -0
- package/src/tokens.css +79 -13
package/package.json
CHANGED
|
@@ -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',
|
|
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
|
-
|
|
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
|
-
|
|
185
|
+
lts="-0.04em"
|
|
186
186
|
c="gray.9"
|
|
187
187
|
/*
|
|
188
188
|
* `break-all` only where the title IS the identifier. On the
|
|
@@ -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="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
lts="-0.04em"
|
|
95
95
|
c="gray.9"
|
|
96
96
|
style={{ overflowWrap: 'anywhere' }}
|
|
97
97
|
>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
69
|
+
lts="-0.02em"
|
|
70
70
|
c="gray.9"
|
|
71
71
|
>
|
|
72
72
|
{title}
|
|
@@ -81,14 +81,13 @@ export function useHealthCheck(enabled, apiUrl) {
|
|
|
81
81
|
/**
|
|
82
82
|
* The status light.
|
|
83
83
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
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-
|
|
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
|
|
@@ -111,7 +110,7 @@ export function HealthRow({ health, host }) {
|
|
|
111
110
|
/>
|
|
112
111
|
<Text
|
|
113
112
|
fz={12}
|
|
114
|
-
fw={
|
|
113
|
+
fw={500}
|
|
115
114
|
c="gray.9"
|
|
116
115
|
lineClamp={1}
|
|
117
116
|
>
|
|
@@ -198,7 +197,7 @@ export function SupportModal({
|
|
|
198
197
|
fz={{ base: 26, sm: 36 }}
|
|
199
198
|
lh={1.05}
|
|
200
199
|
mt={12}
|
|
201
|
-
|
|
200
|
+
lts="-0.04em"
|
|
202
201
|
c="gray.9"
|
|
203
202
|
>
|
|
204
203
|
{title}{' '}
|
|
@@ -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
|
-
|
|
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"
|
package/src/components/notify.js
CHANGED
|
@@ -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
|
|
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
|
*
|
|
@@ -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
|
-
show('
|
|
66
|
+
show('green.6', 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
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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-
|
|
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
|
/*
|
|
@@ -56,3 +53,72 @@
|
|
|
56
53
|
.rlg-danger:focus-visible {
|
|
57
54
|
outline-color: var(--mantine-color-red-7);
|
|
58
55
|
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* The rules below are the same floor the components-kit ships in its global
|
|
59
|
+
* stylesheet. The panels don't load that sheet, so they are repeated here — the
|
|
60
|
+
* two files must move together.
|
|
61
|
+
*/
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* One focus ring everywhere: gray.9, 2px, 2px away, square like the element it
|
|
65
|
+
* surrounds. `.rlg-danger` above keeps the shape and changes only the colour.
|
|
66
|
+
*/
|
|
67
|
+
:focus-visible {
|
|
68
|
+
outline: 2px solid var(--mantine-color-gray-9);
|
|
69
|
+
outline-offset: 2px;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*
|
|
73
|
+
* 44px under a finger. A panel is dense on purpose — 36px search, 36px create
|
|
74
|
+
* button, `sm` buttons — and that is right for a mouse. A thumb needs Apple's
|
|
75
|
+
* 44px (WCAG 2.5.5), so only coarse pointers get the floor.
|
|
76
|
+
*/
|
|
77
|
+
@media (pointer: coarse) {
|
|
78
|
+
.mantine-Button-root,
|
|
79
|
+
.mantine-ActionIcon-root,
|
|
80
|
+
.mantine-CloseButton-root {
|
|
81
|
+
min-height: 44px;
|
|
82
|
+
min-width: 44px;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* Increased contrast moves the ramp one step darker where it is weakest: the
|
|
88
|
+
* gray.4 label (2.6:1) becomes gray.6, gray.5 becomes gray.7, and the gray.2
|
|
89
|
+
* hairline becomes gray.4. `:root:root` beats the `:root` Mantine injects at
|
|
90
|
+
* runtime, after this sheet.
|
|
91
|
+
*/
|
|
92
|
+
@media (prefers-contrast: more) {
|
|
93
|
+
:root:root {
|
|
94
|
+
--mantine-color-gray-2: #9ca3af;
|
|
95
|
+
--mantine-color-gray-3: #9ca3af;
|
|
96
|
+
--mantine-color-gray-4: #4b5563;
|
|
97
|
+
--mantine-color-gray-5: #374151;
|
|
98
|
+
--mantine-color-gray-6: #374151;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Reduced motion: the sidebar appears instead of sliding in. Background and
|
|
104
|
+
* colour fades on rows and menu items stay — a fade is what Apple asks motion
|
|
105
|
+
* to become, not something to remove.
|
|
106
|
+
*/
|
|
107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
108
|
+
.mantine-AppShell-navbar,
|
|
109
|
+
.mantine-AppShell-main,
|
|
110
|
+
.mantine-AppShell-header {
|
|
111
|
+
transition: none !important;
|
|
112
|
+
}
|
|
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
|
+
}
|