@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.
Files changed (38) hide show
  1. package/README.md +40 -0
  2. package/package.json +46 -0
  3. package/src/auth-labels.js +77 -0
  4. package/src/components/ActiveResource.jsx +68 -0
  5. package/src/components/AuthLoader.jsx +66 -0
  6. package/src/components/AuthScreen.jsx +32 -0
  7. package/src/components/CardSkeleton.jsx +82 -0
  8. package/src/components/CommandCenter.css +57 -0
  9. package/src/components/CommandCenter.jsx +185 -0
  10. package/src/components/DashboardFrame.jsx +97 -0
  11. package/src/components/DataTable.jsx +153 -0
  12. package/src/components/DeleteModal.jsx +341 -0
  13. package/src/components/EmptyState.jsx +55 -0
  14. package/src/components/GroupLabel.jsx +28 -0
  15. package/src/components/HoldToConfirm.jsx +151 -0
  16. package/src/components/ListSkeleton.jsx +55 -0
  17. package/src/components/MenuItem.jsx +56 -0
  18. package/src/components/Metric.jsx +178 -0
  19. package/src/components/MoreResults.jsx +137 -0
  20. package/src/components/NavItem.jsx +57 -0
  21. package/src/components/PageHeader.jsx +124 -0
  22. package/src/components/PlanBanner.jsx +88 -0
  23. package/src/components/ResourceItem.jsx +93 -0
  24. package/src/components/SectionCard.jsx +212 -0
  25. package/src/components/SidebarShell.jsx +271 -0
  26. package/src/components/SupportModal.jsx +269 -0
  27. package/src/components/TableSkeleton.jsx +43 -0
  28. package/src/components/TableToolbar.jsx +136 -0
  29. package/src/components/Terminal.jsx +217 -0
  30. package/src/components/Wordmark.jsx +52 -0
  31. package/src/components/ZenLabel.jsx +30 -0
  32. package/src/components/notify-theme.js +103 -0
  33. package/src/components/notify.js +65 -0
  34. package/src/components/table-grid.js +27 -0
  35. package/src/hooks/useExclusiveModal.js +49 -0
  36. package/src/hooks/usePaginatedList.js +206 -0
  37. package/src/index.js +99 -0
  38. 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
+ }