@astryxdesign/cli 0.5.0-canary.07f6c33 → 0.5.0-canary.09d191c
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/api/template/data-token-fallbacks.test.mjs +85 -0
- package/api/theme/build/build.mjs +35 -6
- package/api/theme/targets/targets.test.mjs +1 -0
- package/api/theme/themeTargets.doc.mjs +3 -3
- package/assets/templates/blocks/components/DateInput/DateInputClearable.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +6 -1
- package/assets/templates/blocks/components/DateInput/DateInputFormats.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputShowcase.tsx +4 -1
- package/assets/templates/blocks/components/DateInput/DateInputWithDescription.tsx +5 -1
- package/assets/templates/blocks/components/DateInput/DateInputWithValidation.tsx +5 -1
- package/assets/templates/blocks/components/HoverCard/HoverCardHookUsage.tsx +4 -1
- package/assets/templates/blocks/components/Step/StepContent.doc.mjs +1 -1
- package/assets/templates/blocks/components/Stepper/StepperCustomContent.doc.mjs +1 -1
- package/assets/templates/pages/dashboard-cohort-funnel/page.tsx +1 -1
- package/assets/templates/pages/dashboard-data/page.tsx +1 -1
- package/assets/templates/pages/dashboard-portfolio/page.tsx +5 -5
- package/assets/templates/pages/dashboard-service-monitoring/page.tsx +1 -1
- package/assets/templates/pages/settings-dialog/page.tsx +2543 -757
- package/assets/templates/pages/settings-dialog/template.doc.mjs +1 -2
- package/assets/templates/pages/theme-showcase/page.tsx +36 -17
- package/assets/templates/themes/chocolate/chocolateTheme.ts +3 -1
- package/assets/templates/themes/matcha/matchaTheme.ts +3 -1
- package/assets/templates/themes/neutral/neutralTheme.ts +3 -1
- package/assets/templates/themes/stone/stoneTheme.ts +3 -1
- package/clients/cli/commands/build-theme.color-scheme.test.mjs +7 -6
- package/clients/cli/commands/build-theme.data-tokens.test.mjs +144 -0
- package/clients/cli/commands/build-theme.variants.test.mjs +3 -0
- package/clients/cli/commands/theme-targets.behavior.test.mjs +1 -1
- package/clients/cli/lib/component-format.mjs +2 -2
- package/package.json +9 -9
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Asserts every `var(--color-data-*, <hex>)` fallback in the dashboard
|
|
5
|
+
* templates equals the token's own default.
|
|
6
|
+
*
|
|
7
|
+
* The fallback is what paints when the token does not resolve, so a fallback
|
|
8
|
+
* that differs from its token makes the same chart series two different colours
|
|
9
|
+
* depending on whether a theme is present. Three of them had drifted
|
|
10
|
+
* (`#22c55e`, `#E5484D`, `#008E80`), which is also why "the defaults are
|
|
11
|
+
* byte-identical to the template fallbacks" was not true.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {describe, it, expect} from 'vitest';
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import {fileURLToPath} from 'node:url';
|
|
18
|
+
import {dataTokenDefaults} from '@astryxdesign/core/theme';
|
|
19
|
+
|
|
20
|
+
const TEMPLATES_DIR = path.resolve(
|
|
21
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
22
|
+
'../../assets/templates',
|
|
23
|
+
);
|
|
24
|
+
const CALL_SITE = /var\(\s*(--color-data-[a-z0-9-]+)\s*,\s*([^)]+?)\s*\)/g;
|
|
25
|
+
|
|
26
|
+
/** The light side of a `light-dark(a, b)` pair — what a template falls back to. */
|
|
27
|
+
function lightSide(value) {
|
|
28
|
+
const match = /^light-dark\(\s*([^,]+?)\s*,/.exec(value);
|
|
29
|
+
return match ? match[1] : value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function walk(dir) {
|
|
33
|
+
return fs.readdirSync(dir, {withFileTypes: true}).flatMap(entry => {
|
|
34
|
+
const full = path.join(dir, entry.name);
|
|
35
|
+
return entry.isDirectory() ? walk(full) : [full];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function callSites() {
|
|
40
|
+
const sites = [];
|
|
41
|
+
for (const file of walk(TEMPLATES_DIR)) {
|
|
42
|
+
if (!/\.(tsx|ts|jsx|js|mjs|css)$/.test(file)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
fs.readFileSync(file, 'utf-8')
|
|
46
|
+
.split('\n')
|
|
47
|
+
.forEach((line, index) => {
|
|
48
|
+
for (const match of line.matchAll(CALL_SITE)) {
|
|
49
|
+
sites.push({
|
|
50
|
+
where: `${path.relative(TEMPLATES_DIR, file)}:${index + 1}`,
|
|
51
|
+
token: match[1],
|
|
52
|
+
fallback: match[2],
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return sites;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('template --color-data-* fallbacks', () => {
|
|
61
|
+
const sites = callSites();
|
|
62
|
+
|
|
63
|
+
it('finds the call sites at all', () => {
|
|
64
|
+
expect(sites.length).toBeGreaterThan(40);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('names a real token at every call site', () => {
|
|
68
|
+
const unknown = sites.filter(site => !dataTokenDefaults[site.token]);
|
|
69
|
+
expect(unknown.map(site => `${site.where} ${site.token}`)).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('matches each token default exactly', () => {
|
|
73
|
+
const drifted = sites
|
|
74
|
+
.filter(site => dataTokenDefaults[site.token])
|
|
75
|
+
.filter(
|
|
76
|
+
site => site.fallback !== lightSide(dataTokenDefaults[site.token]),
|
|
77
|
+
)
|
|
78
|
+
.map(
|
|
79
|
+
site =>
|
|
80
|
+
`${site.where} ${site.token}: ${site.fallback} != ${lightSide(dataTokenDefaults[site.token])}`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(drifted).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -62,12 +62,14 @@ import {
|
|
|
62
62
|
/** @type {any} */ let _defineTheme = null;
|
|
63
63
|
/** @type {any} */ let _generateThemeRulesSplit = null;
|
|
64
64
|
/** @type {any} */ let _generateOnMediaCSS = null;
|
|
65
|
+
/** @type {any} */ let _dataTokenDefaults = null;
|
|
65
66
|
/** @type {any} */ let _coreImportError = null;
|
|
66
67
|
try {
|
|
67
68
|
const coreTheme = await import('@astryxdesign/core/theme');
|
|
68
69
|
_defineTheme = coreTheme.defineTheme;
|
|
69
70
|
_generateThemeRulesSplit = coreTheme.generateThemeRulesSplit;
|
|
70
71
|
_generateOnMediaCSS = coreTheme.generateOnMediaCSS;
|
|
72
|
+
_dataTokenDefaults = coreTheme.dataTokenDefaults;
|
|
71
73
|
} catch (e) {
|
|
72
74
|
// Capture the reason so the theme action can surface a precise, actionable
|
|
73
75
|
// error. We don't throw here: this module is imported eagerly by the CLI
|
|
@@ -349,8 +351,8 @@ const _augmentationTargetCache = new Map();
|
|
|
349
351
|
* Resolve a rendered theme class token (the key without `astryx-`) to candidate
|
|
350
352
|
* public core subpaths and interface prefixes that may own its augmentable prop
|
|
351
353
|
* maps. Some tokens are subtargets documented by a parent component
|
|
352
|
-
* (`avatar-status-dot` augments `@astryxdesign/core/Avatar`), and some
|
|
353
|
-
*
|
|
354
|
+
* (`avatar-status-dot` augments `@astryxdesign/core/Avatar`), and some
|
|
355
|
+
* deprecated tokens still omit word separators (`progressbar`, `statusdot`)
|
|
354
356
|
* while the public API keeps `ProgressBar`/`StatusDot` casing. Component docs
|
|
355
357
|
* are the source of truth for the target token → owning component relationship.
|
|
356
358
|
*
|
|
@@ -428,8 +430,8 @@ async function resolveAugmentationTargetCandidates(componentName) {
|
|
|
428
430
|
|
|
429
431
|
// Try the exact rendered token first for documented subtargets such as
|
|
430
432
|
// avatar-status-dot → AvatarStatusDotVariantMap, then the owning public
|
|
431
|
-
// component name for unhyphenated
|
|
432
|
-
// ProgressBarVariantMap/statusdot → StatusDotVariantMap.
|
|
433
|
+
// component name for the deprecated unhyphenated tokens such as
|
|
434
|
+
// progressbar → ProgressBarVariantMap/statusdot → StatusDotVariantMap.
|
|
433
435
|
addCandidate(moduleName, toPascalCase(componentName));
|
|
434
436
|
addCandidate(moduleName, moduleName);
|
|
435
437
|
if (Array.isArray(doc?.components)) {
|
|
@@ -1166,8 +1168,15 @@ export async function themeBuild(
|
|
|
1166
1168
|
if (component.length > 0) {
|
|
1167
1169
|
const componentInner = component.join('\n\n');
|
|
1168
1170
|
const componentScope = `@scope (${scopeSelector}) to (${scopeTo}) {\n${componentInner}\n}`;
|
|
1169
|
-
// #3658: also emit attribute-specific rules so <Theme mode> can override color-scheme
|
|
1170
|
-
|
|
1171
|
+
// #3658: also emit attribute-specific rules so <Theme mode> can override color-scheme.
|
|
1172
|
+
// Decided from the theme's own values, not the generated CSS: that CSS
|
|
1173
|
+
// also carries the data-token defaults, which are light-dark() pairs, so
|
|
1174
|
+
// a substring check on it would fire for every theme.
|
|
1175
|
+
const themeOwnValues = JSON.stringify([
|
|
1176
|
+
resolvedTheme.tokens ?? {},
|
|
1177
|
+
resolvedTheme.components ?? {},
|
|
1178
|
+
]);
|
|
1179
|
+
const colorSchemeDecl = themeOwnValues.includes('light-dark(')
|
|
1171
1180
|
? ' :root { color-scheme: light dark; }\n html[data-theme="light"] { color-scheme: light; }\n html[data-theme="dark"] { color-scheme: dark; }\n\n'
|
|
1172
1181
|
: '';
|
|
1173
1182
|
cssParts.push(
|
|
@@ -1185,6 +1194,26 @@ export async function themeBuild(
|
|
|
1185
1194
|
logger.log('No overrides found — nothing to build.');
|
|
1186
1195
|
return null;
|
|
1187
1196
|
}
|
|
1197
|
+
// The data-token defaults are theme-independent and go in @layer
|
|
1198
|
+
// astryx-base, below the theme's own overrides. Formatted here from the
|
|
1199
|
+
// public `dataTokenDefaults` export, byte for byte as the `<Theme>`
|
|
1200
|
+
// runtime emits it — build-theme.data-tokens.test.mjs is the drift guard.
|
|
1201
|
+
// Placed after the reset block and before the theme block: a layer's order
|
|
1202
|
+
// is fixed by where it is first declared, so emitting it anywhere else in
|
|
1203
|
+
// the file would invert reset < astryx-base < astryx-theme for a consumer
|
|
1204
|
+
// who imports this stylesheet on its own.
|
|
1205
|
+
const baseCss = _dataTokenDefaults
|
|
1206
|
+
? `:root {\n${Object.entries(_dataTokenDefaults)
|
|
1207
|
+
.map(([name, value]) => ` ${name}: ${value};`)
|
|
1208
|
+
.join('\n')}\n}`
|
|
1209
|
+
: '';
|
|
1210
|
+
if (baseCss) {
|
|
1211
|
+
cssParts.splice(
|
|
1212
|
+
prose.length > 0 ? 1 : 0,
|
|
1213
|
+
0,
|
|
1214
|
+
`@layer astryx-base {\n${baseCss}\n}`,
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1188
1217
|
css = cssParts.join('\n\n') + '\n';
|
|
1189
1218
|
}
|
|
1190
1219
|
|
|
@@ -18,9 +18,9 @@ export const doc = {
|
|
|
18
18
|
'Enumerates the whole themeable surface: each `defineTheme` components key, the stable ' +
|
|
19
19
|
'class it paints, the component that declares it, and the props and states that are legal ' +
|
|
20
20
|
'override keys under it. Same source as the Theming table `astryx component <Name>` prints ' +
|
|
21
|
-
'
|
|
21
|
+
'(the component docs), so the list cannot drift from the components, and `theme build` ' +
|
|
22
22
|
'validates overrides against this exact set. A filter naming a component gives that ' +
|
|
23
|
-
'component\
|
|
23
|
+
'component\'s set; anything else is a substring search over the keys.',
|
|
24
24
|
importPath: '@astryxdesign/cli/api',
|
|
25
25
|
signature:
|
|
26
26
|
'themeTargets(filter?: string, ctx?: {cwd?: string}): Promise<ThemeTargetsResponse>',
|
|
@@ -35,7 +35,7 @@ export const doc = {
|
|
|
35
35
|
{
|
|
36
36
|
name: 'ctx.cwd',
|
|
37
37
|
type: 'string',
|
|
38
|
-
description: 'Directory the project
|
|
38
|
+
description: 'Directory the project\'s @astryxdesign/core is resolved from.',
|
|
39
39
|
},
|
|
40
40
|
],
|
|
41
41
|
returns: [
|
|
@@ -16,7 +16,11 @@ export default function DateInputClearable() {
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<Stack
|
|
19
|
+
<Stack
|
|
20
|
+
direction="vertical"
|
|
21
|
+
gap={4}
|
|
22
|
+
width="100%"
|
|
23
|
+
style={{minWidth: 240, maxWidth: 400}}>
|
|
20
24
|
<Text type="supporting" color="secondary">
|
|
21
25
|
{value ? `Selected: ${value}` : 'No date selected'}
|
|
22
26
|
</Text>
|
|
@@ -27,7 +27,11 @@ export default function DateInputDateRange() {
|
|
|
27
27
|
const [value, setValue] = useState<DateString | undefined>(undefined);
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
|
-
<Stack
|
|
30
|
+
<Stack
|
|
31
|
+
direction="vertical"
|
|
32
|
+
gap={4}
|
|
33
|
+
width="100%"
|
|
34
|
+
style={{minWidth: 240, maxWidth: 400}}>
|
|
31
35
|
<Text type="supporting" color="secondary">
|
|
32
36
|
{value ? `Booked: ${value}` : 'Pick a date in the available range'}
|
|
33
37
|
</Text>
|
|
@@ -37,6 +41,7 @@ export default function DateInputDateRange() {
|
|
|
37
41
|
max={max}
|
|
38
42
|
description={description}
|
|
39
43
|
placeholder="Select a booking date"
|
|
44
|
+
nativePicker="never"
|
|
40
45
|
value={value}
|
|
41
46
|
onChange={setValue}
|
|
42
47
|
/>
|
|
@@ -16,7 +16,11 @@ export default function DateInputFormats() {
|
|
|
16
16
|
);
|
|
17
17
|
|
|
18
18
|
return (
|
|
19
|
-
<Stack
|
|
19
|
+
<Stack
|
|
20
|
+
direction="vertical"
|
|
21
|
+
gap={4}
|
|
22
|
+
width="100%"
|
|
23
|
+
style={{minWidth: 240, maxWidth: 400}}>
|
|
20
24
|
<Text type="supporting" color="secondary">
|
|
21
25
|
The same committed date, displayed with different formats.
|
|
22
26
|
</Text>
|
|
@@ -13,7 +13,10 @@ export default function DateInputShowcase() {
|
|
|
13
13
|
const [date, setDate] = useState<DateString | undefined>(undefined);
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
|
-
<Stack
|
|
16
|
+
<Stack
|
|
17
|
+
direction="vertical"
|
|
18
|
+
width="100%"
|
|
19
|
+
style={{minWidth: 240, maxWidth: 400}}>
|
|
17
20
|
<DateInput
|
|
18
21
|
label="Start date"
|
|
19
22
|
placeholder="Select a date"
|
|
@@ -14,7 +14,11 @@ export default function DateInputWithDescription() {
|
|
|
14
14
|
const [value, setValue] = useState<DateString | undefined>(undefined);
|
|
15
15
|
|
|
16
16
|
return (
|
|
17
|
-
<Stack
|
|
17
|
+
<Stack
|
|
18
|
+
direction="vertical"
|
|
19
|
+
gap={4}
|
|
20
|
+
width="100%"
|
|
21
|
+
style={{minWidth: 240, maxWidth: 400}}>
|
|
18
22
|
<Text type="supporting" color="secondary">
|
|
19
23
|
Helper text explains what the field expects
|
|
20
24
|
</Text>
|
|
@@ -21,7 +21,11 @@ export default function DateInputWithValidation() {
|
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
|
-
<Stack
|
|
24
|
+
<Stack
|
|
25
|
+
direction="vertical"
|
|
26
|
+
gap={4}
|
|
27
|
+
width="100%"
|
|
28
|
+
style={{minWidth: 240, maxWidth: 400}}>
|
|
25
29
|
<DateInput
|
|
26
30
|
label="Event date"
|
|
27
31
|
value={errorVal}
|
|
@@ -13,6 +13,7 @@ export default function HoverCardHookUsage() {
|
|
|
13
13
|
placement: 'below',
|
|
14
14
|
delay: 100,
|
|
15
15
|
isDefaultOpen: true,
|
|
16
|
+
label: 'Alex Morgan',
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
return (
|
|
@@ -20,7 +21,9 @@ export default function HoverCardHookUsage() {
|
|
|
20
21
|
<Button
|
|
21
22
|
label="Hover profile"
|
|
22
23
|
ref={hoverCard.ref}
|
|
23
|
-
aria-
|
|
24
|
+
aria-haspopup="dialog"
|
|
25
|
+
aria-controls={hoverCard.isOpen ? hoverCard.id : undefined}
|
|
26
|
+
aria-expanded={hoverCard.isOpen}
|
|
24
27
|
/>
|
|
25
28
|
{hoverCard.renderHoverCard(
|
|
26
29
|
<VStack gap={1}>
|
|
@@ -7,7 +7,7 @@ export const doc = {
|
|
|
7
7
|
name: 'Step — Content Slot',
|
|
8
8
|
displayName: 'Step — Content Slot',
|
|
9
9
|
description:
|
|
10
|
-
'Children passed to a Step render below its description, indented to line up with the label rather than the indicator, and stay outside the clickable label area so buttons inside remain their own targets. In a full flow you gate the slot on the step being active
|
|
10
|
+
'Children passed to a Step render below its description, indented to line up with the label rather than the indicator, and stay outside the clickable label area so buttons inside remain their own targets. In a full flow you gate the slot on the step being active. That is what turns a vertical stepper into an expanding one.',
|
|
11
11
|
isReady: true,
|
|
12
12
|
aspectRatio: 4 / 3,
|
|
13
13
|
componentsUsed: ['Stepper', 'Step', 'TextInput', 'Button'],
|
|
@@ -7,7 +7,7 @@ export const doc = {
|
|
|
7
7
|
name: 'Stepper — Custom Content',
|
|
8
8
|
displayName: 'Stepper — Custom Content',
|
|
9
9
|
description:
|
|
10
|
-
'A vertical stepper where each step owns a slice of the page. The content slot takes any node
|
|
10
|
+
'A vertical stepper where each step owns a slice of the page. The content slot takes any node (form fields, a summary panel, a banner), so a stepper is not limited to multi-step forms. Rendering the slot only for the active step is what makes the flow expand one step at a time.',
|
|
11
11
|
isReady: true,
|
|
12
12
|
aspectRatio: 4 / 3,
|
|
13
13
|
componentsUsed: [
|
|
@@ -79,7 +79,7 @@ const COLORS = {
|
|
|
79
79
|
green: 'var(--color-data-categorical-green, #0B991F)',
|
|
80
80
|
orange: 'var(--color-data-categorical-orange, #EB6E00)',
|
|
81
81
|
purple: 'var(--color-data-categorical-purple, #6B1EFD)',
|
|
82
|
-
teal: 'var(--color-data-categorical-teal, #
|
|
82
|
+
teal: 'var(--color-data-categorical-teal, #08A3A3)',
|
|
83
83
|
};
|
|
84
84
|
const GRID_STROKE = 'var(--color-border, rgba(5, 54, 89, 0.1))';
|
|
85
85
|
const AXIS_TICK = {
|
|
@@ -98,7 +98,7 @@ const COLORS = {
|
|
|
98
98
|
green: 'var(--color-data-categorical-green, #0B991F)',
|
|
99
99
|
orange: 'var(--color-data-categorical-orange, #EB6E00)',
|
|
100
100
|
purple: 'var(--color-data-categorical-purple, #6B1EFD)',
|
|
101
|
-
teal: 'var(--color-data-categorical-teal, #
|
|
101
|
+
teal: 'var(--color-data-categorical-teal, #08A3A3)',
|
|
102
102
|
neutral: 'var(--color-data-neutral, #8494A3)',
|
|
103
103
|
};
|
|
104
104
|
const GRID_STROKE = 'var(--color-border, rgba(5, 54, 89, 0.1))';
|
|
@@ -370,12 +370,12 @@ function PortfolioChart() {
|
|
|
370
370
|
<linearGradient id="portfolioGradient" x1="0" y1="0" x2="0" y2="1">
|
|
371
371
|
<stop
|
|
372
372
|
offset="5%"
|
|
373
|
-
stopColor="var(--color-data-categorical-green, #
|
|
373
|
+
stopColor="var(--color-data-categorical-green, #0B991F)"
|
|
374
374
|
stopOpacity={0.3}
|
|
375
375
|
/>
|
|
376
376
|
<stop
|
|
377
377
|
offset="95%"
|
|
378
|
-
stopColor="var(--color-data-categorical-green, #
|
|
378
|
+
stopColor="var(--color-data-categorical-green, #0B991F)"
|
|
379
379
|
stopOpacity={0.05}
|
|
380
380
|
/>
|
|
381
381
|
</linearGradient>
|
|
@@ -417,7 +417,7 @@ function PortfolioChart() {
|
|
|
417
417
|
<Area
|
|
418
418
|
type="linear"
|
|
419
419
|
dataKey="value"
|
|
420
|
-
stroke="var(--color-data-categorical-green, #
|
|
420
|
+
stroke="var(--color-data-categorical-green, #0B991F)"
|
|
421
421
|
strokeWidth={1.5}
|
|
422
422
|
fill="url(#portfolioGradient)"
|
|
423
423
|
dot={false}
|
|
@@ -434,7 +434,7 @@ function Sparkline({data, positive}: {data: number[]; positive: boolean}) {
|
|
|
434
434
|
const chartData = data.map((v, i) => ({i, v}));
|
|
435
435
|
const color = positive
|
|
436
436
|
? 'var(--color-data-categorical-green, #0B991F)'
|
|
437
|
-
: 'var(--color-data-categorical-red, #
|
|
437
|
+
: 'var(--color-data-categorical-red, #F5394F)';
|
|
438
438
|
return (
|
|
439
439
|
<ResponsiveContainer width="100%" height={40}>
|
|
440
440
|
<LineChart
|
|
@@ -503,7 +503,7 @@ function TrendSparkline({data, positive}: {data: number[]; positive: boolean}) {
|
|
|
503
503
|
const chartData = data.map((v, i) => ({i, v}));
|
|
504
504
|
const color = positive
|
|
505
505
|
? 'var(--color-data-categorical-green, #0B991F)'
|
|
506
|
-
: 'var(--color-data-categorical-red, #
|
|
506
|
+
: 'var(--color-data-categorical-red, #F5394F)';
|
|
507
507
|
return (
|
|
508
508
|
<ResponsiveContainer width="100%" height={24}>
|
|
509
509
|
<LineChart
|
|
@@ -727,7 +727,7 @@ interface ChartLine {
|
|
|
727
727
|
const BLUE = 'var(--color-data-categorical-blue, #0171E3)';
|
|
728
728
|
const ORANGE = 'var(--color-data-categorical-orange, #EB6E00)';
|
|
729
729
|
const PURPLE = 'var(--color-data-categorical-purple, #6B1EFD)';
|
|
730
|
-
const TEAL = 'var(--color-data-categorical-teal, #
|
|
730
|
+
const TEAL = 'var(--color-data-categorical-teal, #08A3A3)';
|
|
731
731
|
|
|
732
732
|
const LATENCY_LINES: ChartLine[] = [
|
|
733
733
|
{key: 'p50', name: 'p50', color: BLUE},
|