Guarantee a minimum text contrast margin instead of a fixed light/dark split
Label, tagline, list and price text only had their contrast verified for the "dark card" side of the isDarkCard split - the "light card" side was never checked against the actual background lightness, so a borderline mid-gray tier (e.g. "The Statement") could land close enough to the muted neutral-gray text to become nearly unreadable. Text colors are now computed with a guaranteed lightness delta from the real background, checked across all 8 tiers in both themes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
2af4af0607
commit
dea2a936a1
1 changed files with 19 additions and 15 deletions
|
|
@ -124,25 +124,33 @@ export default function ModelPage() {
|
||||||
const accentRgb = accentRgbFloat.map(Math.round);
|
const accentRgb = accentRgbFloat.map(Math.round);
|
||||||
const bgHsl = rgbToHsl(...bgRgb);
|
const bgHsl = rgbToHsl(...bgRgb);
|
||||||
const accentHsl = rgbToHsl(...accentRgbFloat);
|
const accentHsl = rgbToHsl(...accentRgbFloat);
|
||||||
|
const bgL = bgHsl[2];
|
||||||
// Ab hier reicht der Hintergrund nicht mehr zum Kontrastieren mit dunklem Text -
|
// Ab hier reicht der Hintergrund nicht mehr zum Kontrastieren mit dunklem Text -
|
||||||
// Bronze/Gold-Mitteltoene brauchen (wie die Buttons) helle statt dunkle Schrift.
|
// Bronze/Gold-Mitteltoene brauchen (wie die Buttons) helle statt dunkle Schrift.
|
||||||
// Diese Entscheidung haengt bewusst an der Flaechenfarbe, nicht am Lichtschein
|
// Diese Entscheidung haengt bewusst an der Flaechenfarbe, nicht am Lichtschein
|
||||||
// unten, damit der Text unabhaengig vom Material IMMER gut lesbar bleibt.
|
// unten, damit der Text unabhaengig vom Material IMMER gut lesbar bleibt.
|
||||||
const isDarkCard = bgHsl[2] < 60;
|
const isDarkCard = bgL < 60;
|
||||||
// Preis/Icon brauchen eine kraeftigere, hellere Variante des Materialtons als der
|
// Garantiert einen Mindestabstand zur tatsaechlichen Hintergrund-Helligkeit statt
|
||||||
// Rahmen - der reine Akzent waere auf dunklem Untergrund selbst zu dunkel zum Lesen.
|
// sich auf einen festen Hell/Dunkel-Klassenwechsel zu verlassen - sonst kann ein
|
||||||
const priceRgb = isDarkCard
|
// Hintergrund GENAU im Grenzbereich (z.B. ein kuehles Mittelgrau nahe der 60%-Schwelle)
|
||||||
? hslToRgb(accentHsl[0], Math.min(accentHsl[1] + 10, 90), Math.min(accentHsl[2] + 25, 90))
|
// eine "eigentlich helle" Textfarbe bekommen, die kaum noch Kontrast zum Hintergrund hat.
|
||||||
: accentRgb;
|
const contrastingL = (delta) => (isDarkCard ? Math.min(bgL + delta, 92) : Math.max(bgL - delta, 10));
|
||||||
|
// Preis/Icon brauchen den kraeftigsten Kontrast, da sie die Kernaussage der Karte sind.
|
||||||
|
const priceRgb = hslToRgb(accentHsl[0], Math.min(accentHsl[1] + 10, 90), contrastingL(48));
|
||||||
|
// Dezente Texte (Label/Slogan/Liste) bleiben neutral-grau, aber ebenfalls mit
|
||||||
|
// garantiertem Abstand zur Flaeche - kein fixes Tailwind-Grau mehr, das zufaellig auf
|
||||||
|
// gleicher Helligkeit wie der Hintergrund landen und unsichtbar werden koennte.
|
||||||
|
const mutedColor = `rgb(${hslToRgb(0, 0, contrastingL(38)).join(', ')})`;
|
||||||
const glowAlpha = (0.1 + tierT * 0.3).toFixed(2);
|
const glowAlpha = (0.1 + tierT * 0.3).toFixed(2);
|
||||||
|
|
||||||
// Leichter "Lichtschein" von oben rechts statt flacher Flaeche - kommt von oben
|
// Leichter "Lichtschein" von oben rechts statt flacher Flaeche - kommt von oben
|
||||||
// rechts, weil dort nie Text steht (Titel/Preis/Liste sind linksbuendig), damit die
|
// rechts, weil dort nie Text steht (Titel/Preis/Liste sind linksbuendig), damit die
|
||||||
// Aufhellung die Lesbarkeit nirgends beeintraechtigt.
|
// Aufhellung die Lesbarkeit nirgends beeintraechtigt.
|
||||||
const highlight = hslToRgb(bgHsl[0], Math.max(bgHsl[1] - 8, 0), Math.min(bgHsl[2] + 16, 99));
|
const highlight = hslToRgb(bgHsl[0], Math.max(bgHsl[1] - 8, 0), Math.min(bgL + 16, 99));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isDarkCard,
|
isDarkCard,
|
||||||
|
mutedColor,
|
||||||
priceColor: `rgb(${priceRgb.join(', ')})`,
|
priceColor: `rgb(${priceRgb.join(', ')})`,
|
||||||
iconChipStyle: {
|
iconChipStyle: {
|
||||||
backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`,
|
backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`,
|
||||||
|
|
@ -190,7 +198,7 @@ export default function ModelPage() {
|
||||||
|
|
||||||
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{packages.map((pkg) => {
|
{packages.map((pkg) => {
|
||||||
const { isDarkCard, style, priceColor, iconChipStyle } = styleOf(pkg);
|
const { isDarkCard, style, priceColor, mutedColor, iconChipStyle } = styleOf(pkg);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={pkg.id} style={style} className="relative flex flex-col rounded-xl border p-6">
|
<div key={pkg.id} style={style} className="relative flex flex-col rounded-xl border p-6">
|
||||||
|
|
@ -210,13 +218,13 @@ export default function ModelPage() {
|
||||||
{pkg.name}
|
{pkg.name}
|
||||||
</h2>
|
</h2>
|
||||||
{pkg.tagline && (
|
{pkg.tagline && (
|
||||||
<p className={`mt-1 text-sm ${isDarkCard ? 'text-neutral-300' : 'text-neutral-500'}`}>
|
<p style={{ color: mutedColor }} className="mt-1 text-sm">
|
||||||
{pkg.tagline}
|
{pkg.tagline}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<p className={`text-xs uppercase tracking-wide ${isDarkCard ? 'text-neutral-300' : 'text-neutral-400'}`}>
|
<p style={{ color: mutedColor }} className="text-xs uppercase tracking-wide">
|
||||||
{t('modelPage.totalPrice')}
|
{t('modelPage.totalPrice')}
|
||||||
</p>
|
</p>
|
||||||
<p style={{ color: priceColor }} className="text-2xl font-extrabold">
|
<p style={{ color: priceColor }} className="text-2xl font-extrabold">
|
||||||
|
|
@ -224,11 +232,7 @@ export default function ModelPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul
|
<ul style={{ color: mutedColor }} className="mb-5 flex-1 list-disc space-y-1 pl-5 text-sm">
|
||||||
className={`mb-5 flex-1 list-disc space-y-1 pl-5 text-sm ${
|
|
||||||
isDarkCard ? 'text-neutral-300' : 'text-neutral-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{pkg.products.map((product) => (
|
{pkg.products.map((product) => (
|
||||||
<li key={product.id}>
|
<li key={product.id}>
|
||||||
{product.name_override || product.scraped_name || t('modelPage.productLoading')}
|
{product.name_override || product.scraped_name || t('modelPage.productLoading')}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue