This commit is contained in:
Lars Nolden
2026-08-13 23:12:07 +02:00
parent 2f09345ad3
commit a5d773d9f3
26 changed files with 260 additions and 134 deletions
@@ -1,13 +1,6 @@
import { cn } from "@/lib/utils"
import buildingAvif from "@/assets/gebos/building.avif"
import buildingWebp from "@/assets/gebos/building.webp"
import consumptionRing from "@/assets/gebos/consumption-ring.svg"
import smokeAlarmAvif from "@/assets/gebos/smoke-alarm.avif"
import smokeAlarmWebp from "@/assets/gebos/smoke-alarm.webp"
import tealBars from "@/assets/gebos/teal-bars.svg"
import uviBadge from "@/assetAs/gebos/uvi-badge.svg"
import { AssetRender, renders } from ""
import { AssetRender, renders } from "@/components/gebos/site-assets"
/**
* Homepage hero composition from the GebOS visual asset kit:
@@ -26,41 +19,15 @@ function HeroArt({ className }: { className?: string }) {
aria-label="GebOS Produktvisualisierung"
role="img"
>
<img
src={tealBars}
alt=""
className="absolute top-[12%] -right-[5%] z-0 w-[46%]"
/>
<AssetRender
render={renders.barChart}
alt=""
className="absolute top-[52%] left-[10%] w-[16%] scale-200"
className="absolute bottom-[0%] right-[10%] w-[16%] scale-200 z-10"
/>
<picture>
<source srcSet={buildingAvif} type="image/avif" />
<img
src={buildingWebp}
alt="Modernes Mehrfamilienhaus"
className="absolute top-[2%] left-[2%] z-1 h-[90%] w-[96%] object-contain mix-blend-multiply"
/>
</picture>
<picture>
<source srcSet={smokeAlarmAvif} type="image/avif" />
<img
src={smokeAlarmWebp}
alt="Rauchwarnmelder"
className="absolute top-[4%] -left-[2%] z-3 w-[22%] rounded-full mix-blend-multiply"
/>
</picture>
<img
src={uviBadge}
alt=""
className="absolute bottom-[7%] left-[2%] z-4 w-[15%]"
/>
<img
src={consumptionRing}
alt=""
className="absolute -bottom-[2%] -right-[2%] z-5 w-[46%]"
<AssetRender
render={renders.buildingAlpha}
className="absolute top-[2%] left-[2%] z-1 h-[90%] w-[96%]"
imgClassName="h-full w-full object-contain"
/>
</div>
)
@@ -34,13 +34,25 @@ import glassBadge2Avif from "@/assets/gebos/glass-badge-2.avif"
import glassBadge2Webp from "@/assets/gebos/glass-badge-2.webp"
import glassBadge3Avif from "@/assets/gebos/glass-badge-3.avif"
import glassBadge3Webp from "@/assets/gebos/glass-badge-3.webp"
import glassBadge4Avif from "@/assets/gebos/glass-badge-4.avif"
import glassBadge4Webp from "@/assets/gebos/glass-badge-4.webp"
import glassBadge5Avif from "@/assets/gebos/glass-badge-5.avif"
import glassBadge5Webp from "@/assets/gebos/glass-badge-5.webp"
import glassBadge6Avif from "@/assets/gebos/glass-badge-6.avif"
import glassBadge6Webp from "@/assets/gebos/glass-badge-6.webp"
import glassCubes from "@/assets/gebos/glass-cubes.svg"
import glassCubesStackAvif from "@/assets/gebos/glass-cubes-stack.avif"
import glassCubesStackWebp from "@/assets/gebos/glass-cubes-stack.webp"
import glassDonutShadowAvif from "@/assets/gebos/glass-donut-shadow.avif"
import glassDonutShadowWebp from "@/assets/gebos/glass-donut-shadow.webp"
import glassDonutAvif from "@/assets/gebos/glass-donut.avif"
import glassDonutWebp from "@/assets/gebos/glass-donut.webp"
import heatMeterAvif from "@/assets/gebos/heat-meter.avif"
import heatMeterWebp from "@/assets/gebos/heat-meter.webp"
import houseSmallIsoAvif from "@/assets/gebos/house-small-isometric.avif"
import houseSmallIsoWebp from "@/assets/gebos/house-small-isometric.webp"
import housesSmallIsoAvif from "@/assets/gebos/houses-small-isometric.avif"
import housesSmallIsoWebp from "@/assets/gebos/houses-small-isometric.webp"
import iconBilling from "@/assets/gebos/icon-billing.svg"
import iconPortal from "@/assets/gebos/icon-portal.svg"
import iconRadio from "@/assets/gebos/icon-radio.svg"
@@ -61,8 +73,12 @@ import shippingBoxClosedAvif from "@/assets/gebos/shipping-box-closed.avif"
import shippingBoxClosedWebp from "@/assets/gebos/shipping-box-closed.webp"
import shippingBoxMetersAvif from "@/assets/gebos/shipping-box-meters.avif"
import shippingBoxMetersWebp from "@/assets/gebos/shipping-box-meters.webp"
import smokeAlarmAlphaAvif from "@/assets/gebos/smoke-alarm-alpha.avif"
import smokeAlarmAlphaWebp from "@/assets/gebos/smoke-alarm-alpha.webp"
import smokeAlarmAvif from "@/assets/gebos/smoke-alarm.avif"
import smokeAlarmWebp from "@/assets/gebos/smoke-alarm.webp"
import sprinterAvif from "@/assets/gebos/sprinter.avif"
import sprinterWebp from "@/assets/gebos/sprinter.webp"
import tealBars from "@/assets/gebos/teal-bars.svg"
import technicianPipeAvif from "@/assets/gebos/technician-pipe.avif"
import technicianPipeWebp from "@/assets/gebos/technician-pipe.webp"
@@ -165,6 +181,13 @@ const renders = {
alt: "Modernes Mehrfamilienhaus",
matte: "alpha",
},
/** same subject as `smokeAlarm`, but with genuine alpha safe on any surface */
smokeAlarmAlpha: {
avif: smokeAlarmAlphaAvif,
webp: smokeAlarmAlphaWebp,
alt: "Rauchwarnmelder",
matte: "alpha",
},
/** isometric corner view of the same building type, genuine alpha */
buildingIso: {
avif: buildingIsoAvif,
@@ -179,6 +202,13 @@ const renders = {
alt: "Einfamilienhaus, isometrisch",
matte: "alpha",
},
/** cluster of small apartment blocks, isometric, genuine alpha */
housesSmallIso: {
avif: housesSmallIsoAvif,
webp: housesSmallIsoWebp,
alt: "Mehrere Mehrfamilienhäuser, isometrisch",
matte: "alpha",
},
/** full hero scene: building, gateway, consumption bars & ring on a glass platform */
fullHeroSection: {
avif: fullHeroSectionAvif,
@@ -211,6 +241,18 @@ const renders = {
alt: "",
matte: "alpha",
},
glassDonut: {
avif: glassDonutAvif,
webp: glassDonutWebp,
alt: "",
matte: "alpha",
},
glassDonutShadow: {
avif: glassDonutShadowAvif,
webp: glassDonutShadowWebp,
alt: "",
matte: "alpha",
},
/** glossy numbered badge "1" process-step numeral, genuine alpha */
glassBadge1: {
avif: glassBadge1Avif,
@@ -232,6 +274,27 @@ const renders = {
alt: "",
matte: "alpha",
},
/** glossy numbered badge "4" process-step numeral, genuine alpha */
glassBadge4: {
avif: glassBadge4Avif,
webp: glassBadge4Webp,
alt: "",
matte: "alpha",
},
/** glossy numbered badge "5" process-step numeral, genuine alpha */
glassBadge5: {
avif: glassBadge5Avif,
webp: glassBadge5Webp,
alt: "",
matte: "alpha",
},
/** glossy numbered badge "6" process-step numeral, genuine alpha */
glassBadge6: {
avif: glassBadge6Avif,
webp: glassBadge6Webp,
alt: "",
matte: "alpha",
},
cubesRound: {
avif: cubesRoundAvif,
webp: cubesRoundWebp,
@@ -262,6 +325,12 @@ const renders = {
alt: "Fußbodenheizungsrohre",
matte: "alpha",
},
sprinter: {
avif: sprinterAvif,
webp: sprinterWebp,
alt: "Servicefahrzeug",
matte: "alpha",
},
workerTablet: {
avif: workerTabletAvif,
webp: workerTabletWebp,
@@ -31,14 +31,16 @@ type NavItem = {
/** Source of truth for nav order desktop pill, dropdown and mobile sheet all read it. */
const NAV_ITEMS: NavItem[] = [
{ to: "/", label: "Start" },
{ to: "/loesungen", label: "Lösungen" },
{ to: "/so-funktionierts", label: "So funktionierts" },
{ to: "/fuer-wen", label: "Für wen?", menu: AUDIENCES },
{ to: "/konfigurator", label: "Konfigurator" },
{ to: "/unternehmen", label: "Unternehmen" },
{ to: "/kontakt", label: "Kontakt" },
]
/** "/" is a prefix of every route, so only the root may match exactly. */
const isExact = (to: string) => to === "/"
/**
* Only colour changes between states: the highlight itself is one shared pill
* that slides, so a weight change here would reflow the row it has to land on.
@@ -147,9 +149,9 @@ function AudienceMenu({ item, active }: { item: NavItem; active: boolean }) {
function DesktopNav() {
const { pathname } = useLocation()
/** mirrors NavLink's default (non-`end`) matching, but for the whole row at once */
/** mirrors each NavLink's own matching, but for the whole row at once */
const activeKey = NAV_ITEMS.find((item) =>
matchPath({ path: item.to, end: false }, pathname)
matchPath({ path: item.to, end: isExact(item.to) }, pathname)
)?.to
const { navRef, pill } = useActivePill(activeKey)
@@ -173,6 +175,7 @@ function DesktopNav() {
<NavLink
key={item.to}
to={item.to}
end={isExact(item.to)}
data-nav={item.to}
className={navItemClass(item.to === activeKey)}
>
+16 -7
View File
@@ -6,9 +6,9 @@ import { cn } from "@/lib/utils"
/**
* On/off switch. The lit track is the same teal glass as a primary button, at
* pill scale (`inset-shadow-glass-pill`); off it is a recessed navy pane. The
* thumb travels on the site's spring easing and carries the checkbox tick, so
* the state reads without relying on colour alone.
* pill scale (`inset-shadow-glass-pill`); underneath sits a recessed navy pane.
* The thumb travels on the site's spring easing and carries the checkbox tick,
* so the state reads without relying on colour alone.
*/
function Switch({
className,
@@ -18,19 +18,28 @@ function Switch({
<SwitchPrimitive.Root
data-slot="switch"
className={cn(
"group peer inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-colors outline-none",
"group peer relative inline-flex h-7 w-12 shrink-0 cursor-pointer items-center rounded-full p-0.5 outline-none",
"inset-shadow-xs bg-navy/20 transition-colors duration-300 motion-reduce:transition-none",
"data-[state=unchecked]:hover:bg-navy/30 data-[state=checked]:bg-brand-600",
"focus-visible:ring-[3px] focus-visible:ring-ring/40",
"disabled:cursor-not-allowed disabled:opacity-50",
"data-[state=unchecked]:inset-shadow-xs data-[state=unchecked]:bg-navy/20 data-[state=unchecked]:hover:bg-navy/30",
"data-[state=checked]:glass-teal data-[state=checked]:inset-shadow-glass-pill data-[state=checked]:shadow-glass data-[state=checked]:bg-brand-600 data-[state=checked]:hover:glass-lit",
className
)}
{...props}
>
{/*
* The teal glass rides its own layer: gradients cannot interpolate from
* `none`, so switching it on the track itself would snap. Fading the
* layer carries the gradient, rim and cast bloom across together.
*/}
<span
aria-hidden="true"
className="glass-teal inset-shadow-glass-pill shadow-glass pointer-events-none absolute inset-0 rounded-full opacity-0 transition-opacity duration-300 group-hover:glass-lit group-data-[state=checked]:opacity-100 motion-reduce:transition-none"
/>
<SwitchPrimitive.Thumb
data-slot="switch-thumb"
className={cn(
"pointer-events-none flex size-6 items-center justify-center rounded-full bg-card ring-1 ring-navy/10",
"pointer-events-none relative flex size-6 items-center justify-center rounded-full bg-card ring-1 ring-navy/10",
"shadow-[0_1px_2px_rgb(19_41_60/0.2)]",
"transition-transform duration-300 ease-[cubic-bezier(0.34,1.16,0.42,1)] motion-reduce:transition-none",
"data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-5"