kyd-shared-badge 0.3.17 → 0.3.18
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/package.json +1 -1
- package/src/colors.ts +34 -1
- package/src/components/GaugeCard.tsx +5 -22
- package/src/components/ReportHeader.tsx +3 -1
- package/src/components/RiskCard.tsx +7 -24
- package/src/utils/date.ts +0 -68
package/package.json
CHANGED
package/src/colors.ts
CHANGED
|
@@ -64,4 +64,37 @@ const red3 = '#F5B7B3'
|
|
|
64
64
|
const red4 = '#FADBDA'
|
|
65
65
|
const red5 = '#FDF0EF'
|
|
66
66
|
|
|
67
|
-
export { red1, red2, red3, red4, red5 }
|
|
67
|
+
export { red1, red2, red3, red4, red5 }
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
export function clampPercent(value?: number): number {
|
|
72
|
+
const n = Math.round(Number(value ?? 0))
|
|
73
|
+
if (Number.isNaN(n)) return 0
|
|
74
|
+
return Math.max(0, Math.min(100, n))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function scoreToColorHex(score: number): string {
|
|
78
|
+
const pct = clampPercent(score)
|
|
79
|
+
if (pct <= 33) return red
|
|
80
|
+
if (pct <= 66) return yellow
|
|
81
|
+
return green
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function scoreToCssVar(score: number): string {
|
|
85
|
+
const pct = clampPercent(score)
|
|
86
|
+
if (pct <= 33) return `var(--status-negative, ${red})`
|
|
87
|
+
if (pct <= 66) return `var(--status-neutral, ${yellow})`
|
|
88
|
+
return `var(--status-positive, ${green})`
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function hexToRgba(hex: string, alpha: number): string {
|
|
92
|
+
const clean = hex.replace('#', '')
|
|
93
|
+
const r = parseInt(clean.substring(0, 2), 16)
|
|
94
|
+
const g = parseInt(clean.substring(2, 4), 16)
|
|
95
|
+
const b = parseInt(clean.substring(4, 6), 16)
|
|
96
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
@@ -3,23 +3,11 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import BusinessRuleLink from './BusinessRuleLink';
|
|
5
5
|
import { FiInfo } from 'react-icons/fi';
|
|
6
|
-
import {
|
|
6
|
+
import { hexToRgba, scoreToColorHex, scoreToCssVar, clampPercent } from '../colors';
|
|
7
7
|
|
|
8
8
|
type TopMover = { label?: string; uid?: string };
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const clean = hex.replace('#', '');
|
|
12
|
-
const r = parseInt(clean.substring(0, 2), 16);
|
|
13
|
-
const g = parseInt(clean.substring(2, 4), 16);
|
|
14
|
-
const b = parseInt(clean.substring(4, 6), 16);
|
|
15
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const pickTint = (score: number) => {
|
|
19
|
-
if (score >= 75) return green;
|
|
20
|
-
if (score >= 50) return yellow;
|
|
21
|
-
return red;
|
|
22
|
-
};
|
|
10
|
+
// color helpers moved to ../utils/color
|
|
23
11
|
|
|
24
12
|
export default function GaugeCard({
|
|
25
13
|
title,
|
|
@@ -38,7 +26,7 @@ export default function GaugeCard({
|
|
|
38
26
|
topMoversTitle?: string;
|
|
39
27
|
tooltipText?: string;
|
|
40
28
|
}) {
|
|
41
|
-
const pct =
|
|
29
|
+
const pct = clampPercent(percent);
|
|
42
30
|
const displayLabel = label || '';
|
|
43
31
|
// Use a fixed internal coordinate system and scale the SVG to container for responsiveness
|
|
44
32
|
const size = 280;
|
|
@@ -47,13 +35,8 @@ export default function GaugeCard({
|
|
|
47
35
|
const circumference = Math.PI * radius;
|
|
48
36
|
const progress = pct / 100;
|
|
49
37
|
const dash = circumference * progress;
|
|
50
|
-
const progressColor =
|
|
51
|
-
|
|
52
|
-
? `var(--status-negative, ${red})`
|
|
53
|
-
: pct <= 66
|
|
54
|
-
? `var(--status-neutral, ${yellow})`
|
|
55
|
-
: `var(--status-positive, ${green})`;
|
|
56
|
-
const headerTint = hexToRgba(pickTint(pct), 0.06);
|
|
38
|
+
const progressColor = scoreToCssVar(pct);
|
|
39
|
+
const headerTint = hexToRgba(scoreToColorHex(pct), 0.06);
|
|
57
40
|
|
|
58
41
|
return (
|
|
59
42
|
<div
|
|
@@ -45,6 +45,8 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
45
45
|
// Use the dynamic image if available, otherwise fall back to the score-based one.
|
|
46
46
|
const finalBadgeImageUrl = badgeImageUrl || getBadgeImageUrl(score || 0);
|
|
47
47
|
const tint = hexToRgba(pickTint(score || 0), 0.06);
|
|
48
|
+
const matchScore = typeof enterpriseMatch?.score === 'number' ? enterpriseMatch.score : undefined;
|
|
49
|
+
const matchTint = matchScore !== undefined ? hexToRgba(pickTint(matchScore), 0.06) : undefined;
|
|
48
50
|
|
|
49
51
|
const formattedDate = updatedAt ? formatLocalDate(updatedAt, {
|
|
50
52
|
year: 'numeric',
|
|
@@ -114,7 +116,7 @@ const ReportHeader = ({ badgeId, developerName, updatedAt, score = 0, badgeImage
|
|
|
114
116
|
</span>
|
|
115
117
|
</div>
|
|
116
118
|
{typeof enterpriseMatch?.score === 'number' && (
|
|
117
|
-
<span className="px-2 py-1 rounded text-xs font-semibold" style={{
|
|
119
|
+
<span className="px-2 py-1 rounded text-xs font-semibold" style={{ backgroundColor: 'var(--content-card-background)', backgroundImage: matchTint ? `linear-gradient(${matchTint}, ${matchTint})` : undefined, border: '1px solid var(--icon-button-secondary)', color: 'var(--text-main)' }}>
|
|
118
120
|
{Math.round(enterpriseMatch.score)}%
|
|
119
121
|
</span>
|
|
120
122
|
)}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React from 'react';
|
|
4
4
|
import BusinessRuleLink from './BusinessRuleLink';
|
|
5
5
|
import { FiInfo } from 'react-icons/fi';
|
|
6
|
-
import {
|
|
6
|
+
import { clampPercent, hexToRgba, scoreToColorHex } from '../colors';
|
|
7
7
|
|
|
8
8
|
type TopMover = { label?: string; uid?: string };
|
|
9
9
|
|
|
@@ -24,32 +24,15 @@ export default function RiskCard({
|
|
|
24
24
|
topMoversTitle?: string;
|
|
25
25
|
tooltipText?: string;
|
|
26
26
|
}) {
|
|
27
|
-
const pctGood =
|
|
27
|
+
const pctGood = clampPercent(percentGood);
|
|
28
28
|
const displayLabel = label || '';
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
const clean = hex.replace('#', '');
|
|
32
|
-
const r = parseInt(clean.substring(0, 2), 16);
|
|
33
|
-
const g = parseInt(clean.substring(2, 4), 16);
|
|
34
|
-
const b = parseInt(clean.substring(4, 6), 16);
|
|
35
|
-
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
36
|
-
};
|
|
30
|
+
// Background tint uses same thresholds as GaugeCard via scoreToColorHex
|
|
37
31
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (score >= 50) return yellow;
|
|
41
|
-
return red;
|
|
42
|
-
};
|
|
32
|
+
// Foreground bar color uses same thresholds as GaugeCard based on overall score
|
|
33
|
+
const activeColor = scoreToColorHex(pctGood);
|
|
43
34
|
|
|
44
|
-
|
|
45
|
-
// indices 4 and 3 (highest bars) => red, index 2 => yellow, indices 1 and 0 => green
|
|
46
|
-
const colorForIndex = (index: number) => {
|
|
47
|
-
if (index >= 3) return red;
|
|
48
|
-
if (index === 2) return yellow;
|
|
49
|
-
return green;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const headerTint = hexToRgba(pickTint(pctGood), 0.06);
|
|
35
|
+
const headerTint = hexToRgba(scoreToColorHex(pctGood), 0.06);
|
|
53
36
|
|
|
54
37
|
// bar heights ascending representation
|
|
55
38
|
const bars = [40, 60, 85, 110, 140];
|
|
@@ -101,7 +84,7 @@ export default function RiskCard({
|
|
|
101
84
|
style={{
|
|
102
85
|
width: 36,
|
|
103
86
|
height: h,
|
|
104
|
-
backgroundColor: i === activeIndex ?
|
|
87
|
+
backgroundColor: i === activeIndex ? activeColor : 'var(--icon-button-secondary)',
|
|
105
88
|
borderRadius: 4,
|
|
106
89
|
}}
|
|
107
90
|
/>
|
package/src/utils/date.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
export type DateLike = string | number | Date | null | undefined;
|
|
2
|
-
|
|
3
|
-
const hasTimezoneDesignator = (value: string): boolean => {
|
|
4
|
-
return /[zZ]$/.test(value) || /[+-]\d{2}:?\d{2}$/.test(value);
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const truncateExcessFraction = (value: string): string => {
|
|
8
|
-
// Keep at most 3 fractional digits for milliseconds
|
|
9
|
-
return value.replace(/(\.\d{3})\d+/, '$1');
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const parseUtcDate = (input: DateLike): Date => {
|
|
13
|
-
if (input == null) return new Date(Number.NaN);
|
|
14
|
-
if (input instanceof Date) return new Date(input.getTime());
|
|
15
|
-
if (typeof input === 'number') {
|
|
16
|
-
const millis = input < 1e12 ? input * 1000 : input;
|
|
17
|
-
return new Date(millis);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const raw = String(input).trim();
|
|
21
|
-
if (!raw) return new Date(Number.NaN);
|
|
22
|
-
|
|
23
|
-
// Numeric strings (epoch seconds or millis)
|
|
24
|
-
if (/^\d{10}$/.test(raw)) return new Date(Number(raw) * 1000);
|
|
25
|
-
if (/^\d{13}$/.test(raw)) return new Date(Number(raw));
|
|
26
|
-
|
|
27
|
-
// Already has timezone; trust the browser parser
|
|
28
|
-
if (hasTimezoneDesignator(raw)) return new Date(raw);
|
|
29
|
-
|
|
30
|
-
// Normalize common forms: "YYYY-MM-DD HH:MM:SS[.fff...]" -> ISO-like
|
|
31
|
-
let norm = raw.replace(' ', 'T');
|
|
32
|
-
norm = truncateExcessFraction(norm);
|
|
33
|
-
|
|
34
|
-
// Date-only string -> treat as UTC midnight
|
|
35
|
-
if (/^\d{4}-\d{2}-\d{2}$/.test(norm)) {
|
|
36
|
-
return new Date(`${norm}T00:00:00Z`);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// If ISO-like without timezone, explicitly mark as UTC
|
|
40
|
-
if (/^\d{4}-\d{2}-\d{2}T/.test(norm) && !hasTimezoneDesignator(norm)) {
|
|
41
|
-
return new Date(`${norm}Z`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Fallback to native parsing
|
|
45
|
-
return new Date(norm);
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const formatLocalDate = (value: DateLike, options?: Intl.DateTimeFormatOptions): string => {
|
|
49
|
-
const d = value instanceof Date ? value : parseUtcDate(value);
|
|
50
|
-
if (Number.isNaN(d.getTime())) return 'N/A';
|
|
51
|
-
return d.toLocaleDateString(undefined, options);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const formatLocalDateTime = (value: DateLike, options?: Intl.DateTimeFormatOptions): string => {
|
|
55
|
-
const d = value instanceof Date ? value : parseUtcDate(value);
|
|
56
|
-
if (Number.isNaN(d.getTime())) return 'N/A';
|
|
57
|
-
const base: Intl.DateTimeFormatOptions = {
|
|
58
|
-
year: 'numeric',
|
|
59
|
-
month: 'long',
|
|
60
|
-
day: 'numeric',
|
|
61
|
-
hour: 'numeric',
|
|
62
|
-
minute: '2-digit',
|
|
63
|
-
timeZoneName: 'short',
|
|
64
|
-
};
|
|
65
|
-
return d.toLocaleString(undefined, { ...base, ...(options || {}) });
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
|