@principal-ade/industry-theme 0.1.21 → 0.1.22
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/dist/cjs/ContrastReport.d.ts +20 -0
- package/dist/cjs/ContrastReport.d.ts.map +1 -0
- package/dist/cjs/contrast.d.ts +75 -0
- package/dist/cjs/contrast.d.ts.map +1 -0
- package/dist/cjs/index.d.ts +4 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +283 -9
- package/dist/esm/ContrastReport.d.ts +20 -0
- package/dist/esm/ContrastReport.d.ts.map +1 -0
- package/dist/esm/contrast.d.ts +75 -0
- package/dist/esm/contrast.d.ts.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +283 -9
- package/package.json +8 -7
- package/src/ContrastReport.stories.tsx +77 -0
- package/src/ContrastReport.tsx +226 -0
- package/src/README.md +2 -3
- package/src/ThemeProvider.tsx +1 -20
- package/src/contrast.ts +172 -0
- package/src/index.ts +22 -0
- package/src/themes.ts +8 -8
package/dist/esm/index.js
CHANGED
|
@@ -1872,7 +1872,7 @@ var iceTangerineDarkTheme = {
|
|
|
1872
1872
|
zIndices: [0, 1, 10, 20, 30, 40, 50],
|
|
1873
1873
|
colors: {
|
|
1874
1874
|
text: "#d0e5ea",
|
|
1875
|
-
background: "#
|
|
1875
|
+
background: "#0a1829",
|
|
1876
1876
|
primary: "#ff6b35",
|
|
1877
1877
|
secondary: "#ff8257",
|
|
1878
1878
|
accent: "#0893d2",
|
|
@@ -1882,26 +1882,26 @@ var iceTangerineDarkTheme = {
|
|
|
1882
1882
|
warning: "#f59e0b",
|
|
1883
1883
|
error: "#ef4444",
|
|
1884
1884
|
info: "#0893d2",
|
|
1885
|
-
border: "#
|
|
1885
|
+
border: "#5a82aa",
|
|
1886
1886
|
backgroundSecondary: "#0f2e58",
|
|
1887
1887
|
backgroundTertiary: "#123461",
|
|
1888
1888
|
backgroundLight: "#0b1f3f",
|
|
1889
|
-
backgroundDark: "#
|
|
1889
|
+
backgroundDark: "#040b15",
|
|
1890
1890
|
backgroundHover: "#2a1f18",
|
|
1891
1891
|
primaryBlade: "#0e2b53",
|
|
1892
1892
|
surface: "#0f2e58",
|
|
1893
1893
|
textSecondary: "#9fc4d4",
|
|
1894
1894
|
textTertiary: "#7ba8bc",
|
|
1895
|
-
textMuted: "#
|
|
1895
|
+
textMuted: "#73a0b3",
|
|
1896
1896
|
highlightBg: "#2a1f18",
|
|
1897
1897
|
highlightBorder: "#ff6b35",
|
|
1898
|
-
textOnPrimary: "#
|
|
1899
|
-
textOnSecondary: "#
|
|
1900
|
-
textOnAccent: "#
|
|
1898
|
+
textOnPrimary: "#0d274d",
|
|
1899
|
+
textOnSecondary: "#0d274d",
|
|
1900
|
+
textOnAccent: "#0a1829"
|
|
1901
1901
|
},
|
|
1902
1902
|
buttons: {
|
|
1903
1903
|
primary: {
|
|
1904
|
-
color: "#
|
|
1904
|
+
color: "#0d274d",
|
|
1905
1905
|
bg: "primary",
|
|
1906
1906
|
borderWidth: 0,
|
|
1907
1907
|
"&:hover": {
|
|
@@ -2805,6 +2805,272 @@ var ThemeShowcase = ({
|
|
|
2805
2805
|
}
|
|
2806
2806
|
}, modeName)))));
|
|
2807
2807
|
};
|
|
2808
|
+
// src/contrast.ts
|
|
2809
|
+
var WCAG_THRESHOLDS = {
|
|
2810
|
+
text: { AA: 4.5, AAA: 7 },
|
|
2811
|
+
large: { AA: 3, AAA: 4.5 },
|
|
2812
|
+
ui: { AA: 3, AAA: 3 }
|
|
2813
|
+
};
|
|
2814
|
+
function parseColor(input) {
|
|
2815
|
+
if (!input)
|
|
2816
|
+
return null;
|
|
2817
|
+
const color = input.trim().toLowerCase();
|
|
2818
|
+
if (color.startsWith("#")) {
|
|
2819
|
+
let hex = color.slice(1);
|
|
2820
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
2821
|
+
hex = hex.split("").map((c) => c + c).join("");
|
|
2822
|
+
}
|
|
2823
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
2824
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
2825
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
2826
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
2827
|
+
if ([r, g, b].some((n) => Number.isNaN(n)))
|
|
2828
|
+
return null;
|
|
2829
|
+
return [r, g, b];
|
|
2830
|
+
}
|
|
2831
|
+
return null;
|
|
2832
|
+
}
|
|
2833
|
+
const rgbMatch = color.match(/^rgba?\(([^)]+)\)$/);
|
|
2834
|
+
if (rgbMatch) {
|
|
2835
|
+
const parts = rgbMatch[1].split(/[,/\s]+/).filter(Boolean).slice(0, 3);
|
|
2836
|
+
if (parts.length < 3)
|
|
2837
|
+
return null;
|
|
2838
|
+
const channels = parts.map((p) => p.endsWith("%") ? Math.round(parseFloat(p) / 100 * 255) : parseFloat(p));
|
|
2839
|
+
if (channels.some((n) => Number.isNaN(n)))
|
|
2840
|
+
return null;
|
|
2841
|
+
return [channels[0], channels[1], channels[2]];
|
|
2842
|
+
}
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
function relativeLuminance([r, g, b]) {
|
|
2846
|
+
const toLinear = (c) => {
|
|
2847
|
+
const s = c / 255;
|
|
2848
|
+
return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
|
|
2849
|
+
};
|
|
2850
|
+
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
|
|
2851
|
+
}
|
|
2852
|
+
function contrastRatio(fg, bg) {
|
|
2853
|
+
const a = parseColor(fg);
|
|
2854
|
+
const b = parseColor(bg);
|
|
2855
|
+
if (!a || !b)
|
|
2856
|
+
return null;
|
|
2857
|
+
const l1 = relativeLuminance(a);
|
|
2858
|
+
const l2 = relativeLuminance(b);
|
|
2859
|
+
const lighter = Math.max(l1, l2);
|
|
2860
|
+
const darker = Math.min(l1, l2);
|
|
2861
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
2862
|
+
}
|
|
2863
|
+
function gradeContrast(ratio, use) {
|
|
2864
|
+
const t = WCAG_THRESHOLDS[use];
|
|
2865
|
+
if (ratio >= t.AAA)
|
|
2866
|
+
return "AAA";
|
|
2867
|
+
if (ratio >= t.AA)
|
|
2868
|
+
return "AA";
|
|
2869
|
+
return "fail";
|
|
2870
|
+
}
|
|
2871
|
+
var CONTRAST_PAIRS = [
|
|
2872
|
+
{ label: "text on background", fg: "text", bg: "background", use: "text" },
|
|
2873
|
+
{ label: "text on surface", fg: "text", bg: "surface", use: "text" },
|
|
2874
|
+
{ label: "text on backgroundSecondary", fg: "text", bg: "backgroundSecondary", use: "text" },
|
|
2875
|
+
{ label: "textSecondary on background", fg: "textSecondary", bg: "background", use: "text" },
|
|
2876
|
+
{ label: "textTertiary on background", fg: "textTertiary", bg: "background", use: "text" },
|
|
2877
|
+
{ label: "textMuted on background", fg: "textMuted", bg: "background", use: "text" },
|
|
2878
|
+
{ label: "textOnPrimary on primary", fg: "textOnPrimary", bg: "primary", use: "text" },
|
|
2879
|
+
{ label: "textOnSecondary on secondary", fg: "textOnSecondary", bg: "secondary", use: "text" },
|
|
2880
|
+
{ label: "textOnAccent on accent", fg: "textOnAccent", bg: "accent", use: "text" },
|
|
2881
|
+
{ label: "primary on background", fg: "primary", bg: "background", use: "large" },
|
|
2882
|
+
{ label: "success on background", fg: "success", bg: "background", use: "ui" },
|
|
2883
|
+
{ label: "warning on background", fg: "warning", bg: "background", use: "ui" },
|
|
2884
|
+
{ label: "error on background", fg: "error", bg: "background", use: "ui" },
|
|
2885
|
+
{ label: "info on background", fg: "info", bg: "background", use: "ui" },
|
|
2886
|
+
{ label: "border on background", fg: "border", bg: "background", use: "ui" },
|
|
2887
|
+
{ label: "border on surface", fg: "border", bg: "surface", use: "ui" }
|
|
2888
|
+
];
|
|
2889
|
+
function evaluateThemeContrast(theme2) {
|
|
2890
|
+
const results = CONTRAST_PAIRS.map((pair) => {
|
|
2891
|
+
const fgColor = theme2.colors[pair.fg] ?? "";
|
|
2892
|
+
const bgColor = theme2.colors[pair.bg] ?? "";
|
|
2893
|
+
const ratio = contrastRatio(fgColor, bgColor);
|
|
2894
|
+
const required = WCAG_THRESHOLDS[pair.use].AA;
|
|
2895
|
+
const level = ratio === null ? "fail" : gradeContrast(ratio, pair.use);
|
|
2896
|
+
return { pair, fgColor, bgColor, ratio, required, level };
|
|
2897
|
+
});
|
|
2898
|
+
const failures = results.filter((r) => r.level === "fail");
|
|
2899
|
+
return { results, failures, passesAA: failures.length === 0 };
|
|
2900
|
+
}
|
|
2901
|
+
// src/ContrastReport.tsx
|
|
2902
|
+
import React3 from "react";
|
|
2903
|
+
var LEVEL_STYLE = {
|
|
2904
|
+
AAA: { bg: "#0f7b3f", fg: "#ffffff", label: "AAA" },
|
|
2905
|
+
AA: { bg: "#1f6feb", fg: "#ffffff", label: "AA" },
|
|
2906
|
+
fail: { bg: "#cf222e", fg: "#ffffff", label: "FAIL" }
|
|
2907
|
+
};
|
|
2908
|
+
var ui = {
|
|
2909
|
+
fontFamily: "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
2910
|
+
text: "#1c2128",
|
|
2911
|
+
subtle: "#57606a",
|
|
2912
|
+
border: "#d0d7de",
|
|
2913
|
+
surface: "#ffffff",
|
|
2914
|
+
panel: "#f6f8fa"
|
|
2915
|
+
};
|
|
2916
|
+
var Badge = ({ level }) => {
|
|
2917
|
+
const s = LEVEL_STYLE[level];
|
|
2918
|
+
return /* @__PURE__ */ React3.createElement("span", {
|
|
2919
|
+
style: {
|
|
2920
|
+
display: "inline-block",
|
|
2921
|
+
minWidth: 44,
|
|
2922
|
+
textAlign: "center",
|
|
2923
|
+
padding: "2px 8px",
|
|
2924
|
+
borderRadius: 999,
|
|
2925
|
+
fontSize: 11,
|
|
2926
|
+
fontWeight: 700,
|
|
2927
|
+
letterSpacing: 0.4,
|
|
2928
|
+
backgroundColor: s.bg,
|
|
2929
|
+
color: s.fg
|
|
2930
|
+
}
|
|
2931
|
+
}, s.label);
|
|
2932
|
+
};
|
|
2933
|
+
var Swatch = ({ fg, bg }) => /* @__PURE__ */ React3.createElement("span", {
|
|
2934
|
+
title: `${fg} on ${bg}`,
|
|
2935
|
+
style: {
|
|
2936
|
+
display: "inline-flex",
|
|
2937
|
+
alignItems: "center",
|
|
2938
|
+
justifyContent: "center",
|
|
2939
|
+
width: 40,
|
|
2940
|
+
height: 24,
|
|
2941
|
+
borderRadius: 4,
|
|
2942
|
+
border: `1px solid ${ui.border}`,
|
|
2943
|
+
backgroundColor: bg,
|
|
2944
|
+
color: fg,
|
|
2945
|
+
fontSize: 13,
|
|
2946
|
+
fontWeight: 700
|
|
2947
|
+
}
|
|
2948
|
+
}, "Aa");
|
|
2949
|
+
var Row = ({ r }) => {
|
|
2950
|
+
const failed = r.level === "fail";
|
|
2951
|
+
return /* @__PURE__ */ React3.createElement("tr", {
|
|
2952
|
+
style: { backgroundColor: failed ? "#fff5f5" : "transparent" }
|
|
2953
|
+
}, /* @__PURE__ */ React3.createElement("td", {
|
|
2954
|
+
style: cell
|
|
2955
|
+
}, /* @__PURE__ */ React3.createElement(Swatch, {
|
|
2956
|
+
fg: r.fgColor,
|
|
2957
|
+
bg: r.bgColor
|
|
2958
|
+
})), /* @__PURE__ */ React3.createElement("td", {
|
|
2959
|
+
style: { ...cell, fontWeight: 500 }
|
|
2960
|
+
}, r.pair.label, /* @__PURE__ */ React3.createElement("div", {
|
|
2961
|
+
style: { fontSize: 11, color: ui.subtle, fontFamily: "monospace" }
|
|
2962
|
+
}, r.fgColor, " / ", r.bgColor)), /* @__PURE__ */ React3.createElement("td", {
|
|
2963
|
+
style: { ...cell, textAlign: "right", fontVariantNumeric: "tabular-nums" }
|
|
2964
|
+
}, /* @__PURE__ */ React3.createElement("span", {
|
|
2965
|
+
style: { fontWeight: 700, color: failed ? "#cf222e" : ui.text }
|
|
2966
|
+
}, r.ratio === null ? "—" : `${r.ratio.toFixed(2)}:1`)), /* @__PURE__ */ React3.createElement("td", {
|
|
2967
|
+
style: {
|
|
2968
|
+
...cell,
|
|
2969
|
+
textAlign: "right",
|
|
2970
|
+
color: ui.subtle,
|
|
2971
|
+
fontVariantNumeric: "tabular-nums"
|
|
2972
|
+
}
|
|
2973
|
+
}, r.required, ":1"), /* @__PURE__ */ React3.createElement("td", {
|
|
2974
|
+
style: { ...cell, textAlign: "center", textTransform: "capitalize", color: ui.subtle }
|
|
2975
|
+
}, r.pair.use), /* @__PURE__ */ React3.createElement("td", {
|
|
2976
|
+
style: { ...cell, textAlign: "center" }
|
|
2977
|
+
}, /* @__PURE__ */ React3.createElement(Badge, {
|
|
2978
|
+
level: r.level
|
|
2979
|
+
})));
|
|
2980
|
+
};
|
|
2981
|
+
var cell = {
|
|
2982
|
+
padding: "8px 12px",
|
|
2983
|
+
borderBottom: `1px solid ${ui.border}`,
|
|
2984
|
+
fontSize: 13,
|
|
2985
|
+
verticalAlign: "middle"
|
|
2986
|
+
};
|
|
2987
|
+
var headCell = {
|
|
2988
|
+
padding: "8px 12px",
|
|
2989
|
+
textAlign: "left",
|
|
2990
|
+
fontSize: 11,
|
|
2991
|
+
fontWeight: 700,
|
|
2992
|
+
textTransform: "uppercase",
|
|
2993
|
+
letterSpacing: 0.5,
|
|
2994
|
+
color: ui.subtle,
|
|
2995
|
+
borderBottom: `2px solid ${ui.border}`
|
|
2996
|
+
};
|
|
2997
|
+
var ThemeTable = ({ name, theme: theme2 }) => {
|
|
2998
|
+
const report = evaluateThemeContrast(theme2);
|
|
2999
|
+
const fails = report.failures.length;
|
|
3000
|
+
return /* @__PURE__ */ React3.createElement("section", {
|
|
3001
|
+
style: {
|
|
3002
|
+
marginBottom: 28,
|
|
3003
|
+
border: `1px solid ${ui.border}`,
|
|
3004
|
+
borderRadius: 8,
|
|
3005
|
+
overflow: "hidden",
|
|
3006
|
+
backgroundColor: ui.surface
|
|
3007
|
+
}
|
|
3008
|
+
}, /* @__PURE__ */ React3.createElement("header", {
|
|
3009
|
+
style: {
|
|
3010
|
+
display: "flex",
|
|
3011
|
+
alignItems: "center",
|
|
3012
|
+
justifyContent: "space-between",
|
|
3013
|
+
gap: 12,
|
|
3014
|
+
padding: "12px 16px",
|
|
3015
|
+
backgroundColor: ui.panel,
|
|
3016
|
+
borderBottom: `1px solid ${ui.border}`
|
|
3017
|
+
}
|
|
3018
|
+
}, /* @__PURE__ */ React3.createElement("h3", {
|
|
3019
|
+
style: { margin: 0, fontSize: 16, color: ui.text }
|
|
3020
|
+
}, name), /* @__PURE__ */ React3.createElement("span", {
|
|
3021
|
+
style: {
|
|
3022
|
+
fontSize: 12,
|
|
3023
|
+
fontWeight: 700,
|
|
3024
|
+
padding: "4px 10px",
|
|
3025
|
+
borderRadius: 999,
|
|
3026
|
+
backgroundColor: report.passesAA ? "#dafbe1" : "#ffebe9",
|
|
3027
|
+
color: report.passesAA ? "#0f7b3f" : "#cf222e"
|
|
3028
|
+
}
|
|
3029
|
+
}, report.passesAA ? "✓ Passes AA" : `${fails} AA failure${fails === 1 ? "" : "s"}`)), /* @__PURE__ */ React3.createElement("table", {
|
|
3030
|
+
style: { width: "100%", borderCollapse: "collapse" }
|
|
3031
|
+
}, /* @__PURE__ */ React3.createElement("thead", null, /* @__PURE__ */ React3.createElement("tr", null, /* @__PURE__ */ React3.createElement("th", {
|
|
3032
|
+
style: headCell
|
|
3033
|
+
}, "Sample"), /* @__PURE__ */ React3.createElement("th", {
|
|
3034
|
+
style: headCell
|
|
3035
|
+
}, "Pair"), /* @__PURE__ */ React3.createElement("th", {
|
|
3036
|
+
style: { ...headCell, textAlign: "right" }
|
|
3037
|
+
}, "Ratio"), /* @__PURE__ */ React3.createElement("th", {
|
|
3038
|
+
style: { ...headCell, textAlign: "right" }
|
|
3039
|
+
}, "Req. (AA)"), /* @__PURE__ */ React3.createElement("th", {
|
|
3040
|
+
style: { ...headCell, textAlign: "center" }
|
|
3041
|
+
}, "Use"), /* @__PURE__ */ React3.createElement("th", {
|
|
3042
|
+
style: { ...headCell, textAlign: "center" }
|
|
3043
|
+
}, "Grade"))), /* @__PURE__ */ React3.createElement("tbody", null, report.results.map((r, i) => /* @__PURE__ */ React3.createElement(Row, {
|
|
3044
|
+
key: i,
|
|
3045
|
+
r
|
|
3046
|
+
})))));
|
|
3047
|
+
};
|
|
3048
|
+
var ContrastReport = ({
|
|
3049
|
+
theme: theme2,
|
|
3050
|
+
themes,
|
|
3051
|
+
title = "WCAG Contrast Report"
|
|
3052
|
+
}) => {
|
|
3053
|
+
const list = themes ?? (theme2 ? [{ name: title, theme: theme2 }] : []);
|
|
3054
|
+
return /* @__PURE__ */ React3.createElement("div", {
|
|
3055
|
+
style: {
|
|
3056
|
+
fontFamily: ui.fontFamily,
|
|
3057
|
+
color: ui.text,
|
|
3058
|
+
backgroundColor: ui.panel,
|
|
3059
|
+
padding: 24,
|
|
3060
|
+
minHeight: "100vh"
|
|
3061
|
+
}
|
|
3062
|
+
}, /* @__PURE__ */ React3.createElement("header", {
|
|
3063
|
+
style: { marginBottom: 20 }
|
|
3064
|
+
}, /* @__PURE__ */ React3.createElement("h1", {
|
|
3065
|
+
style: { margin: "0 0 6px", fontSize: 22 }
|
|
3066
|
+
}, title), /* @__PURE__ */ React3.createElement("p", {
|
|
3067
|
+
style: { margin: 0, fontSize: 13, color: ui.subtle, maxWidth: 720 }
|
|
3068
|
+
}, "WCAG 2.x contrast ratios for each theme's declared color roles. Text pairs require AA", " ", WCAG_THRESHOLDS.text.AA, ":1 (AAA ", WCAG_THRESHOLDS.text.AAA, ":1); large text requires", " ", WCAG_THRESHOLDS.large.AA, ":1; borders and other non-text UI require", " ", WCAG_THRESHOLDS.ui.AA, ":1.")), list.map((t) => /* @__PURE__ */ React3.createElement(ThemeTable, {
|
|
3069
|
+
key: t.name,
|
|
3070
|
+
name: t.name,
|
|
3071
|
+
theme: t.theme
|
|
3072
|
+
})));
|
|
3073
|
+
};
|
|
2808
3074
|
|
|
2809
3075
|
// src/index.ts
|
|
2810
3076
|
var theme = terminalTheme;
|
|
@@ -2843,7 +3109,9 @@ export {
|
|
|
2843
3109
|
scaleThemeFonts,
|
|
2844
3110
|
responsive,
|
|
2845
3111
|
resetFontScale,
|
|
3112
|
+
relativeLuminance,
|
|
2846
3113
|
regalTheme,
|
|
3114
|
+
parseColor,
|
|
2847
3115
|
overrideColors,
|
|
2848
3116
|
neuralPulseTheme,
|
|
2849
3117
|
mergeThemes,
|
|
@@ -2856,6 +3124,7 @@ export {
|
|
|
2856
3124
|
iceTangerineTheme,
|
|
2857
3125
|
iceTangerineDarkTheme,
|
|
2858
3126
|
humanCentricTheme,
|
|
3127
|
+
gradeContrast,
|
|
2859
3128
|
getZIndex,
|
|
2860
3129
|
getSpace,
|
|
2861
3130
|
getShadow,
|
|
@@ -2863,6 +3132,7 @@ export {
|
|
|
2863
3132
|
getMode,
|
|
2864
3133
|
getFontSize,
|
|
2865
3134
|
getColor,
|
|
3135
|
+
evaluateThemeContrast,
|
|
2866
3136
|
enterpriseTheme,
|
|
2867
3137
|
defaultTerminalTheme,
|
|
2868
3138
|
defaultMarkdownTheme,
|
|
@@ -2870,7 +3140,11 @@ export {
|
|
|
2870
3140
|
src_default as default,
|
|
2871
3141
|
decreaseFontScale,
|
|
2872
3142
|
createStyle,
|
|
3143
|
+
contrastRatio,
|
|
2873
3144
|
addMode,
|
|
3145
|
+
WCAG_THRESHOLDS,
|
|
2874
3146
|
ThemeShowcase,
|
|
2875
|
-
ThemeProvider
|
|
3147
|
+
ThemeProvider,
|
|
3148
|
+
ContrastReport,
|
|
3149
|
+
CONTRAST_PAIRS
|
|
2876
3150
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@principal-ade/industry-theme",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Theme components and styles for industry-themed markdown",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -41,10 +41,11 @@
|
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@chromatic-com/storybook": "^4.1.3",
|
|
43
43
|
"@eslint/js": "^9.32.0",
|
|
44
|
-
"@storybook/addon-
|
|
45
|
-
"@storybook/addon-
|
|
46
|
-
"@storybook/addon-
|
|
47
|
-
"@storybook/
|
|
44
|
+
"@storybook/addon-a11y": "^10.4.2",
|
|
45
|
+
"@storybook/addon-docs": "^10.4.2",
|
|
46
|
+
"@storybook/addon-links": "^10.4.2",
|
|
47
|
+
"@storybook/addon-onboarding": "^10.4.2",
|
|
48
|
+
"@storybook/react-vite": "^10.4.2",
|
|
48
49
|
"@types/bun": "latest",
|
|
49
50
|
"@types/react": "^19.1.12",
|
|
50
51
|
"@types/react-dom": "^19.0.0",
|
|
@@ -53,11 +54,11 @@
|
|
|
53
54
|
"eslint-config-prettier": "^10.1.8",
|
|
54
55
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
55
56
|
"eslint-plugin-import": "^2.32.0",
|
|
56
|
-
"eslint-plugin-storybook": "^10.
|
|
57
|
+
"eslint-plugin-storybook": "^10.4.2",
|
|
57
58
|
"prettier": "^3.6.2",
|
|
58
59
|
"react": "^19.2.4",
|
|
59
60
|
"react-dom": "^19.2.4",
|
|
60
|
-
"storybook": "^10.
|
|
61
|
+
"storybook": "^10.4.2",
|
|
61
62
|
"typescript": "^5.0.4",
|
|
62
63
|
"typescript-eslint": "^8.38.0",
|
|
63
64
|
"vite": "^6.0.7"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { ContrastReport } from './ContrastReport';
|
|
3
|
+
import {
|
|
4
|
+
terminalTheme,
|
|
5
|
+
regalTheme,
|
|
6
|
+
matrixTheme,
|
|
7
|
+
matrixMinimalTheme,
|
|
8
|
+
slateTheme,
|
|
9
|
+
slateNeonTheme,
|
|
10
|
+
slateGoldTheme,
|
|
11
|
+
iceTangerineTheme,
|
|
12
|
+
iceTangerineDarkTheme,
|
|
13
|
+
enterpriseTheme,
|
|
14
|
+
neuralPulseTheme,
|
|
15
|
+
humanCentricTheme,
|
|
16
|
+
landingPageTheme,
|
|
17
|
+
landingPageLightTheme,
|
|
18
|
+
} from './themes';
|
|
19
|
+
|
|
20
|
+
const allThemes = [
|
|
21
|
+
{ name: 'Terminal', theme: terminalTheme },
|
|
22
|
+
{ name: 'Regal', theme: regalTheme },
|
|
23
|
+
{ name: 'Matrix', theme: matrixTheme },
|
|
24
|
+
{ name: 'Matrix Minimal', theme: matrixMinimalTheme },
|
|
25
|
+
{ name: 'Slate', theme: slateTheme },
|
|
26
|
+
{ name: 'Slate Neon', theme: slateNeonTheme },
|
|
27
|
+
{ name: 'Slate Gold', theme: slateGoldTheme },
|
|
28
|
+
{ name: 'Ice Tangerine', theme: iceTangerineTheme },
|
|
29
|
+
{ name: 'Ice Tangerine Dark', theme: iceTangerineDarkTheme },
|
|
30
|
+
{ name: 'Enterprise', theme: enterpriseTheme },
|
|
31
|
+
{ name: 'Neural Pulse', theme: neuralPulseTheme },
|
|
32
|
+
{ name: 'Human-Centric', theme: humanCentricTheme },
|
|
33
|
+
{ name: 'Landing Page (Dark)', theme: landingPageTheme },
|
|
34
|
+
{ name: 'Landing Page (Light)', theme: landingPageLightTheme },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const meta: Meta<typeof ContrastReport> = {
|
|
38
|
+
title: 'Themes/Contrast Report',
|
|
39
|
+
component: ContrastReport,
|
|
40
|
+
parameters: {
|
|
41
|
+
layout: 'fullscreen',
|
|
42
|
+
// This story renders raw color swatches by design; axe would double-report
|
|
43
|
+
// the same contrast findings the table already surfaces.
|
|
44
|
+
a11y: { disable: true },
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default meta;
|
|
49
|
+
type Story = StoryObj<typeof ContrastReport>;
|
|
50
|
+
|
|
51
|
+
/** WCAG audit across every theme in the library. */
|
|
52
|
+
export const AllThemes: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
title: 'WCAG Contrast Report — All Themes',
|
|
55
|
+
themes: allThemes,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** Tangerine family + Slate Neon side by side. */
|
|
60
|
+
export const TangerineAndSlateNeon: Story = {
|
|
61
|
+
args: {
|
|
62
|
+
title: 'Ice Tangerine, Ice Tangerine Dark & Slate Neon',
|
|
63
|
+
themes: [
|
|
64
|
+
{ name: 'Ice Tangerine', theme: iceTangerineTheme },
|
|
65
|
+
{ name: 'Ice Tangerine Dark', theme: iceTangerineDarkTheme },
|
|
66
|
+
{ name: 'Slate Neon', theme: slateNeonTheme },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const Terminal: Story = {
|
|
72
|
+
args: { title: 'Terminal Theme', theme: terminalTheme },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const LandingPageDark: Story = {
|
|
76
|
+
args: { title: 'Landing Page (Dark)', theme: landingPageTheme },
|
|
77
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { ContrastLevel, ContrastResult, evaluateThemeContrast, WCAG_THRESHOLDS } from './contrast';
|
|
4
|
+
|
|
5
|
+
import { Theme } from './index';
|
|
6
|
+
|
|
7
|
+
export interface ContrastReportProps {
|
|
8
|
+
/** A single theme to audit. */
|
|
9
|
+
theme?: Theme;
|
|
10
|
+
/** Multiple named themes to audit side by side. */
|
|
11
|
+
themes?: { name: string; theme: Theme }[];
|
|
12
|
+
title?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LEVEL_STYLE: Record<ContrastLevel, { bg: string; fg: string; label: string }> = {
|
|
16
|
+
AAA: { bg: '#0f7b3f', fg: '#ffffff', label: 'AAA' },
|
|
17
|
+
AA: { bg: '#1f6feb', fg: '#ffffff', label: 'AA' },
|
|
18
|
+
fail: { bg: '#cf222e', fg: '#ffffff', label: 'FAIL' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const ui = {
|
|
22
|
+
fontFamily: "ui-sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif",
|
|
23
|
+
text: '#1c2128',
|
|
24
|
+
subtle: '#57606a',
|
|
25
|
+
border: '#d0d7de',
|
|
26
|
+
surface: '#ffffff',
|
|
27
|
+
panel: '#f6f8fa',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const Badge: React.FC<{ level: ContrastLevel }> = ({ level }) => {
|
|
31
|
+
const s = LEVEL_STYLE[level];
|
|
32
|
+
return (
|
|
33
|
+
<span
|
|
34
|
+
style={{
|
|
35
|
+
display: 'inline-block',
|
|
36
|
+
minWidth: 44,
|
|
37
|
+
textAlign: 'center',
|
|
38
|
+
padding: '2px 8px',
|
|
39
|
+
borderRadius: 999,
|
|
40
|
+
fontSize: 11,
|
|
41
|
+
fontWeight: 700,
|
|
42
|
+
letterSpacing: 0.4,
|
|
43
|
+
backgroundColor: s.bg,
|
|
44
|
+
color: s.fg,
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{s.label}
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const Swatch: React.FC<{ fg: string; bg: string }> = ({ fg, bg }) => (
|
|
53
|
+
<span
|
|
54
|
+
title={`${fg} on ${bg}`}
|
|
55
|
+
style={{
|
|
56
|
+
display: 'inline-flex',
|
|
57
|
+
alignItems: 'center',
|
|
58
|
+
justifyContent: 'center',
|
|
59
|
+
width: 40,
|
|
60
|
+
height: 24,
|
|
61
|
+
borderRadius: 4,
|
|
62
|
+
border: `1px solid ${ui.border}`,
|
|
63
|
+
backgroundColor: bg,
|
|
64
|
+
color: fg,
|
|
65
|
+
fontSize: 13,
|
|
66
|
+
fontWeight: 700,
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
Aa
|
|
70
|
+
</span>
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const Row: React.FC<{ r: ContrastResult }> = ({ r }) => {
|
|
74
|
+
const failed = r.level === 'fail';
|
|
75
|
+
return (
|
|
76
|
+
<tr style={{ backgroundColor: failed ? '#fff5f5' : 'transparent' }}>
|
|
77
|
+
<td style={cell}>
|
|
78
|
+
<Swatch fg={r.fgColor} bg={r.bgColor} />
|
|
79
|
+
</td>
|
|
80
|
+
<td style={{ ...cell, fontWeight: 500 }}>
|
|
81
|
+
{r.pair.label}
|
|
82
|
+
<div style={{ fontSize: 11, color: ui.subtle, fontFamily: 'monospace' }}>
|
|
83
|
+
{r.fgColor} / {r.bgColor}
|
|
84
|
+
</div>
|
|
85
|
+
</td>
|
|
86
|
+
<td style={{ ...cell, textAlign: 'right', fontVariantNumeric: 'tabular-nums' }}>
|
|
87
|
+
<span style={{ fontWeight: 700, color: failed ? '#cf222e' : ui.text }}>
|
|
88
|
+
{r.ratio === null ? '—' : `${r.ratio.toFixed(2)}:1`}
|
|
89
|
+
</span>
|
|
90
|
+
</td>
|
|
91
|
+
<td
|
|
92
|
+
style={{
|
|
93
|
+
...cell,
|
|
94
|
+
textAlign: 'right',
|
|
95
|
+
color: ui.subtle,
|
|
96
|
+
fontVariantNumeric: 'tabular-nums',
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
{r.required}:1
|
|
100
|
+
</td>
|
|
101
|
+
<td style={{ ...cell, textAlign: 'center', textTransform: 'capitalize', color: ui.subtle }}>
|
|
102
|
+
{r.pair.use}
|
|
103
|
+
</td>
|
|
104
|
+
<td style={{ ...cell, textAlign: 'center' }}>
|
|
105
|
+
<Badge level={r.level} />
|
|
106
|
+
</td>
|
|
107
|
+
</tr>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const cell: React.CSSProperties = {
|
|
112
|
+
padding: '8px 12px',
|
|
113
|
+
borderBottom: `1px solid ${ui.border}`,
|
|
114
|
+
fontSize: 13,
|
|
115
|
+
verticalAlign: 'middle',
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const headCell: React.CSSProperties = {
|
|
119
|
+
padding: '8px 12px',
|
|
120
|
+
textAlign: 'left',
|
|
121
|
+
fontSize: 11,
|
|
122
|
+
fontWeight: 700,
|
|
123
|
+
textTransform: 'uppercase',
|
|
124
|
+
letterSpacing: 0.5,
|
|
125
|
+
color: ui.subtle,
|
|
126
|
+
borderBottom: `2px solid ${ui.border}`,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const ThemeTable: React.FC<{ name: string; theme: Theme }> = ({ name, theme }) => {
|
|
130
|
+
const report = evaluateThemeContrast(theme);
|
|
131
|
+
const fails = report.failures.length;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<section
|
|
135
|
+
style={{
|
|
136
|
+
marginBottom: 28,
|
|
137
|
+
border: `1px solid ${ui.border}`,
|
|
138
|
+
borderRadius: 8,
|
|
139
|
+
overflow: 'hidden',
|
|
140
|
+
backgroundColor: ui.surface,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
<header
|
|
144
|
+
style={{
|
|
145
|
+
display: 'flex',
|
|
146
|
+
alignItems: 'center',
|
|
147
|
+
justifyContent: 'space-between',
|
|
148
|
+
gap: 12,
|
|
149
|
+
padding: '12px 16px',
|
|
150
|
+
backgroundColor: ui.panel,
|
|
151
|
+
borderBottom: `1px solid ${ui.border}`,
|
|
152
|
+
}}
|
|
153
|
+
>
|
|
154
|
+
<h3 style={{ margin: 0, fontSize: 16, color: ui.text }}>{name}</h3>
|
|
155
|
+
<span
|
|
156
|
+
style={{
|
|
157
|
+
fontSize: 12,
|
|
158
|
+
fontWeight: 700,
|
|
159
|
+
padding: '4px 10px',
|
|
160
|
+
borderRadius: 999,
|
|
161
|
+
backgroundColor: report.passesAA ? '#dafbe1' : '#ffebe9',
|
|
162
|
+
color: report.passesAA ? '#0f7b3f' : '#cf222e',
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
{report.passesAA ? '✓ Passes AA' : `${fails} AA failure${fails === 1 ? '' : 's'}`}
|
|
166
|
+
</span>
|
|
167
|
+
</header>
|
|
168
|
+
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
169
|
+
<thead>
|
|
170
|
+
<tr>
|
|
171
|
+
<th style={headCell}>Sample</th>
|
|
172
|
+
<th style={headCell}>Pair</th>
|
|
173
|
+
<th style={{ ...headCell, textAlign: 'right' }}>Ratio</th>
|
|
174
|
+
<th style={{ ...headCell, textAlign: 'right' }}>Req. (AA)</th>
|
|
175
|
+
<th style={{ ...headCell, textAlign: 'center' }}>Use</th>
|
|
176
|
+
<th style={{ ...headCell, textAlign: 'center' }}>Grade</th>
|
|
177
|
+
</tr>
|
|
178
|
+
</thead>
|
|
179
|
+
<tbody>
|
|
180
|
+
{report.results.map((r, i) => (
|
|
181
|
+
<Row key={i} r={r} />
|
|
182
|
+
))}
|
|
183
|
+
</tbody>
|
|
184
|
+
</table>
|
|
185
|
+
</section>
|
|
186
|
+
);
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Renders a WCAG contrast audit for one or more themes — a deterministic,
|
|
191
|
+
* exhaustive view of every declared foreground/background pairing with its
|
|
192
|
+
* ratio and AA/AAA grade. Complements `@storybook/addon-a11y`, which only
|
|
193
|
+
* checks contrast on actually-rendered text.
|
|
194
|
+
*/
|
|
195
|
+
export const ContrastReport: React.FC<ContrastReportProps> = ({
|
|
196
|
+
theme,
|
|
197
|
+
themes,
|
|
198
|
+
title = 'WCAG Contrast Report',
|
|
199
|
+
}) => {
|
|
200
|
+
const list = themes ?? (theme ? [{ name: title, theme }] : []);
|
|
201
|
+
|
|
202
|
+
return (
|
|
203
|
+
<div
|
|
204
|
+
style={{
|
|
205
|
+
fontFamily: ui.fontFamily,
|
|
206
|
+
color: ui.text,
|
|
207
|
+
backgroundColor: ui.panel,
|
|
208
|
+
padding: 24,
|
|
209
|
+
minHeight: '100vh',
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
<header style={{ marginBottom: 20 }}>
|
|
213
|
+
<h1 style={{ margin: '0 0 6px', fontSize: 22 }}>{title}</h1>
|
|
214
|
+
<p style={{ margin: 0, fontSize: 13, color: ui.subtle, maxWidth: 720 }}>
|
|
215
|
+
WCAG 2.x contrast ratios for each theme's declared color roles. Text pairs require AA{' '}
|
|
216
|
+
{WCAG_THRESHOLDS.text.AA}:1 (AAA {WCAG_THRESHOLDS.text.AAA}:1); large text requires{' '}
|
|
217
|
+
{WCAG_THRESHOLDS.large.AA}:1; borders and other non-text UI require{' '}
|
|
218
|
+
{WCAG_THRESHOLDS.ui.AA}:1.
|
|
219
|
+
</p>
|
|
220
|
+
</header>
|
|
221
|
+
{list.map((t) => (
|
|
222
|
+
<ThemeTable key={t.name} name={t.name} theme={t.theme} />
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
);
|
|
226
|
+
};
|