Skip to content

[Appdir] Thumbnail recoloring #3776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 15, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/* stylelint-disable csstools/value-no-unknown-custom-properties */
.imageAsModule {
width: var(--website-svg-size);
height: var(--website-svg-size);
}
82 changes: 82 additions & 0 deletions aksel.nav.no/website/app/_ui/image-as-svg/ImageAsSvg.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import DOMPurify from "isomorphic-dompurify";
import styles from "./ImageAsSvg.module.css";

/**
* Utility function to fetch and format a image as SVG with recoloring.
*/
async function ImageAsThemedSvg({
url,
size,
className,
}: {
url?: string;
size: number;
className?: string;
}) {
if (!url) {
return null;
}

const svgString = await fetch(url)
.then((res) => res.text())
.catch(() => null);

if (!svgString) {
return null;
}

const formattedSvgString = formatSvgString(svgString);

const cleanedSvg = DOMPurify.sanitize(formattedSvgString, {
USE_PROFILES: { svg: true },
});

return (
<div
// biome-ignore lint/security/noDangerouslySetInnerHtml: We purify the SVG string, so its safer to use locally here.
dangerouslySetInnerHTML={{ __html: cleanedSvg }}
aria-hidden
style={{ "--website-svg-size": `${size}px` }}
className={className}
/>
);
}

const colorReplacements = {
"#99F6E4": "var(--ax-bg-moderate-hoverA)",
"#AEF4E4": "var(--ax-bg-moderate-hoverA)",
"#003453": "var(--ax-text-subtle)",
"#262626": "var(--ax-text-subtle)",
"#23262A": "var(--ax-text-subtle)",
white: "var(--ax-bg-default)",
"#417DA0": "var(--ax-bg-strong)",
"#D7E6F0": "var(--ax-bg-moderate-hover)",
"#C0D6E4": "var(--ax-bg-moderate-pressed)",
"#E3EFF7": "var(--ax-bg-moderate)",
"#005A92": "var(--ax-bg-strong)",
};

/**
* Formats the SVG string by replacing colors and adding classes.
*/
function formatSvgString(svgString: string) {
let formattedSvgString = svgString;

formattedSvgString = formattedSvgString.replace(
"<svg",
`<svg class="${styles.imageAsModule}" aria-hidden`,
);

Object.keys(colorReplacements).forEach((color) => {
const replacement = colorReplacements[color];

formattedSvgString = formattedSvgString.replace(
new RegExp(color, "g"),
replacement,
);
});

return formattedSvgString;
}

export { ImageAsThemedSvg };
Original file line number Diff line number Diff line change
Expand Up @@ -72,16 +72,13 @@
align-items: center;
aspect-ratio: 24 / 9;
border-radius: 16px;
background-color: var(--ax-bg-brand-blue-moderate);
background-color: var(--ax-bg-moderate);
margin-block-start: var(--ax-space-28);
overflow: hidden;
}

.thumbnailImage {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 10;
}

.thumbnailCube {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,89 +1,20 @@
import { ImageAsThemedSvg } from "@/app/_ui/image-as-svg/ImageAsSvg";
import styles from "./Designsystemet.module.css";

function DesignsystemetThumbnail() {
function DesignsystemetThumbnail({ thumbnailUrl }: { thumbnailUrl?: string }) {
if (!thumbnailUrl) {
return null;
}

return (
<div className={styles.thumbnailContainer}>
<CubeShape />
<Thumbnail />
</div>
);
}

function Thumbnail() {
return (
<svg
viewBox="0 0 289 289"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
className={styles.thumbnailImage}
>
<path
d="M35.06 91.7645C35.06 82.6549 42.4449 75.27 51.5545 75.27H237.445C246.555 75.27 253.94 82.6549 253.94 91.7646V197.016C253.94 206.125 246.555 213.51 237.445 213.51H51.5546C42.4449 213.51 35.06 206.125 35.06 197.015V91.7645Z"
fill="white"
/>
<mask id="path-3-inside-1_1364_1724" fill="white">
<path d="M35.06 78.0191C35.06 76.5008 36.2908 75.27 37.8091 75.27H251.191C252.709 75.27 253.94 76.5008 253.94 78.0191V118.601C253.94 120.119 252.709 121.35 251.191 121.35H37.8091C36.2908 121.35 35.06 120.119 35.06 118.601V78.0191Z" />
</mask>
<path
d="M35.06 75.27H253.94H35.06ZM253.94 118.601C253.94 121.71 251.42 124.23 248.311 124.23H40.6891C37.5802 124.23 35.06 121.71 35.06 118.601V118.47C35.06 118.47 36.2908 118.47 37.8091 118.47H251.191C252.709 118.47 253.94 118.47 253.94 118.47V118.601ZM35.06 121.35V75.27V121.35ZM253.94 75.27V121.35V75.27Z"
fill="#C0D6E4"
mask="url(#path-3-inside-1_1364_1724)"
<ImageAsThemedSvg
size={280}
url={thumbnailUrl}
className={styles.thumbnailImage}
/>
<path
d="M51.1398 92.23L59.0598 100.15L66.9798 92.23"
stroke="#417DA0"
strokeWidth="2.88"
strokeLinecap="round"
strokeLinejoin="round"
/>
<rect
x="83.62"
y="90.79"
width="122.4"
height="11.52"
rx="5.76"
fill="#417DA0"
/>
<mask id="path-7-inside-2_1364_1724" fill="white">
<path d="M35.06 124.099C35.06 122.581 36.2908 121.35 37.8091 121.35H251.191C252.709 121.35 253.94 122.581 253.94 124.099V164.681C253.94 166.199 252.709 167.43 251.191 167.43H37.8091C36.2908 167.43 35.06 166.199 35.06 164.681V124.099Z" />
</mask>
<path
d="M35.06 121.35H253.94H35.06ZM253.94 164.681C253.94 167.79 251.42 170.31 248.311 170.31H40.6891C37.5802 170.31 35.06 167.79 35.06 164.681V164.55C35.06 164.55 36.2908 164.55 37.8091 164.55H251.191C252.709 164.55 253.94 164.55 253.94 164.55V164.681ZM35.06 167.43V121.35V167.43ZM253.94 121.35V167.43V121.35Z"
fill="#C0D6E4"
mask="url(#path-7-inside-2_1364_1724)"
/>
<path
d="M51.1398 138.31L59.0598 146.23L66.9798 138.31"
stroke="#417DA0"
strokeWidth="2.88"
strokeLinecap="round"
strokeLinejoin="round"
/>
<rect
x="83.62"
y="136.87"
width="136.8"
height="11.52"
rx="5.76"
fill="#417DA0"
/>
<path
d="M51.1398 184.39L59.0598 192.31L66.9798 184.39"
stroke="#417DA0"
strokeWidth="2.88"
strokeLinecap="round"
strokeLinejoin="round"
/>
<rect
x="83.62"
y="182.95"
width="109.44"
height="11.52"
rx="5.76"
fill="#417DA0"
/>
</svg>
</div>
);
}

Expand All @@ -102,19 +33,19 @@ function CubeShape() {
fillRule="evenodd"
clipRule="evenodd"
d="M778.342 -296.311C776.611 -298.043 773.803 -298.043 772.071 -296.311L533.962 -58.2022C532.231 -56.4706 532.231 -53.663 533.962 -51.9313L772.071 186.178C773.803 187.909 776.611 187.909 778.342 186.178L1016.45 -51.9313C1018.18 -53.6629 1018.18 -56.4705 1016.45 -58.2022L778.342 -296.311ZM775.207 -286.905L1007.04 -55.0667L775.207 176.771L543.369 -55.0668L775.207 -286.905Z"
fill="#EEF6FC"
fill="var(--ax-bg-brand-blue-soft)"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M302.748 -62.907C301.572 -62.907 300.445 -62.4398 299.613 -61.6082L61.5029 176.502C60.2347 177.77 59.8554 179.677 60.5417 181.334C61.228 182.991 62.8449 184.072 64.6384 184.072L302.391 184.072C303.567 184.072 304.695 183.604 305.527 182.773L543.637 -55.3373C544.905 -56.6055 545.285 -58.5127 544.598 -60.1697C543.912 -61.8266 542.295 -62.907 540.502 -62.907L302.748 -62.907ZM304.585 -54.0386L529.797 -54.0386L300.555 175.203H75.3435L304.585 -54.0386Z"
fill="#EEF6FC"
fill="var(--ax-bg-brand-blue-soft)"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M71.1787 173.097C69.447 171.365 66.6394 171.365 64.9078 173.097L-173.201 411.206C-174.933 412.938 -174.933 415.745 -173.201 417.477L64.9077 655.586C66.6394 657.318 69.447 657.318 71.1786 655.586L309.288 417.477C311.019 415.745 311.019 412.938 309.288 411.206L71.1787 173.097ZM68.0432 182.503L299.881 414.341L68.0432 646.18L-163.795 414.341L68.0432 182.503Z"
fill="#EEF6FC"
fill="var(--ax-bg-brand-blue-soft)"
/>
</svg>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { PortableTextBlock } from "next-sanity";
import { Image } from "sanity";
import { BodyShort, Box, HStack, Heading, Tag } from "@navikt/ds-react";
import {
GRUNNLEGGENDE_BY_SLUG_QUERYResult,
KOMPONENT_BY_SLUG_QUERYResult,
MONSTER_MALER_BY_SLUG_QUERYResult,
} from "@/app/_sanity/query-types";
import { urlForImage } from "@/app/_sanity/utils";
import { CustomPortableText } from "@/app/_ui/portable-text/CustomPortableText";
import { getStatusTag } from "@/app/_ui/theming/theme-config";
import { DesignsystemetEyebrow } from "@/app/dev/(designsystemet)/_ui/Designsystemet.eyebrow";
Expand Down Expand Up @@ -51,8 +53,10 @@ async function DesignsystemetPageHeader({ data }: DesignsystemetPageT) {

const isComponentPage = data?._type === "komponent_artikkel";

const imageUrl = urlForImage(data?.status?.bilde as Image)?.url();

return (
<Box marginBlock="space-0 space-28">
<Box marginBlock="space-0 space-28" data-color-role={statusTag?.colorRole}>
<DesignsystemetEyebrow type={data?._type} />

<Box marginBlock="space-0 space-8" asChild>
Expand Down Expand Up @@ -87,7 +91,7 @@ async function DesignsystemetPageHeader({ data }: DesignsystemetPageT) {
</HStack>
{isComponentPage && <KomponentLinks data={data} />}

<DesignsystemetThumbnail />
<DesignsystemetThumbnail thumbnailUrl={imageUrl} />
</Box>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
aspect-ratio: 16 / 10;
border-start-start-radius: calc(var(--ax-border-radius-xlarge) - 1px);
border-start-end-radius: calc(var(--ax-border-radius-xlarge) - 1px);
background: var(--ax-bg-moderate);
background: var(--ax-bg-soft);
}

/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
:global(.dark) .overviewImageWrapper {
background: var(--ax-bg-neutral-soft);
}

.overviewCardHeading {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import NextImage from "next/image";
import type { Image } from "sanity";
import {
Bleed,
Expand All @@ -11,6 +10,7 @@ import {
} from "@navikt/ds-react";
import { DESIGNSYSTEM_OVERVIEW_BY_CATEGORY_QUERYResult } from "@/app/_sanity/query-types";
import { urlForImage } from "@/app/_sanity/utils";
import { ImageAsThemedSvg } from "@/app/_ui/image-as-svg/ImageAsSvg";
import { getStatusTag } from "@/app/_ui/theming/theme-config";
import { MarkdownText } from "@/app/_ui/typography/MarkdownText";
import {
Expand Down Expand Up @@ -79,77 +79,37 @@ function DesignsystemetOverviewCard({
return (
<LinkCard data-color-role={statusTag?.colorRole} autoLayout={false}>
<VStack gap="space-16">
<Bleed marginInline="space-20" marginBlock="space-16 0">
<span className={styles.overviewImageWrapper}>
{imageUrl ? (
<NextImage
src={imageUrl}
width={200}
height={200}
alt={page?.heading + " thumbnail"}
aria-hidden
/>
) : (
<FallbackSvg />
)}
</span>
</Bleed>

<LinkCardTitle as="h2">
<LinkCardAnchor href={`/${page?.slug}`}>
<HStack as="span" gap="space-8" align="center">
<span>{page?.heading} </span>
{statusTagWithoutStable && (
<Box position="relative" asChild>
<Bleed marginInline="space-20" marginBlock="space-16 0">
<span className={styles.overviewImageWrapper}>
<ImageAsThemedSvg url={imageUrl} size={200} />
</span>
{statusTagWithoutStable && (
<Box asChild position="absolute" bottom="space-8" left="space-8">
<Tag
size="small"
variant="success"
variant="alt1-filled"
data-color-role={statusTag?.colorRole}
>
{/* TODO: Remove underline from tag */}
{statusTagWithoutStable.text}
</Tag>
)}
</HStack>
</LinkCardAnchor>
</Box>
)}
</Bleed>
</Box>

<LinkCardTitle as="h2">
<HStack as="span" gap="space-8" align="center">
<LinkCardAnchor href={`/${page?.slug}`}>
{page?.heading}
</LinkCardAnchor>
</HStack>
</LinkCardTitle>
</VStack>
</LinkCard>
);
}

function FallbackSvg() {
return (
<svg
width="200"
height="200"
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M100 153C129.271 153 153 129.271 153 100C153 70.7289 129.271 47 100 47C70.7289 47 47 70.7289 47 100C47 129.271 70.7289 153 100 153ZM100 165C135.899 165 165 135.899 165 100C165 64.1015 135.899 35 100 35C64.1015 35 35 64.1015 35 100C35 135.899 64.1015 165 100 165Z"
fill="#001630"
fillOpacity="0.188235"
/>
<path
d="M96.768 67C97.5378 65.6667 99.4622 65.6667 100.232 67L112.789 88.75C113.559 90.0833 112.597 91.75 111.057 91.75H85.9426C84.403 91.75 83.4408 90.0833 84.2106 88.75L96.768 67Z"
fill="#5D6573"
/>
<path
d="M96 114.5C96 121.956 89.9558 128 82.5 128C75.0442 128 69 121.956 69 114.5C69 107.044 75.0442 101 82.5 101C89.9558 101 96 107.044 96 114.5Z"
fill="#5D6573"
/>
<path
d="M105 104C105 102.895 105.895 102 107 102H129C130.105 102 131 102.895 131 104V126C131 127.105 130.105 128 129 128H107C105.895 128 105 127.105 105 126V104Z"
fill="#5D6573"
/>
</svg>
);
}

function sortDesignsystemetOverviewList(
list: DESIGNSYSTEM_OVERVIEW_BY_CATEGORY_QUERYResult,
) {
Expand Down
Loading
Loading