@ciromaciel/dashboard-kit 1.0.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/README.md +40 -0
- package/package.json +46 -0
- package/src/auth-labels.js +77 -0
- package/src/components/ActiveResource.jsx +68 -0
- package/src/components/AuthLoader.jsx +66 -0
- package/src/components/AuthScreen.jsx +32 -0
- package/src/components/CardSkeleton.jsx +82 -0
- package/src/components/CommandCenter.css +57 -0
- package/src/components/CommandCenter.jsx +185 -0
- package/src/components/DashboardFrame.jsx +97 -0
- package/src/components/DataTable.jsx +153 -0
- package/src/components/DeleteModal.jsx +341 -0
- package/src/components/EmptyState.jsx +55 -0
- package/src/components/GroupLabel.jsx +28 -0
- package/src/components/HoldToConfirm.jsx +151 -0
- package/src/components/ListSkeleton.jsx +55 -0
- package/src/components/MenuItem.jsx +56 -0
- package/src/components/Metric.jsx +178 -0
- package/src/components/MoreResults.jsx +137 -0
- package/src/components/NavItem.jsx +57 -0
- package/src/components/PageHeader.jsx +124 -0
- package/src/components/PlanBanner.jsx +88 -0
- package/src/components/ResourceItem.jsx +93 -0
- package/src/components/SectionCard.jsx +212 -0
- package/src/components/SidebarShell.jsx +271 -0
- package/src/components/SupportModal.jsx +269 -0
- package/src/components/TableSkeleton.jsx +43 -0
- package/src/components/TableToolbar.jsx +136 -0
- package/src/components/Terminal.jsx +217 -0
- package/src/components/Wordmark.jsx +52 -0
- package/src/components/ZenLabel.jsx +30 -0
- package/src/components/notify-theme.js +103 -0
- package/src/components/notify.js +65 -0
- package/src/components/table-grid.js +27 -0
- package/src/hooks/useExclusiveModal.js +49 -0
- package/src/hooks/usePaginatedList.js +206 -0
- package/src/index.js +99 -0
- package/src/tokens.css +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# @ciromaciel/dashboard-kit
|
|
2
|
+
|
|
3
|
+
Os componentes que os sete painéis da RiLiGar compartilham.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun add @ciromaciel/dashboard-kit
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```jsx
|
|
10
|
+
import { DataTable, EmptyState, AuthLoader } from '@ciromaciel/dashboard-kit'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## O que entra aqui
|
|
14
|
+
|
|
15
|
+
O que é **o mesmo** nos painéis. Não entra o que é do produto — o semáforo de
|
|
16
|
+
estado do Monitors, a régua de moeda do Payments. A linha divisória: se um
|
|
17
|
+
painel precisou mudar o componente para servir ao produto, o componente ainda
|
|
18
|
+
não é de todos.
|
|
19
|
+
|
|
20
|
+
| | |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `DataTable` · `TableSkeleton` · `templateFor` | a tabela e a espera dela |
|
|
23
|
+
| `EmptyState` | o vazio — que não é a mesma coisa que "ainda carregando" |
|
|
24
|
+
| `SectionCard` · `Field` · `FieldStacked` · `CardActions` | o cartão de seção |
|
|
25
|
+
| `PageHeader` | título e trilha |
|
|
26
|
+
| `HoldToConfirmButton` · `HoldToConfirmIcon` | a confirmação destrutiva |
|
|
27
|
+
| `AuthLoader` | a tela do instante em que o painel não sabe quem você é |
|
|
28
|
+
| `Wordmark` | a marca, em dois papéis. O nome vem por prop |
|
|
29
|
+
| `ZenLabel` · `notify` | rótulo e aviso |
|
|
30
|
+
|
|
31
|
+
## Por que não há build
|
|
32
|
+
|
|
33
|
+
O pacote publica o fonte. Os sete painéis são Vite, que compila JSX de
|
|
34
|
+
dependência sem cerimônia — um passo de build só produziria o que o consumidor
|
|
35
|
+
já sabe produzir. Menos uma engrenagem para quebrar.
|
|
36
|
+
|
|
37
|
+
## Peers
|
|
38
|
+
|
|
39
|
+
`@mantine/core`, `@mantine/hooks`, `@mantine/notifications`,
|
|
40
|
+
`@tabler/icons-react`, `react`. Todos já presentes nos sete painéis.
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ciromaciel/dashboard-kit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Os componentes que os sete painéis da RiLiGar compartilham: a tabela, a espera, o vazio, a confirmação destrutiva e a marca.",
|
|
7
|
+
"main": "src/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "bun test tests/",
|
|
17
|
+
"format": "prettier --write \"src/**/*.{js,jsx,css}\"",
|
|
18
|
+
"format:check": "prettier --check \"src/**/*.{js,jsx,css}\""
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"@mantine/core": "^7.0.0 || ^8.0.0",
|
|
22
|
+
"@mantine/hooks": "^7.0.0 || ^8.0.0",
|
|
23
|
+
"@tabler/icons-react": "^3.0.0",
|
|
24
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
25
|
+
"@mantine/notifications": "^7.0.0 || ^8.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@happy-dom/global-registrator": "^20.14.3",
|
|
29
|
+
"@mantine/core": "^8.0.0",
|
|
30
|
+
"@mantine/hooks": "^8.0.0",
|
|
31
|
+
"@mantine/notifications": "^8.0.0",
|
|
32
|
+
"@tabler/icons-react": "^3.0.0",
|
|
33
|
+
"@testing-library/react": "^16.0.0",
|
|
34
|
+
"happy-dom": "^15.0.0",
|
|
35
|
+
"react": "^19.0.0",
|
|
36
|
+
"react-dom": "^19.0.0"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/riligar/packages.git",
|
|
44
|
+
"directory": "dashboard-kit"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words of the authentication screen, in one place.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS FILE EXISTS
|
|
5
|
+
*
|
|
6
|
+
* `@ciromaciel/auth-react` renders the auth form but takes every visible string
|
|
7
|
+
* as a prop. Each dashboard was therefore carrying its own copy of the same
|
|
8
|
+
* Portuguese — seven times over. They were identical when compared
|
|
9
|
+
* (`signInButton: 'Entrar'` in all seven), which is precisely the state that
|
|
10
|
+
* does not survive: the next wording change lands in one panel and the other
|
|
11
|
+
* six keep the old text, with nothing failing and no test noticing.
|
|
12
|
+
*
|
|
13
|
+
* WHY THE LABELS ARE IN PORTUGUESE AND THE KEYS IN ENGLISH
|
|
14
|
+
*
|
|
15
|
+
* The keys are the SDK's contract (`emailPlaceholder`, `sendCodeButton`) and
|
|
16
|
+
* belong to the code. The values are what a person reads on screen. That is the
|
|
17
|
+
* boundary the house draws in https://github.com/riligar/infrastructure/blob/prod/apps/website/docs/2026-09-10-idioma-do-codigo.md: not
|
|
18
|
+
* "string vs. not-string", but WHO READS IT.
|
|
19
|
+
*
|
|
20
|
+
* WHY THERE IS ONLY ONE SCREEN HERE
|
|
21
|
+
*
|
|
22
|
+
* There used to be five — sign-in, sign-up, magic link, reset, verification.
|
|
23
|
+
* All five answered the same question (does this person control this inbox?),
|
|
24
|
+
* and four of them answered it with a URL. Now there is one way in: the code
|
|
25
|
+
* that arrives by email. There is no separate sign-up — the first entry creates
|
|
26
|
+
* the account.
|
|
27
|
+
*
|
|
28
|
+
* HOW TO CHANGE A WORD
|
|
29
|
+
*
|
|
30
|
+
* Change it here and the seven dashboards change together. If one product ever
|
|
31
|
+
* needs a different word, spread over it at the call site
|
|
32
|
+
* (`{...AUTH_LABELS.signIn, sendCodeButton: 'Acessar'}`) rather than copying
|
|
33
|
+
* the block back into the panel — a copy is how this file was needed in the
|
|
34
|
+
* first place.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export const AUTH_LABELS = {
|
|
38
|
+
signIn: {
|
|
39
|
+
email: 'Email',
|
|
40
|
+
emailPlaceholder: 'seu@email.com',
|
|
41
|
+
invalidEmail: 'Email inválido',
|
|
42
|
+
sendCodeButton: 'Enviar código',
|
|
43
|
+
sendingCode: 'Enviando…',
|
|
44
|
+
codeSent: 'Digite o código que enviamos',
|
|
45
|
+
codeSentTo: 'Enviado para',
|
|
46
|
+
codeLabel: 'Código de acesso',
|
|
47
|
+
confirmCode: 'Confirmar',
|
|
48
|
+
verifyingCode: 'Entrando…',
|
|
49
|
+
invalidCode: 'Código inválido.',
|
|
50
|
+
resendCode: 'Reenviar código',
|
|
51
|
+
changeEmail: 'Usar outro e-mail',
|
|
52
|
+
// The terms notice under the first step. Two keys, not one sentence:
|
|
53
|
+
// the link text has to be a separate node, and splitting it here keeps
|
|
54
|
+
// the whole sentence readable instead of hiding half of it in JSX.
|
|
55
|
+
termsNotice: 'Criando uma conta, você concorda com todos os nossos',
|
|
56
|
+
termsLink: 'termos e condições',
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The titles and the notification texts of the screen.
|
|
62
|
+
*
|
|
63
|
+
* Separate from `AUTH_LABELS` because these are not the SDK's contract: the
|
|
64
|
+
* panel passes them as `title`/`subtitle` and uses them in its own `notify`
|
|
65
|
+
* calls. Same reason for existing — seven identical copies.
|
|
66
|
+
*/
|
|
67
|
+
export const AUTH_TEXTS = {
|
|
68
|
+
signIn: {
|
|
69
|
+
title: 'Entrar',
|
|
70
|
+
subtitle: 'Enviamos um código para o seu e-mail',
|
|
71
|
+
codeSent: 'Código enviado',
|
|
72
|
+
codeSentMessage: 'Confira sua caixa de entrada e informe o código.',
|
|
73
|
+
success: 'Bem-vindo!',
|
|
74
|
+
successMessage: 'Você foi autenticado com sucesso',
|
|
75
|
+
error: 'Erro ao entrar',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Box, Text } from '@mantine/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* O recurso aberto, no topo da coluna.
|
|
5
|
+
*
|
|
6
|
+
* VEIO DE SEIS CÓPIAS COM QUATRO NOMES: `ActiveProject` (payments, messages,
|
|
7
|
+
* hoster), `ActiveInstance` (storage), `ActiveApplication` (auth) e
|
|
8
|
+
* `ActiveFunction` (functions). Payments e Messages eram idênticos exceto pelo
|
|
9
|
+
* idioma do comentário; as outras diferiam apenas em DE ONDE vinham o título e
|
|
10
|
+
* o subtítulo — `project.name`/`tenant.name`, `summary.label`/
|
|
11
|
+
* `tenant.environment`.
|
|
12
|
+
*
|
|
13
|
+
* Isso não é um componente por produto: é um componente que recebia o dado já
|
|
14
|
+
* mastigado em quatro lugares diferentes. Aqui ele recebe `title` e `subtitle`
|
|
15
|
+
* prontos, e cada painel decide o que são.
|
|
16
|
+
*
|
|
17
|
+
* O centro é o enquadramento: o nome centrado marca "você está DENTRO deste
|
|
18
|
+
* recurso", contra a lista alinhada à esquerda que ficava acima. É o que separa
|
|
19
|
+
* navegar de operar.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} title O nome do recurso. Uma linha, com reticências.
|
|
22
|
+
* @param {string} subtitle A segunda linha — estado, ambiente, endereço. Some
|
|
23
|
+
* quando não houver: um rótulo vazio ocuparia a linha
|
|
24
|
+
* mais valiosa para não dizer nada.
|
|
25
|
+
* @param {'muted'|'alert'} tone `alert` pinta o subtítulo de vermelho. É a
|
|
26
|
+
* exceção cromática do sistema, e existe para o caso
|
|
27
|
+
* em que o subtítulo É o problema — "SEM GATEWAY" num
|
|
28
|
+
* projeto que não consegue cobrar.
|
|
29
|
+
* @param {boolean} mono Título em monoespaçado, para nome que é identificador
|
|
30
|
+
* (o Functions nomeia função, não produto).
|
|
31
|
+
*/
|
|
32
|
+
export function ActiveResource({ title, subtitle, tone = 'muted', mono = false }) {
|
|
33
|
+
return (
|
|
34
|
+
<Box
|
|
35
|
+
w="100%"
|
|
36
|
+
px={12}
|
|
37
|
+
pb={10}
|
|
38
|
+
mb={4}
|
|
39
|
+
ta="center"
|
|
40
|
+
>
|
|
41
|
+
<Text
|
|
42
|
+
fz={13}
|
|
43
|
+
fw={900}
|
|
44
|
+
c="gray.9"
|
|
45
|
+
lts="-0.02em"
|
|
46
|
+
lh={1.3}
|
|
47
|
+
lineClamp={1}
|
|
48
|
+
ff={mono ? 'monospace' : undefined}
|
|
49
|
+
>
|
|
50
|
+
{title}
|
|
51
|
+
</Text>
|
|
52
|
+
|
|
53
|
+
{subtitle && (
|
|
54
|
+
<Text
|
|
55
|
+
fz={9}
|
|
56
|
+
fw={800}
|
|
57
|
+
c={tone === 'alert' ? 'red.6' : 'gray.4'}
|
|
58
|
+
tt="uppercase"
|
|
59
|
+
lts="1.5px"
|
|
60
|
+
mt={3}
|
|
61
|
+
lineClamp={1}
|
|
62
|
+
>
|
|
63
|
+
{subtitle}
|
|
64
|
+
</Text>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
)
|
|
68
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Flex, Loader, Stack, Text } from '@mantine/core'
|
|
2
|
+
|
|
3
|
+
import { Wordmark } from './Wordmark'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The screen for the instant when the dashboard doesn't yet know who you are.
|
|
7
|
+
*
|
|
8
|
+
* It is the SAME screen across all seven dashboards. Each one used to have its
|
|
9
|
+
* own: two said "Verificando sua sessão...", one only showed the wordmark with
|
|
10
|
+
* no text, and another said "Iniciando sua experiência..." with the markup
|
|
11
|
+
* loose in the routes file. Anyone using more than one product saw different
|
|
12
|
+
* screens for the same moment of waiting.
|
|
13
|
+
*
|
|
14
|
+
* THE THREE RUNGS, AND WHY ALL THREE
|
|
15
|
+
*
|
|
16
|
+
* - the wordmark (variant `label`) says WHERE you are;
|
|
17
|
+
* - the spinner says that SOMETHING is happening;
|
|
18
|
+
* - the text says WHAT is happening.
|
|
19
|
+
*
|
|
20
|
+
* The spinner alone is an object spinning: it tells you the system hasn't
|
|
21
|
+
* frozen, and nothing more. Without the wordmark, the screen could belong to
|
|
22
|
+
* any product — and in a suite of seven that matters. Without the text, you
|
|
23
|
+
* don't know whether to expect two seconds or whether something is hung.
|
|
24
|
+
*
|
|
25
|
+
* THE LOADER SIZE IS `xs`, AND THAT IS A RULE
|
|
26
|
+
*
|
|
27
|
+
* The wait here is network, not heavy work. A large loader promises a delay
|
|
28
|
+
* that doesn't exist and draws more attention than the wordmark — inverting the
|
|
29
|
+
* hierarchy of the three rungs.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} name - The product name. Required: it is the "where" rung.
|
|
32
|
+
* @param {string} label - The waiting sentence. The default covers the session
|
|
33
|
+
* case; whoever is checking something else (permissions,
|
|
34
|
+
* for instance) passes their own.
|
|
35
|
+
*/
|
|
36
|
+
export function AuthLoader({ name, label = 'Verificando sua sessão...' }) {
|
|
37
|
+
return (
|
|
38
|
+
<Flex
|
|
39
|
+
h="100dvh"
|
|
40
|
+
justify="center"
|
|
41
|
+
align="center"
|
|
42
|
+
>
|
|
43
|
+
<Stack
|
|
44
|
+
align="center"
|
|
45
|
+
gap="md"
|
|
46
|
+
>
|
|
47
|
+
<Wordmark
|
|
48
|
+
name={name}
|
|
49
|
+
variant="label"
|
|
50
|
+
lh={1.4}
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<Loader size="xs" />
|
|
54
|
+
|
|
55
|
+
<Text
|
|
56
|
+
ta="center"
|
|
57
|
+
fw={500}
|
|
58
|
+
fz="sm"
|
|
59
|
+
c="gray.6"
|
|
60
|
+
>
|
|
61
|
+
{label}
|
|
62
|
+
</Text>
|
|
63
|
+
</Stack>
|
|
64
|
+
</Flex>
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Flex } from '@mantine/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The stage for the authentication screens.
|
|
5
|
+
*
|
|
6
|
+
* The five auth screens (sign-in, sign-up, magic link, reset password, verify
|
|
7
|
+
* email) are the same framing with an SDK component in the middle: one card
|
|
8
|
+
* centred on an empty screen. Repeating the `<Flex>` in each file gave every
|
|
9
|
+
* dashboard five definitions of the same measurement — and that is how one of
|
|
10
|
+
* them keeps `100vh` after the others have moved on.
|
|
11
|
+
*
|
|
12
|
+
* It was born in the Monitors dashboard, which had already collapsed its own
|
|
13
|
+
* five copies into one. This is that component, raised to the kit so the other
|
|
14
|
+
* six stop keeping their own.
|
|
15
|
+
*
|
|
16
|
+
* `100dvh`, not `100vh`: on mobile Safari and Chrome the address bar counts
|
|
17
|
+
* towards `vh`, and part of the card ended up below the fold. The `px` keeps it
|
|
18
|
+
* from touching both edges on a narrow screen.
|
|
19
|
+
*/
|
|
20
|
+
export function AuthScreen({ children }) {
|
|
21
|
+
return (
|
|
22
|
+
<Flex
|
|
23
|
+
h="100dvh"
|
|
24
|
+
px="md"
|
|
25
|
+
justify="center"
|
|
26
|
+
align="center"
|
|
27
|
+
direction="column"
|
|
28
|
+
>
|
|
29
|
+
{children}
|
|
30
|
+
</Flex>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Box, Card, Group, Skeleton, Stack } from '@mantine/core'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The `SectionCard` skeleton — for the screens that list CARDS instead of table
|
|
5
|
+
* rows (domains, gateways, projects).
|
|
6
|
+
*
|
|
7
|
+
* It exists for the same reason as `TableSkeleton`: while the data hasn't
|
|
8
|
+
* arrived, the screen shows the SHAPE of what's coming, in the place where it
|
|
9
|
+
* will appear. What used to be here was an `EmptyState` with the title
|
|
10
|
+
* "Carregando" — a sentence about the wait occupying the space reserved for the
|
|
11
|
+
* content. The skeleton says the same thing without spending a line of text,
|
|
12
|
+
* and the layout doesn't jump on the swap because the space was already
|
|
13
|
+
* reserved.
|
|
14
|
+
*
|
|
15
|
+
* The geometry follows the SectionCard's real header: same padding
|
|
16
|
+
* (px 16/24 · py 20), a 10px eyebrow above the 18px title.
|
|
17
|
+
*
|
|
18
|
+
* The widths VARY between cards (`58% 44% 66%`): identical bars read as a grid,
|
|
19
|
+
* not as a list of different things.
|
|
20
|
+
*/
|
|
21
|
+
const LARGURAS = ['58%', '44%', '66%', '51%']
|
|
22
|
+
|
|
23
|
+
export function CardSkeleton({ cards = 2, bodyLines = 2 }) {
|
|
24
|
+
return (
|
|
25
|
+
<Stack
|
|
26
|
+
gap={20}
|
|
27
|
+
aria-hidden
|
|
28
|
+
>
|
|
29
|
+
{Array.from({ length: cards }, (_, i) => (
|
|
30
|
+
<Card
|
|
31
|
+
key={i}
|
|
32
|
+
p={0}
|
|
33
|
+
withBorder
|
|
34
|
+
styles={{ root: { overflow: 'hidden' } }}
|
|
35
|
+
>
|
|
36
|
+
<Group
|
|
37
|
+
justify="space-between"
|
|
38
|
+
align="flex-end"
|
|
39
|
+
px={{ base: 16, sm: 24 }}
|
|
40
|
+
py={20}
|
|
41
|
+
gap={16}
|
|
42
|
+
wrap="nowrap"
|
|
43
|
+
>
|
|
44
|
+
<Box
|
|
45
|
+
miw={0}
|
|
46
|
+
flex={1}
|
|
47
|
+
>
|
|
48
|
+
<Skeleton
|
|
49
|
+
height={10}
|
|
50
|
+
width={72}
|
|
51
|
+
radius={0}
|
|
52
|
+
mb={6}
|
|
53
|
+
/>
|
|
54
|
+
<Skeleton
|
|
55
|
+
height={18}
|
|
56
|
+
width={LARGURAS[i % LARGURAS.length]}
|
|
57
|
+
radius={0}
|
|
58
|
+
/>
|
|
59
|
+
</Box>
|
|
60
|
+
</Group>
|
|
61
|
+
|
|
62
|
+
{bodyLines > 0 && (
|
|
63
|
+
<Stack
|
|
64
|
+
gap={12}
|
|
65
|
+
px={{ base: 16, sm: 24 }}
|
|
66
|
+
pb={20}
|
|
67
|
+
>
|
|
68
|
+
{Array.from({ length: bodyLines }, (_, j) => (
|
|
69
|
+
<Skeleton
|
|
70
|
+
key={j}
|
|
71
|
+
height={14}
|
|
72
|
+
width={LARGURAS[(i + j + 1) % LARGURAS.length]}
|
|
73
|
+
radius={0}
|
|
74
|
+
/>
|
|
75
|
+
))}
|
|
76
|
+
</Stack>
|
|
77
|
+
)}
|
|
78
|
+
</Card>
|
|
79
|
+
))}
|
|
80
|
+
</Stack>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* THE HAIRLINE BETWEEN THE METRICS — and why this is CSS, not inline `style`.
|
|
3
|
+
*
|
|
4
|
+
* The band changes shape three times: one column on phones, two at `sm`, four
|
|
5
|
+
* at `md`. The divider has to follow along, SWITCHING AXIS — vertical between
|
|
6
|
+
* columns, horizontal between stacked rows. A vertical border on a one-column
|
|
7
|
+
* grid becomes a stray stroke to the left of each block.
|
|
8
|
+
*
|
|
9
|
+
* The first version of this component used a `<Divider orientation="vertical">`
|
|
10
|
+
* hidden below `md`. The result was that FOUR METRICS IN 2×2 (the Monitors case,
|
|
11
|
+
* on a tablet) ended up with no separator at all: the vertical one was hidden
|
|
12
|
+
* and there was no horizontal one. The Monitors dashboard had already solved
|
|
13
|
+
* this in `global.css`, and the extraction into the kit had lost the fix.
|
|
14
|
+
*
|
|
15
|
+
* It depends on a media query and on `:nth-child` — neither of which can be
|
|
16
|
+
* expressed in inline style, and that is why it has to be a stylesheet.
|
|
17
|
+
*
|
|
18
|
+
* `!important` because Mantine's stylesheet comes in afterwards and would put
|
|
19
|
+
* the border back.
|
|
20
|
+
*/
|
|
21
|
+
.rlg-metric-cell {
|
|
22
|
+
border-left: none !important;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* base: one column. The divider is horizontal, between the rows. */
|
|
26
|
+
.rlg-metric-cell + .rlg-metric-cell {
|
|
27
|
+
border-top: var(--rlg-border, 1px solid var(--mantine-color-gray-2)) !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* sm: two columns. Vertical on the right-hand cell of each pair; horizontal
|
|
31
|
+
only between rows — that is, from the third cell on. */
|
|
32
|
+
@media (min-width: 48em) {
|
|
33
|
+
.rlg-metric-cell:nth-child(2n) {
|
|
34
|
+
border-left: var(--rlg-border, 1px solid var(--mantine-color-gray-2)) !important;
|
|
35
|
+
padding-left: 24px;
|
|
36
|
+
}
|
|
37
|
+
.rlg-metric-cell + .rlg-metric-cell {
|
|
38
|
+
border-top: none !important;
|
|
39
|
+
}
|
|
40
|
+
.rlg-metric-cell:nth-child(n + 3) {
|
|
41
|
+
border-top: var(--rlg-border, 1px solid var(--mantine-color-gray-2)) !important;
|
|
42
|
+
padding-top: 20px;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* md: four in a single row. Vertical on all but the first, and no horizontal
|
|
47
|
+
one is left. */
|
|
48
|
+
@media (min-width: 62em) {
|
|
49
|
+
.rlg-metric-cell:not(:first-child) {
|
|
50
|
+
border-left: var(--rlg-border, 1px solid var(--mantine-color-gray-2)) !important;
|
|
51
|
+
padding-left: 24px;
|
|
52
|
+
}
|
|
53
|
+
.rlg-metric-cell:nth-child(n + 3) {
|
|
54
|
+
border-top: none !important;
|
|
55
|
+
padding-top: 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Box, Card, Group, SimpleGrid } from '@mantine/core'
|
|
2
|
+
|
|
3
|
+
import './CommandCenter.css'
|
|
4
|
+
|
|
5
|
+
import { PageHeader } from './PageHeader'
|
|
6
|
+
import { Metric } from './Metric'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The Centro de Comando — the first screen of every RiLiGar dashboard.
|
|
10
|
+
*
|
|
11
|
+
* ONE NAME, IN THE USER'S LANGUAGE
|
|
12
|
+
*
|
|
13
|
+
* Each dashboard called its landing screen something different — "Visão geral",
|
|
14
|
+
* "Painel", "Command Center" — three names for the same screen, which forced
|
|
15
|
+
* anyone using two products to keep two translations in their head. None of the
|
|
16
|
+
* first two says what the screen DOES; "Centro de Comando" does: here is the
|
|
17
|
+
* picture of the account, and what needs action.
|
|
18
|
+
*
|
|
19
|
+
* It is in Portuguese because the rule is the reader, not the file: what a
|
|
20
|
+
* person reads stays in Portuguese, identifiers and comments stay in English
|
|
21
|
+
* (https://github.com/riligar/infrastructure/blob/prod/apps/website/docs/2026-09-10-idioma-do-codigo.md). The component, the file and the
|
|
22
|
+
* export keep the English name; only the rendered word changed.
|
|
23
|
+
*
|
|
24
|
+
* Five dashboards open with a GREETING ("Boa tarde, Ciro.") and the Hoster opens
|
|
25
|
+
* with the project id. Pinning the title here would erase that from all of them
|
|
26
|
+
* — trading something good that already exists for a consistency nobody asked
|
|
27
|
+
* for. The title is free; "Centro de Comando" is the DEFAULT and lives in the
|
|
28
|
+
* crumb, which is where you read "where am I" without losing the "hello".
|
|
29
|
+
*
|
|
30
|
+
* THE THREE LAWS THIS SCREEN CARRIES
|
|
31
|
+
*
|
|
32
|
+
* 1. It shows what NEEDS ATTENTION, not the inventory. With dozens of
|
|
33
|
+
* resources, listing them all turns this screen into a second list, and the
|
|
34
|
+
* information that brought someone here gets diluted among everything that
|
|
35
|
+
* is healthy. The list screens are the inventory, with search and filter.
|
|
36
|
+
*
|
|
37
|
+
* 2. A big number comes with a NOTE. A bare "99,4%" doesn't say over what
|
|
38
|
+
* period or across how many samples — and a dashboard that asserts without
|
|
39
|
+
* backing is advertising, not instrumentation. `Metric` is what carries that
|
|
40
|
+
* rule; here we only guarantee that the metrics sit at the top, together,
|
|
41
|
+
* separated by a hairline and without a frame.
|
|
42
|
+
*
|
|
43
|
+
* 3. One call, not N. The whole picture comes from a single endpoint. With N
|
|
44
|
+
* detail calls, each number would be computed at a different moment and the
|
|
45
|
+
* total wouldn't match the sum of the rows — the worst kind of error in a
|
|
46
|
+
* dashboard, because it looks right.
|
|
47
|
+
*
|
|
48
|
+
* WHAT THIS COMPONENT DOES, AND WHAT IT DOES NOT
|
|
49
|
+
*
|
|
50
|
+
* It is the FRAME: header, work-in-progress band, metrics band, and the body.
|
|
51
|
+
* It doesn't search data, it doesn't know what uptime is or what a charge is —
|
|
52
|
+
* each product passes its own metrics and its own body. That's what allows the
|
|
53
|
+
* same screen across seven products without the kit having to know any of them.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} product - The product name, for the breadcrumb ("RiLiGar Hoster").
|
|
56
|
+
* Becomes "RiLiGar Hoster · Centro de Comando".
|
|
57
|
+
* @param {string} title - The title. Defaults to "Centro de Comando"; pass the
|
|
58
|
+
* greeting, the project name, whatever the screen asks for.
|
|
59
|
+
* @param {string} subtitle - What this screen shows, in one sentence.
|
|
60
|
+
* @param {node} actions - The screen's primary button, if any. Just one —
|
|
61
|
+
* two primary buttons is none.
|
|
62
|
+
* @param {node} working - Work-in-progress band. Only shows up when there is
|
|
63
|
+
* something to say: a permanent band reading "0 in
|
|
64
|
+
* progress" would be noise in the most valuable spot
|
|
65
|
+
* on the screen.
|
|
66
|
+
* @param {array} metrics - `[{ label, value, note, delta? }]`. Up to four:
|
|
67
|
+
* THREE. The rule is in the component body.
|
|
68
|
+
* @param {node} children - The body — usually tabs with whatever needs action.
|
|
69
|
+
*/
|
|
70
|
+
export function CommandCenter({ product, title, subtitle, actions, working, metrics = [], children }) {
|
|
71
|
+
/*
|
|
72
|
+
* THREE METRICS. NOT FOUR.
|
|
73
|
+
*
|
|
74
|
+
* The seven dashboards converged on three on their own — only Monitors had
|
|
75
|
+
* four, and the fourth was "Ping médio", which the table right below
|
|
76
|
+
* already gave more precisely, in a column per monitor.
|
|
77
|
+
*
|
|
78
|
+
* Three is what you read at a glance; from the fourth on, the band stops
|
|
79
|
+
* being a picture and becomes a short list, something you scan instead of
|
|
80
|
+
* read. And at `md` the grid divides the width by the number of metrics:
|
|
81
|
+
* with four, each 64px number loses a quarter of its space and breaks on
|
|
82
|
+
* medium screens.
|
|
83
|
+
*
|
|
84
|
+
* The warning is a `console.warn` and not a silent truncation: dropping the
|
|
85
|
+
* fourth metric on our own would hide data someone chose to show. Whoever
|
|
86
|
+
* needs a fourth number decides what goes — not the component.
|
|
87
|
+
*/
|
|
88
|
+
if (metrics.length > 3 && typeof console !== 'undefined') {
|
|
89
|
+
console.warn(
|
|
90
|
+
`[dashboard-kit] <CommandCenter> received ${metrics.length} metrics; the standard is 3. From the fourth on, the strip becomes a list, and the 64px number breaks on medium screens.`,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<>
|
|
96
|
+
<PageHeader
|
|
97
|
+
crumb={product ? `${product} · Centro de Comando` : 'Centro de Comando'}
|
|
98
|
+
title={title ?? 'Centro de Comando'}
|
|
99
|
+
subtitle={subtitle}
|
|
100
|
+
actions={actions}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
{/*
|
|
104
|
+
WORK IN PROGRESS, at the top and before everything else.
|
|
105
|
+
|
|
106
|
+
Without this band, someone who kicked off a long operation on
|
|
107
|
+
another screen and came back here has no way to tell "the system
|
|
108
|
+
is working" from "the system stopped" — and the two look identical
|
|
109
|
+
when nothing moves.
|
|
110
|
+
*/}
|
|
111
|
+
{working && (
|
|
112
|
+
<Card
|
|
113
|
+
p={0}
|
|
114
|
+
mb={20}
|
|
115
|
+
>
|
|
116
|
+
<Group
|
|
117
|
+
px={{ base: 16, sm: 24 }}
|
|
118
|
+
py={14}
|
|
119
|
+
gap={12}
|
|
120
|
+
>
|
|
121
|
+
{working}
|
|
122
|
+
</Group>
|
|
123
|
+
</Card>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{metrics.length > 0 && (
|
|
127
|
+
<Card
|
|
128
|
+
p={0}
|
|
129
|
+
mb={20}
|
|
130
|
+
>
|
|
131
|
+
{/*
|
|
132
|
+
Separated by a hairline, with no frame per metric. Each one
|
|
133
|
+
in its own card would create four objects competing for
|
|
134
|
+
attention where there is a single subject: the state of the
|
|
135
|
+
account.
|
|
136
|
+
*/}
|
|
137
|
+
<SimpleGrid
|
|
138
|
+
cols={{ base: 1, sm: Math.min(metrics.length, 2), md: Math.min(metrics.length, 3) }}
|
|
139
|
+
spacing={0}
|
|
140
|
+
px={{ base: 16, sm: 24 }}
|
|
141
|
+
py={20}
|
|
142
|
+
>
|
|
143
|
+
{metrics.map(m => (
|
|
144
|
+
<Box
|
|
145
|
+
key={m.label}
|
|
146
|
+
miw={0}
|
|
147
|
+
className="rlg-metric-cell"
|
|
148
|
+
>
|
|
149
|
+
{/*
|
|
150
|
+
`xl` (up to 64px) is the default HERE, and
|
|
151
|
+
not on the list screens.
|
|
152
|
+
|
|
153
|
+
This is the account-picture screen: the
|
|
154
|
+
number IS the content, not a datum sitting
|
|
155
|
+
next to something else. On the inner
|
|
156
|
+
screens the metric accompanies a table and
|
|
157
|
+
competing with it would be noise; here it
|
|
158
|
+
is the subject, and the size says so
|
|
159
|
+
before any word does.
|
|
160
|
+
|
|
161
|
+
64px is what the six dashboards were
|
|
162
|
+
already using before this component
|
|
163
|
+
existed. Dropping to 36 would have been a
|
|
164
|
+
44% loss on the very element the screen
|
|
165
|
+
exists to show — and it would have come
|
|
166
|
+
from me, not from a reason.
|
|
167
|
+
|
|
168
|
+
Anyone who needs a different size passes
|
|
169
|
+
`size` on the metric itself: the spread
|
|
170
|
+
comes afterwards and wins.
|
|
171
|
+
*/}
|
|
172
|
+
<Metric
|
|
173
|
+
size="xl"
|
|
174
|
+
{...m}
|
|
175
|
+
/>
|
|
176
|
+
</Box>
|
|
177
|
+
))}
|
|
178
|
+
</SimpleGrid>
|
|
179
|
+
</Card>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{children}
|
|
183
|
+
</>
|
|
184
|
+
)
|
|
185
|
+
}
|