From dea2a936a13a01b20961393527155baca5a23551 Mon Sep 17 00:00:00 2001 From: Maaxxs <61059039+MaaxxsDev@users.noreply.github.com> Date: Tue, 7 Jul 2026 23:04:24 +0200 Subject: [PATCH] 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 --- hifi-src/src/pages/public/ModelPage.jsx | 34 ++++++++++++++----------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/hifi-src/src/pages/public/ModelPage.jsx b/hifi-src/src/pages/public/ModelPage.jsx index da47b42..aa1c804 100644 --- a/hifi-src/src/pages/public/ModelPage.jsx +++ b/hifi-src/src/pages/public/ModelPage.jsx @@ -124,25 +124,33 @@ export default function ModelPage() { const accentRgb = accentRgbFloat.map(Math.round); const bgHsl = rgbToHsl(...bgRgb); const accentHsl = rgbToHsl(...accentRgbFloat); + const bgL = bgHsl[2]; // Ab hier reicht der Hintergrund nicht mehr zum Kontrastieren mit dunklem Text - // Bronze/Gold-Mitteltoene brauchen (wie die Buttons) helle statt dunkle Schrift. // Diese Entscheidung haengt bewusst an der Flaechenfarbe, nicht am Lichtschein // unten, damit der Text unabhaengig vom Material IMMER gut lesbar bleibt. - const isDarkCard = bgHsl[2] < 60; - // Preis/Icon brauchen eine kraeftigere, hellere Variante des Materialtons als der - // Rahmen - der reine Akzent waere auf dunklem Untergrund selbst zu dunkel zum Lesen. - const priceRgb = isDarkCard - ? hslToRgb(accentHsl[0], Math.min(accentHsl[1] + 10, 90), Math.min(accentHsl[2] + 25, 90)) - : accentRgb; + const isDarkCard = bgL < 60; + // Garantiert einen Mindestabstand zur tatsaechlichen Hintergrund-Helligkeit statt + // sich auf einen festen Hell/Dunkel-Klassenwechsel zu verlassen - sonst kann ein + // Hintergrund GENAU im Grenzbereich (z.B. ein kuehles Mittelgrau nahe der 60%-Schwelle) + // eine "eigentlich helle" Textfarbe bekommen, die kaum noch Kontrast zum Hintergrund hat. + 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); // Leichter "Lichtschein" von oben rechts statt flacher Flaeche - kommt von oben // rechts, weil dort nie Text steht (Titel/Preis/Liste sind linksbuendig), damit die // 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 { isDarkCard, + mutedColor, priceColor: `rgb(${priceRgb.join(', ')})`, iconChipStyle: { backgroundColor: `rgba(${accentRgb.join(', ')}, 0.16)`, @@ -190,7 +198,7 @@ export default function ModelPage() {
{packages.map((pkg) => { - const { isDarkCard, style, priceColor, iconChipStyle } = styleOf(pkg); + const { isDarkCard, style, priceColor, mutedColor, iconChipStyle } = styleOf(pkg); return (
@@ -210,13 +218,13 @@ export default function ModelPage() { {pkg.name} {pkg.tagline && ( -

+

{pkg.tagline}

)}
-

+

{t('modelPage.totalPrice')}

@@ -224,11 +232,7 @@ export default function ModelPage() {

-
    +
      {pkg.products.map((product) => (
    • {product.name_override || product.scraped_name || t('modelPage.productLoading')}