basalt-ui 1.7.0 → 1.8.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/CHANGELOG.md +7 -0
- package/agent/skills/basalt-design/SKILL.md +5 -2
- package/dist/dashboard/stat-card.d.ts +4 -2
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +6 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +1 -1
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/dashboard/stat-card.test.tsx +81 -0
- package/src/dashboard/stat-card.tsx +18 -4
- package/src/guard/check-source.test.ts +2 -1
- package/src/guard/index.ts +6 -3
- package/src/theme/contrast.test.ts +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.8.0](https://github.com/jkrumm/basalt-ui/compare/v1.7.0...v1.8.0) (2026-08-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* give the stat card's threshold rail a good tone ([49e795f](https://github.com/jkrumm/basalt-ui/commit/49e795f2f0da85334953bf6280ca108c730d532c))
|
|
7
|
+
|
|
1
8
|
# [1.7.0](https://github.com/jkrumm/basalt-ui/compare/v1.6.0...v1.7.0) (2026-08-02)
|
|
2
9
|
|
|
3
10
|
|
|
@@ -133,8 +133,11 @@ structure, the accent only points.
|
|
|
133
133
|
`mantine-shade-index` (a shade-pinned color — `c="yellow.7"` is one fixed swatch in both schemes;
|
|
134
134
|
use `VX.status.*` or the bare hue name), `raw-html-layout`, `inline-spacing`, `inline-display`.
|
|
135
135
|
Only the Mantine-free `src/charts/**` may use raw `<div>` (still with `VX.*` tokens).
|
|
136
|
-
- **Reach for the shipped composite before wrapping one.** A `StatCard`
|
|
137
|
-
`tone="warn" | "bad"` (an accent rail down its leading edge)
|
|
136
|
+
- **Reach for the shipped composite before wrapping one.** A `StatCard` carrying a threshold verdict
|
|
137
|
+
takes `tone="good" | "warn" | "bad"` (an accent rail down its leading edge). Omitting `tone` is not
|
|
138
|
+
the same as `"good"` — omitted means "fine, or nothing measured" and stays untinted, so `"good"` is
|
|
139
|
+
a positive assertion you opt into for a value that earned it (`0 min` downtime), never a default.
|
|
140
|
+
A chart goes in `ChartCard`. Wrapping
|
|
138
141
|
a shipped card in a hand-positioned `Box` to add a mark it already has is how an app grows a second
|
|
139
142
|
card idiom — one the guard cannot recognize as a card, and therefore cannot police. If a composite
|
|
140
143
|
genuinely can't express what you need, that is a gap to report, not to route around.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
/** A threshold verdict on the card's value. `undefined` is never tinted — see the module docblock. */
|
|
3
|
-
export type StatCardTone = 'warn' | 'bad';
|
|
3
|
+
export type StatCardTone = 'good' | 'warn' | 'bad';
|
|
4
4
|
export type StatCardProps = {
|
|
5
5
|
/** Mono uppercase micro-label (docs/DESIGN-SPEC.md §3: 12.5px, tracking 0.06em, faint). */
|
|
6
6
|
label: string;
|
|
@@ -16,7 +16,9 @@ export type StatCardProps = {
|
|
|
16
16
|
/** Optional header-right slot (e.g. a ghost "..." menu trigger) — consumer-owned. */
|
|
17
17
|
menu?: ReactNode;
|
|
18
18
|
/** Threshold verdict — draws an accent rail down the card's leading edge and announces itself to
|
|
19
|
-
* assistive tech.
|
|
19
|
+
* assistive tech. Omitting it is NOT `'good'`: omitted covers a reading that is fine AND one that
|
|
20
|
+
* is absent, and stays untinted so a card with nothing measured can never render green. Pass
|
|
21
|
+
* `'good'` only to assert a measured value that has earned the verdict. */
|
|
20
22
|
tone?: StatCardTone;
|
|
21
23
|
};
|
|
22
24
|
export declare function StatCard({ label, value, delta, deltaPeriod, sparkline, menu, tone, }: StatCardProps): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../src/dashboard/stat-card.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stat-card.d.ts","sourceRoot":"","sources":["../../src/dashboard/stat-card.tsx"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAItC,sGAAsG;AACtG,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAA;AAYlD,MAAM,MAAM,aAAa,GAAG;IAC1B,2FAA2F;IAC3F,KAAK,EAAE,MAAM,CAAA;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAA;IACb,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,oGAAoG;IACpG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;kGAC8F;IAC9F,SAAS,CAAC,EAAE,SAAS,CAAA;IACrB,qFAAqF;IACrF,IAAI,CAAC,EAAE,SAAS,CAAA;IAChB;;;+EAG2E;IAC3E,IAAI,CAAC,EAAE,YAAY,CAAA;CACpB,CAAA;AAED,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,KAAK,EACL,KAAK,EACL,WAAW,EACX,SAAS,EACT,IAAI,EACJ,IAAI,GACL,EAAE,aAAa,+BAyFf"}
|
|
@@ -4,6 +4,7 @@ import { DeltaBadge } from "./delta-badge.js";
|
|
|
4
4
|
import { VX } from "../tokens/index.js";
|
|
5
5
|
const TONE_RAIL_WIDTH = 3;
|
|
6
6
|
const TONE_LABEL = {
|
|
7
|
+
good: "Within the good threshold",
|
|
7
8
|
warn: "Past the warning threshold",
|
|
8
9
|
bad: "Past the severe threshold"
|
|
9
10
|
};
|
|
@@ -50,7 +51,11 @@ function StatCard({
|
|
|
50
51
|
insetBlock: 0,
|
|
51
52
|
insetInlineStart: 0,
|
|
52
53
|
width: TONE_RAIL_WIDTH,
|
|
53
|
-
|
|
54
|
+
// `StatCardTone`'s members are `VX.status` keys by construction, so the rail reads the
|
|
55
|
+
// per-scheme `--vx-status-*` solid directly — never a hex, so it re-resolves in both
|
|
56
|
+
// schemes and under a consumer's `derive` retune. Contrast against
|
|
57
|
+
// `--vx-surface-panel` is measured in `theme/contrast.test.ts`.
|
|
58
|
+
background: VX.status[tone]
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;AAoP9D,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,WAoBlC,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/guard/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAA;AAE7E,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,CAAA;AAoP9D,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB,EAAE,WAoBlC,CAAA;AAuTD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;gCA0BL,WAAW;;;;;;;gCAcX,WAAW;;;;;gCAMX,WAAW;;;;;;;gCAaX,WAAW;;;;;;;gCAQX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCASX,WAAW;;;;;;gCAOX,WAAW;;;;;;;gCASX,WAAW;;;;;;gCAMX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;gCAUX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCASX,WAAW;;;;;;;gCAUX,WAAW;;;;CAOmB,CAAA;AAUjD;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,GAAG,OAAO,EAAE,CA2PtF"}
|
package/dist/guard/index.js
CHANGED
|
@@ -78,7 +78,7 @@ const DEFAULT_GUARD_CONFIG = {
|
|
|
78
78
|
severity: {}
|
|
79
79
|
};
|
|
80
80
|
const GRACE_PERIOD_KINDS = {
|
|
81
|
-
"mantine-shade-index": "introduced 1.7.0 \u2014 promote to error in 1.
|
|
81
|
+
"mantine-shade-index": "introduced 1.7.0 \u2014 promote to error in 1.9.0"
|
|
82
82
|
};
|
|
83
83
|
function severityOf(kind, cfg) {
|
|
84
84
|
return cfg.severity?.[kind] ?? (kind in GRACE_PERIOD_KINDS ? "warn" : "error");
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# basalt-ui v1.
|
|
1
|
+
# basalt-ui v1.8.0
|
|
2
2
|
# Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
|
|
3
3
|
#
|
|
4
4
|
# basalt-ui is an opinionated framework for Mantine-based React apps.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
# ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
|
|
20
20
|
|
|
21
21
|
Package: basalt-ui
|
|
22
|
-
Version: 1.
|
|
22
|
+
Version: 1.8.0
|
|
23
23
|
|
|
24
24
|
## basalt-ui
|
|
25
25
|
Import: import { ... } from 'basalt-ui'
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `StatCard`'s threshold rail — the one behavior in this component that carries information rather
|
|
3
|
+
* than layout, and the one where a regression is silent: a wrong rail colour still renders a
|
|
4
|
+
* perfectly good-looking card.
|
|
5
|
+
*
|
|
6
|
+
* Two invariants, both asserted against real rendered markup:
|
|
7
|
+
*
|
|
8
|
+
* 1. EVERY tone states its verdict in TEXT. The rail is `aria-hidden` decoration; colour alone must
|
|
9
|
+
* never be the only carrier of a threshold (WCAG 1.4.1), so a `VisuallyHidden` string ships with
|
|
10
|
+
* it — for `good` no less than for `bad`.
|
|
11
|
+
* 2. OMISSION IS NOT `good`. `tone` undefined means "fine, OR nothing measured", and those two are
|
|
12
|
+
* the same untinted card on purpose. A card with no reading must not be able to render green by
|
|
13
|
+
* leaving the prop off, so the absence case asserts no rail and no verdict text at all — which a
|
|
14
|
+
* future `tone = tone ?? 'good'` style default would immediately fail.
|
|
15
|
+
*
|
|
16
|
+
* No jsdom is configured in this repo (see `theme/use-basalt-spacing.test.tsx`'s doc) —
|
|
17
|
+
* `renderToStaticMarkup` inside a real `MantineProvider` is enough here, since the rail is an inline
|
|
18
|
+
* style on server-rendered markup.
|
|
19
|
+
*/
|
|
20
|
+
import { MantineProvider } from '@mantine/core'
|
|
21
|
+
import { describe, expect, test } from 'bun:test'
|
|
22
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
23
|
+
import { StatCard } from './stat-card'
|
|
24
|
+
import type { StatCardTone } from './stat-card'
|
|
25
|
+
|
|
26
|
+
function render(tone?: StatCardTone) {
|
|
27
|
+
return renderToStaticMarkup(
|
|
28
|
+
<MantineProvider>
|
|
29
|
+
<StatCard label="Downtime · last 24h" value="0 min" tone={tone} />
|
|
30
|
+
</MantineProvider>,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TONES: { tone: StatCardTone; token: string; verdict: string }[] = [
|
|
35
|
+
{ tone: 'good', token: 'var(--vx-status-good)', verdict: 'Within the good threshold' },
|
|
36
|
+
{ tone: 'warn', token: 'var(--vx-status-warn)', verdict: 'Past the warning threshold' },
|
|
37
|
+
{ tone: 'bad', token: 'var(--vx-status-bad)', verdict: 'Past the severe threshold' },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
describe('every tone draws its rail from the per-scheme status token', () => {
|
|
41
|
+
for (const { tone, token } of TONES) {
|
|
42
|
+
test(tone, () => {
|
|
43
|
+
const markup = render(tone)
|
|
44
|
+
// A hex here would be one scheme's colour frozen into both — the rail has to re-resolve with
|
|
45
|
+
// the palette, including under a consumer's `derive` retune.
|
|
46
|
+
expect(markup).toContain(`background:${token}`)
|
|
47
|
+
expect(markup).toContain(`data-tone="${tone}"`)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('every tone also states its verdict in text', () => {
|
|
53
|
+
for (const { tone, verdict } of TONES) {
|
|
54
|
+
test(tone, () => {
|
|
55
|
+
expect(render(tone)).toContain(verdict)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('omitting tone is not "good" — it is untinted, and says nothing', () => {
|
|
61
|
+
const markup = render()
|
|
62
|
+
|
|
63
|
+
test('no rail is drawn', () => {
|
|
64
|
+
expect(markup).not.toContain('var(--vx-status-')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('the card carries no data-tone at all', () => {
|
|
68
|
+
// Part of the same contract, and the half a consumer can style against: `[data-tone]` must not
|
|
69
|
+
// match an unmarked card. A default of any kind — including a placeholder like `tone ?? 'none'`
|
|
70
|
+
// that draws no rail — would put the attribute back and break that selector silently.
|
|
71
|
+
expect(markup).not.toContain('data-tone')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('no verdict is announced', () => {
|
|
75
|
+
for (const { verdict } of TONES) expect(markup).not.toContain(verdict)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('the value still renders — the card is normal, just unmarked', () => {
|
|
79
|
+
expect(markup).toContain('0 min')
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
* the guard has no way to recognize as one. The rail is the largest signal available without
|
|
16
16
|
* touching the value's own type: full card height, overlaying the card's edge rather than adding to
|
|
17
17
|
* it, so layout is unchanged. `undefined` renders nothing at all — absence of a reading is neither a
|
|
18
|
-
* good one nor a bad one, and must never be tinted.
|
|
18
|
+
* good one nor a bad one, and must never be tinted. `'good'` is a deliberate positive verdict, never
|
|
19
|
+
* the meaning of omission: it exists for the threshold where the calm number IS the finding (zero
|
|
20
|
+
* downtime over the window), and a card that has measured nothing must not be able to reach green by
|
|
21
|
+
* leaving the prop off.
|
|
19
22
|
*
|
|
20
23
|
* @example
|
|
21
24
|
* import { StatCard } from 'basalt-ui'
|
|
@@ -31,6 +34,10 @@
|
|
|
31
34
|
* @example
|
|
32
35
|
* // Past a threshold — an accent rail down the leading edge, value untouched.
|
|
33
36
|
* <StatCard label="Downtime" value="29 min" tone="bad" />
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // The earned zero — a measured value worth asserting, not a card that merely has no reading.
|
|
40
|
+
* <StatCard label="Downtime · last 24h" value="0 min" tone="good" />
|
|
34
41
|
*/
|
|
35
42
|
import { Box, Card, Group, VisuallyHidden } from '@mantine/core'
|
|
36
43
|
import type { ReactNode } from 'react'
|
|
@@ -38,13 +45,14 @@ import { DeltaBadge } from './delta-badge'
|
|
|
38
45
|
import { VX } from '../tokens'
|
|
39
46
|
|
|
40
47
|
/** A threshold verdict on the card's value. `undefined` is never tinted — see the module docblock. */
|
|
41
|
-
export type StatCardTone = 'warn' | 'bad'
|
|
48
|
+
export type StatCardTone = 'good' | 'warn' | 'bad'
|
|
42
49
|
|
|
43
50
|
/** The rail's width, in px. Wide enough to read as a deliberate mark at a glance, narrow enough
|
|
44
51
|
* that it cannot be mistaken for the card's own edge. */
|
|
45
52
|
const TONE_RAIL_WIDTH = 3
|
|
46
53
|
|
|
47
54
|
const TONE_LABEL: Record<StatCardTone, string> = {
|
|
55
|
+
good: 'Within the good threshold',
|
|
48
56
|
warn: 'Past the warning threshold',
|
|
49
57
|
bad: 'Past the severe threshold',
|
|
50
58
|
}
|
|
@@ -64,7 +72,9 @@ export type StatCardProps = {
|
|
|
64
72
|
/** Optional header-right slot (e.g. a ghost "..." menu trigger) — consumer-owned. */
|
|
65
73
|
menu?: ReactNode
|
|
66
74
|
/** Threshold verdict — draws an accent rail down the card's leading edge and announces itself to
|
|
67
|
-
* assistive tech.
|
|
75
|
+
* assistive tech. Omitting it is NOT `'good'`: omitted covers a reading that is fine AND one that
|
|
76
|
+
* is absent, and stays untinted so a card with nothing measured can never render green. Pass
|
|
77
|
+
* `'good'` only to assert a measured value that has earned the verdict. */
|
|
68
78
|
tone?: StatCardTone
|
|
69
79
|
}
|
|
70
80
|
|
|
@@ -111,7 +121,11 @@ export function StatCard({
|
|
|
111
121
|
insetBlock: 0,
|
|
112
122
|
insetInlineStart: 0,
|
|
113
123
|
width: TONE_RAIL_WIDTH,
|
|
114
|
-
|
|
124
|
+
// `StatCardTone`'s members are `VX.status` keys by construction, so the rail reads the
|
|
125
|
+
// per-scheme `--vx-status-*` solid directly — never a hex, so it re-resolves in both
|
|
126
|
+
// schemes and under a consumer's `derive` retune. Contrast against
|
|
127
|
+
// `--vx-surface-panel` is measured in `theme/contrast.test.ts`.
|
|
128
|
+
background: VX.status[tone],
|
|
115
129
|
}}
|
|
116
130
|
/>
|
|
117
131
|
</>
|
|
@@ -209,7 +209,8 @@ describe('mantine-shade-index', () => {
|
|
|
209
209
|
})
|
|
210
210
|
|
|
211
211
|
// The grace-minor doctrine (package CLAUDE.md): a kind that rejects previously-passing code
|
|
212
|
-
// lands as `warn` for one minor. Deleting its GRACE_PERIOD_KINDS entry
|
|
212
|
+
// lands as `warn` for one minor. Deleting its GRACE_PERIOD_KINDS entry flips this — scheduled
|
|
213
|
+
// for 1.9.0, deferred one minor because 1.8.0 shipped the same day as 1.7.0 (see the entry).
|
|
213
214
|
it('lands as a warning, not an error, for its grace minor', () => {
|
|
214
215
|
const f = find(`<Text c="yellow.7" />`)
|
|
215
216
|
expect(f.find((x) => x.kind === 'mantine-shade-index')?.severity).toBe('warn')
|
package/src/guard/index.ts
CHANGED
|
@@ -282,8 +282,11 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
|
282
282
|
* promotion, and belongs in its own commit so the changelog says enforcement got stricter.
|
|
283
283
|
*
|
|
284
284
|
* `mantine-shade-index` landed in 1.7.0 and is the sole entry — it rejects `c="yellow.7"`, which
|
|
285
|
-
* previously passed, and the one real consumer has several.
|
|
286
|
-
*
|
|
285
|
+
* previously passed, and the one real consumer has several. Its promotion is **deferred to 1.9.0**:
|
|
286
|
+
* 1.8.0 shipped the same day as 1.7.0, so promoting on the next minor number would have given the
|
|
287
|
+
* consumer roughly no runway at all — which is the exact failure the grace minor exists to prevent,
|
|
288
|
+
* and the runway is measured in consumer upgrades, not in version numbers. Promote it by deleting
|
|
289
|
+
* the entry (its own commit).
|
|
287
290
|
*
|
|
288
291
|
* @example
|
|
289
292
|
* const GRACE: Partial<Record<GuardKind, string>> = {
|
|
@@ -291,7 +294,7 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
|
291
294
|
* }
|
|
292
295
|
*/
|
|
293
296
|
const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
|
|
294
|
-
'mantine-shade-index': 'introduced 1.7.0 — promote to error in 1.
|
|
297
|
+
'mantine-shade-index': 'introduced 1.7.0 — promote to error in 1.9.0',
|
|
295
298
|
}
|
|
296
299
|
|
|
297
300
|
/** A kind's effective severity: consumer override first, then the grace table, then `error`. */
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { DEFAULT_THEME, getPrimaryShade, mergeMantineTheme } from '@mantine/core'
|
|
25
25
|
import type { MantineTheme } from '@mantine/core'
|
|
26
26
|
import { describe, expect, test } from 'bun:test'
|
|
27
|
-
import { ACCENT, FILL, SURFACE } from '../tokens/palette'
|
|
27
|
+
import { ACCENT, FILL, STATUS, SURFACE } from '../tokens/palette'
|
|
28
28
|
import { baseTheme, cssVariablesResolver } from './index'
|
|
29
29
|
|
|
30
30
|
const theme: MantineTheme = mergeMantineTheme(DEFAULT_THEME, baseTheme)
|
|
@@ -324,6 +324,23 @@ describe('doctrine inversion #1 reaches every default-variant CONTROL, not just
|
|
|
324
324
|
})
|
|
325
325
|
})
|
|
326
326
|
|
|
327
|
+
describe("the StatCard tone rail reads against the panel it's drawn on", () => {
|
|
328
|
+
// The rail (`dashboard/stat-card.tsx`) is a 3px non-text mark carrying a threshold verdict, so it
|
|
329
|
+
// takes WCAG's 3:1 non-text floor against `--vx-surface-panel` — the card background it overlays,
|
|
330
|
+
// not the page — in BOTH schemes. Worth measuring rather than eyeballing for the same reason the
|
|
331
|
+
// fill band is: the status solids are DERIVED, so retuning the generator could sink one of them
|
|
332
|
+
// into the panel it sits on without anything else failing.
|
|
333
|
+
const TONES = ['good', 'warn', 'bad'] as const
|
|
334
|
+
|
|
335
|
+
for (const scheme of SCHEMES) {
|
|
336
|
+
for (const tone of TONES) {
|
|
337
|
+
test(`${tone} / ${scheme}`, () => {
|
|
338
|
+
expect(contrastRatio(STATUS[tone][scheme], SURFACE.panel[scheme])).toBeGreaterThanOrEqual(3)
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
327
344
|
describe('the shipped palette is the generator output at DEFAULT_DERIVE_CONFIG', () => {
|
|
328
345
|
// A handful of exact hexes from `deriveTokens(DEFAULT_DERIVE_CONFIG)` (`tokens/derive.ts`) — the
|
|
329
346
|
// shipped default identity (seed #0077bd, zinc, all four knobs at level 0). Drift here means
|