@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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A server-backed list, one page at a time.
|
|
5
|
+
*
|
|
6
|
+
* THIS HOOK IS THE GENERALIZATION OF `useMonitorsPaginated`, and it exists
|
|
7
|
+
* because the four decisions below cannot be deduced by reading the code — they
|
|
8
|
+
* were found by measuring. Every screen that paginated on its own would have to
|
|
9
|
+
* rediscover all four, and the first three fail silently.
|
|
10
|
+
*
|
|
11
|
+
* 1. SEARCH, FILTER AND ORDER GO TO THE SERVER. Without that, paginating BREAKS
|
|
12
|
+
* search: a client-side search over the loaded page says "no results" for an
|
|
13
|
+
* item that exists on page 3 — worse than having no search at all, because it
|
|
14
|
+
* looks like an answer. Changing any of the three returns to the first page;
|
|
15
|
+
* keeping the offset would show an empty list with "23 results" at the top.
|
|
16
|
+
*
|
|
17
|
+
* 2. THE RELOAD BRINGS BACK WHAT IS ALREADY ON SCREEN, not just the first page.
|
|
18
|
+
* Someone who scrolled to the fourth page and watches the periodic cycle
|
|
19
|
+
* return 50 rows has lost both their place and their work. `offset: 0` with
|
|
20
|
+
* the accumulated size is a larger request in bytes, and the only one that
|
|
21
|
+
* preserves position.
|
|
22
|
+
*
|
|
23
|
+
* 3. `loadingMore` IS SEPARATE FROM `loading`. The first keeps the table on
|
|
24
|
+
* screen with a loading footer; the second swaps everything for a skeleton.
|
|
25
|
+
* A single flag would make the whole list flash on every new page.
|
|
26
|
+
*
|
|
27
|
+
* 4. THE IN-FLIGHT RESPONSE IS INVALIDATED BY GENERATION, not by
|
|
28
|
+
* AbortController. Typing fast fires N searches, and the slow response to the
|
|
29
|
+
* first can arrive after the fifth — the classic race. A counter that only
|
|
30
|
+
* accepts the response of the current generation solves it without requiring
|
|
31
|
+
* the server to understand cancellation.
|
|
32
|
+
*
|
|
33
|
+
* THE SYNCHRONOUS LOCK (`fetchingMore`) deserves its own note: `loadingMore` is
|
|
34
|
+
* React state and only takes effect after render, but the `IntersectionObserver`
|
|
35
|
+
* in `MoreResults` fires several times within the same frame while the scroll
|
|
36
|
+
* runs. Measured: every page was requested TWICE — seven requests for four
|
|
37
|
+
* pages. A `ref` changes at the very instant of the call, so the second attempt
|
|
38
|
+
* in the same frame already finds the lock closed.
|
|
39
|
+
*
|
|
40
|
+
* @param {function} fetchPage - `({ limit, offset }) => Promise<{items, page}>`.
|
|
41
|
+
* The contract envelope; see `collection()` in
|
|
42
|
+
* `@ciromaciel/contract`.
|
|
43
|
+
* @param {array} deps - What, when changed, returns to the first page:
|
|
44
|
+
* search, filter, order. Same as a `useEffect`
|
|
45
|
+
* dependency array.
|
|
46
|
+
* @param {number} size - Items per page.
|
|
47
|
+
* @param {number} intervalMs - Periodic reload. `0` disables it.
|
|
48
|
+
* @param {function} pace - `(items) => ms`, to speed up the reload while
|
|
49
|
+
* work is in progress. Monitors returns 4s while
|
|
50
|
+
* some row is auditing and 30s at rest: an audit
|
|
51
|
+
* lasts about a minute, and without this the row
|
|
52
|
+
* spends half a minute showing the stale state
|
|
53
|
+
* AFTER it has finished.
|
|
54
|
+
*/
|
|
55
|
+
export function usePaginatedList({ fetchPage, deps = [], size = 50, intervalMs = 0, pace } = {}) {
|
|
56
|
+
const [items, setItems] = useState([])
|
|
57
|
+
const [loading, setLoading] = useState(true)
|
|
58
|
+
const [loadingMore, setLoadingMore] = useState(false)
|
|
59
|
+
const [error, setError] = useState(null)
|
|
60
|
+
const [total, setTotal] = useState(0)
|
|
61
|
+
const [hasMore, setHasMore] = useState(false)
|
|
62
|
+
/*
|
|
63
|
+
* THE REST OF THE BODY, outside `items` and `page`.
|
|
64
|
+
*
|
|
65
|
+
* The contract puts at the ROOT whatever is context about the set rather
|
|
66
|
+
* than pagination: Monitors' per-state `counts`, Hoster's `activeDeployId`,
|
|
67
|
+
* Storage's blob `cursor`. Without exposing that, every panel needing one of
|
|
68
|
+
* them would have to keep its own copy of this hook — which is exactly the
|
|
69
|
+
* situation it came to undo.
|
|
70
|
+
*/
|
|
71
|
+
const [extra, setExtra] = useState(null)
|
|
72
|
+
|
|
73
|
+
const alive = useRef(true)
|
|
74
|
+
const generation = useRef(0)
|
|
75
|
+
const loaded = useRef(0)
|
|
76
|
+
const fetchingMore = useRef(false)
|
|
77
|
+
|
|
78
|
+
// `fetchPage` is usually a fresh arrow on every render. Held in a ref, it
|
|
79
|
+
// stays out of the dependencies — otherwise the effect would restart on
|
|
80
|
+
// every render and the list would fetch the first page forever.
|
|
81
|
+
const fetchPageRef = useRef(fetchPage)
|
|
82
|
+
fetchPageRef.current = fetchPage
|
|
83
|
+
|
|
84
|
+
const requestPage = useCallback(
|
|
85
|
+
async ({ offset, limit = size, mode }) => {
|
|
86
|
+
const mine = ++generation.current
|
|
87
|
+
|
|
88
|
+
if (mode === 'more') setLoadingMore(true)
|
|
89
|
+
else if (mode === 'replace') setLoading(true)
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const r = await fetchPageRef.current({ limit, offset })
|
|
93
|
+
if (!alive.current || mine !== generation.current) return
|
|
94
|
+
|
|
95
|
+
const list = r?.items ?? []
|
|
96
|
+
const append = offset > 0 && mode === 'more'
|
|
97
|
+
|
|
98
|
+
setItems(previous => (append ? [...previous, ...list] : list))
|
|
99
|
+
loaded.current = append ? loaded.current + list.length : list.length
|
|
100
|
+
|
|
101
|
+
setTotal(r?.page?.total ?? list.length)
|
|
102
|
+
setHasMore(Boolean(r?.page?.hasMore))
|
|
103
|
+
|
|
104
|
+
// Anything that is neither `items` nor `page` is context about the set.
|
|
105
|
+
const { items: _i, page: _p, ...rest } = r ?? {}
|
|
106
|
+
setExtra(Object.keys(rest).length ? rest : null)
|
|
107
|
+
|
|
108
|
+
setError(null)
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (alive.current && mine === generation.current) setError(e?.message ?? String(e))
|
|
111
|
+
} finally {
|
|
112
|
+
// The lock ALWAYS opens, including when the response is discarded by
|
|
113
|
+
// generation — stuck, infinite scroll simply stops working.
|
|
114
|
+
if (mode === 'more') fetchingMore.current = false
|
|
115
|
+
if (alive.current && mine === generation.current) {
|
|
116
|
+
setLoading(false)
|
|
117
|
+
setLoadingMore(false)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
[size],
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
const loadMore = useCallback(() => {
|
|
125
|
+
if (fetchingMore.current || loading || !hasMore) return
|
|
126
|
+
fetchingMore.current = true
|
|
127
|
+
requestPage({ offset: loaded.current, mode: 'more' })
|
|
128
|
+
}, [requestPage, loading, hasMore])
|
|
129
|
+
|
|
130
|
+
/** Reloads what is on screen, preserving the scroll position (decision 2). */
|
|
131
|
+
const reload = useCallback(() => requestPage({ offset: 0, limit: Math.max(loaded.current, size), mode: 'silent' }), [requestPage, size])
|
|
132
|
+
|
|
133
|
+
/*
|
|
134
|
+
* The trigger goes through a `setTimeout(0)` rather than straight into the
|
|
135
|
+
* effect body. `requestPage` calls `setLoading(true)` synchronously, and a
|
|
136
|
+
* setState in the effect body chains an extra render on every keystroke in
|
|
137
|
+
* the search box. Scheduled, the loading state lands after the commit — and
|
|
138
|
+
* the `clearTimeout` cancels the search for a term the user has already
|
|
139
|
+
* replaced, before it even leaves.
|
|
140
|
+
*/
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
alive.current = true
|
|
143
|
+
loaded.current = 0
|
|
144
|
+
fetchingMore.current = false
|
|
145
|
+
|
|
146
|
+
/*
|
|
147
|
+
* THE GENERATION IS INCREMENTED HERE, and not only inside `requestPage`.
|
|
148
|
+
*
|
|
149
|
+
* The `setTimeout(0)` below defers the request until after the commit.
|
|
150
|
+
* Without this increment, changing the search during an in-flight
|
|
151
|
+
* response played out like this: the cleanup cancelled the new
|
|
152
|
+
* generation's timer BEFORE it existed, the slow response from the old
|
|
153
|
+
* generation arrived, found the counter unchanged — and won. The list
|
|
154
|
+
* ended up showing the result for the term the user had already
|
|
155
|
+
* replaced.
|
|
156
|
+
*
|
|
157
|
+
* Invalidating at the moment of the CHANGE, not at the moment of the
|
|
158
|
+
* request, is what closes that window: any earlier response is already
|
|
159
|
+
* born stale.
|
|
160
|
+
*/
|
|
161
|
+
generation.current++
|
|
162
|
+
|
|
163
|
+
const t = setTimeout(() => requestPage({ offset: 0, mode: 'replace' }), 0)
|
|
164
|
+
return () => {
|
|
165
|
+
clearTimeout(t)
|
|
166
|
+
alive.current = false
|
|
167
|
+
}
|
|
168
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
+
}, [requestPage, ...deps])
|
|
170
|
+
|
|
171
|
+
/*
|
|
172
|
+
* THE PACE FOLLOWS WHAT IS HAPPENING.
|
|
173
|
+
*
|
|
174
|
+
* A fixed interval is right at rest, but wrong while work is in progress:
|
|
175
|
+
* during a long-running operation the row can spend half a minute showing
|
|
176
|
+
* the stale state AFTER it has finished. `pace` lets the product decide —
|
|
177
|
+
* and it returns to the resting interval on its own.
|
|
178
|
+
*/
|
|
179
|
+
const cadence = pace?.(items) || intervalMs
|
|
180
|
+
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (!cadence) return
|
|
183
|
+
const t = setInterval(reload, cadence)
|
|
184
|
+
return () => clearInterval(t)
|
|
185
|
+
}, [reload, cadence])
|
|
186
|
+
|
|
187
|
+
return { items, loading, loadingMore, error, total, hasMore, extra, loadMore, reload }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Waits for the user to stop typing before going to the network.
|
|
192
|
+
*
|
|
193
|
+
* A client-side search does not need this — it is not worth delaying the
|
|
194
|
+
* response to the keyboard in order to filter an in-memory array. A SERVER
|
|
195
|
+
* search does: every keystroke would be a request, and 300 ms is the interval at
|
|
196
|
+
* which the person typing perceives no wait but the server receives one search
|
|
197
|
+
* instead of ten.
|
|
198
|
+
*/
|
|
199
|
+
export function useDebounce(value, ms = 300) {
|
|
200
|
+
const [delayed, setDelayed] = useState(value)
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
const t = setTimeout(() => setDelayed(value), ms)
|
|
203
|
+
return () => clearTimeout(t)
|
|
204
|
+
}, [value, ms])
|
|
205
|
+
return delayed
|
|
206
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// @ciromaciel/dashboard-kit — what the seven dashboards share
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// The websites already had `@ciromaciel/components-kit`. The DASHBOARDS had
|
|
6
|
+
// nothing: each one kept its own copy of the table, the loading state, the
|
|
7
|
+
// empty state and the destructive confirmation — 4,624 duplicated lines across
|
|
8
|
+
// four of them, plus seven copies of `AuthLoader`.
|
|
9
|
+
//
|
|
10
|
+
// THE ADMISSION CRITERION
|
|
11
|
+
//
|
|
12
|
+
// What goes in here is what is THE SAME across the dashboards. What belongs to
|
|
13
|
+
// a product stays out: the Monitors status light, its boot skeleton, the
|
|
14
|
+
// Payments currency rule. The dividing line is simple — if a dashboard had to
|
|
15
|
+
// change the component to serve its product, the component isn't everyone's
|
|
16
|
+
// yet.
|
|
17
|
+
//
|
|
18
|
+
// WHAT THE EXTRACTION FOUND
|
|
19
|
+
//
|
|
20
|
+
// Four components were byte-for-byte identical across four dashboards. Three
|
|
21
|
+
// had two variants, and in each case the divergence was EVOLUTION, not drift:
|
|
22
|
+
// the Hoster added `quietSelection` to the table with the reasoning written
|
|
23
|
+
// down, Monitors fixed a `<div>` inside a `<p>` that the browser was tearing
|
|
24
|
+
// apart. The kit kept the superset in each case — no dashboard loses behavior
|
|
25
|
+
// by adopting it.
|
|
26
|
+
//
|
|
27
|
+
// `Wordmark` was the one genuine exception: the four copies diverged because
|
|
28
|
+
// the product name was a literal in the JSX, and changing one word forced you
|
|
29
|
+
// to maintain the whole file. Here it comes in as a prop.
|
|
30
|
+
//
|
|
31
|
+
// WHY THERE IS NO BUILD
|
|
32
|
+
//
|
|
33
|
+
// The package publishes source. All seven dashboards are Vite, which compiles
|
|
34
|
+
// JSX from a dependency without ceremony — and a build step would only exist to
|
|
35
|
+
// produce what the consumer already knows how to produce. One less moving part
|
|
36
|
+
// to break.
|
|
37
|
+
|
|
38
|
+
import './tokens.css'
|
|
39
|
+
|
|
40
|
+
export { DataTable } from './components/DataTable'
|
|
41
|
+
export { templateFor, ROW_PADDING, HEAD_PADDING } from './components/table-grid'
|
|
42
|
+
export { TableSkeleton } from './components/TableSkeleton'
|
|
43
|
+
export { EmptyState } from './components/EmptyState'
|
|
44
|
+
export { SectionCard, Field, FieldStacked, CardActions } from './components/SectionCard'
|
|
45
|
+
export { PageHeader } from './components/PageHeader'
|
|
46
|
+
|
|
47
|
+
// ── The billing banner ───────────────────────────────────────────────────
|
|
48
|
+
// Freeze, do not disconnect: it warns, it never blocks. It takes the state
|
|
49
|
+
// from `usePlanNotice(product)` in `@ciromaciel/payments-react` and paints it —
|
|
50
|
+
// the kit does not know the Payments' address.
|
|
51
|
+
export { PlanBanner } from './components/PlanBanner'
|
|
52
|
+
export { HoldToConfirmIcon, HoldToConfirmButton } from './components/HoldToConfirm'
|
|
53
|
+
|
|
54
|
+
// ── The destructive modal ────────────────────────────────────────────────
|
|
55
|
+
// The top of the escalation: reversible → hold → this. An audit found fifteen
|
|
56
|
+
// of them across the seven panels and no two alike, so the shape lives here
|
|
57
|
+
// now. `design-system/10-modal-de-exclusao.md` says why.
|
|
58
|
+
export { DeleteModal, deleteTitleScale } from './components/DeleteModal'
|
|
59
|
+
export { ZenLabel } from './components/ZenLabel'
|
|
60
|
+
export { notify } from './components/notify'
|
|
61
|
+
export { NOTIFICATIONS_PROPS, NOTIFY_DESCRIPTION_CSS, notificationTheme } from './components/notify-theme'
|
|
62
|
+
export { Wordmark } from './components/Wordmark'
|
|
63
|
+
export { SupportModal, HealthRow, useHealthCheck } from './components/SupportModal'
|
|
64
|
+
export { AuthLoader } from './components/AuthLoader'
|
|
65
|
+
export { AuthScreen } from './components/AuthScreen'
|
|
66
|
+
export { Terminal, TerminalOutput } from './components/Terminal'
|
|
67
|
+
export { useExclusiveModal } from './hooks/useExclusiveModal'
|
|
68
|
+
|
|
69
|
+
// ── The sidebar's pieces ─────────────────────────────────────────────────
|
|
70
|
+
// Extracted after measuring: `MenuItem` was byte-identical in seven panels
|
|
71
|
+
// (the seventh differing only by quote style), and `NavItem`/`GroupLabel`
|
|
72
|
+
// differed by one optional prop each.
|
|
73
|
+
export { MenuItem } from './components/MenuItem'
|
|
74
|
+
export { NavItem } from './components/NavItem'
|
|
75
|
+
export { GroupLabel } from './components/GroupLabel'
|
|
76
|
+
export { ResourceItem } from './components/ResourceItem'
|
|
77
|
+
export { ActiveResource } from './components/ActiveResource'
|
|
78
|
+
export { ListSkeleton } from './components/ListSkeleton'
|
|
79
|
+
export { DashboardFrame, SIDEBAR_WIDTH } from './components/DashboardFrame'
|
|
80
|
+
export { SidebarShell } from './components/SidebarShell'
|
|
81
|
+
export { AUTH_LABELS, AUTH_TEXTS } from './auth-labels'
|
|
82
|
+
export { CardSkeleton } from './components/CardSkeleton'
|
|
83
|
+
export { MoreResults } from './components/MoreResults'
|
|
84
|
+
export { TableToolbar } from './components/TableToolbar'
|
|
85
|
+
|
|
86
|
+
// ── The paginated list ───────────────────────────────────────────────────
|
|
87
|
+
// The hook and the footer travel together: `usePaginatedList` holds the four
|
|
88
|
+
// decisions you can't infer from reading the code, and `MoreResults` is the
|
|
89
|
+
// only surface that triggers them. See the hook's header.
|
|
90
|
+
export { usePaginatedList, useDebounce } from './hooks/usePaginatedList'
|
|
91
|
+
|
|
92
|
+
// ── The screen ───────────────────────────────────────────────────────────
|
|
93
|
+
// `Metric` and `CommandCenter` aren't just components: they are the pattern for
|
|
94
|
+
// the first screen. Each dashboard used to call its own something different —
|
|
95
|
+
// "Visão geral", "Painel", "Agendamentos" — and none of those names says what
|
|
96
|
+
// the screen does. See the header of `CommandCenter.jsx` for the three laws it
|
|
97
|
+
// carries.
|
|
98
|
+
export { Metric } from './components/Metric'
|
|
99
|
+
export { CommandCenter } from './components/CommandCenter'
|
package/src/tokens.css
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* THE TOKENS THE KIT USES AND CANNOT ASSUME EXIST.
|
|
3
|
+
*
|
|
4
|
+
* Six components in this package draw a hairline with `var(--rlg-border)` — the
|
|
5
|
+
* table, its header, the skeleton, the search, the pagination footer and the
|
|
6
|
+
* Command Center's metrics band. The variable was born in each dashboard's CSS,
|
|
7
|
+
* and six of the seven defined it. The seventh (the Auth) didn't — and there the
|
|
8
|
+
* migrated table would have rendered WITH NO HAIRLINE AT ALL: header, rows and
|
|
9
|
+
* search flush against each other, with no build error and no console warning,
|
|
10
|
+
* because a `var()` with no value and no fallback simply doesn't apply the
|
|
11
|
+
* property.
|
|
12
|
+
*
|
|
13
|
+
* A package that requires a token to draw correctly has to ship that token's
|
|
14
|
+
* default value. `@property` doesn't work here (the value is a border
|
|
15
|
+
* shorthand), and repeating the fallback across the 13 call sites would be
|
|
16
|
+
* thirteen places to diverge.
|
|
17
|
+
*
|
|
18
|
+
* This is a FLOOR, not a lock: any dashboard's `:root` beats this declaration by
|
|
19
|
+
* cascade order — the product's CSS is imported afterwards.
|
|
20
|
+
*/
|
|
21
|
+
:root {
|
|
22
|
+
--rlg-border: 1px solid var(--mantine-color-gray-2);
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* O filete dentro do Terminal, sobre fundo escuro. Nasceu literal em cinco
|
|
26
|
+
* cópias e virou token no Hoster; o piso fica aqui para que o componente
|
|
27
|
+
* não dependa de cada painel declarar a variável.
|
|
28
|
+
*/
|
|
29
|
+
--rlg-terminal-rule: rgba(255, 255, 255, 0.08);
|
|
30
|
+
|
|
31
|
+
/*
|
|
32
|
+
* The system's only green, used by the SupportModal's status light.
|
|
33
|
+
*
|
|
34
|
+
* Five panels declared it with this same value; Auth and Storage did not —
|
|
35
|
+
* and those are exactly the two that did not yet have the modal. Without
|
|
36
|
+
* this floor the "No ar" dot would render WITH NO COLOUR at all in those
|
|
37
|
+
* two: a `var()` with no value and no fallback does not apply the property,
|
|
38
|
+
* and there is no build error and no console warning. It is the same defect
|
|
39
|
+
* as the hairline described above.
|
|
40
|
+
*
|
|
41
|
+
* It lives outside the gray ramp because this is not the success of an
|
|
42
|
+
* ACTION (that is gray.9 + IconCheck): it is the measured state of an
|
|
43
|
+
* external system. The rule that keeps the exception honest is that the
|
|
44
|
+
* colour never appears without the word beside it.
|
|
45
|
+
*/
|
|
46
|
+
--rlg-status-up: #16a34a;
|
|
47
|
+
}
|