diff --git a/component-library/src/assets/gebos/full-hero-section.avif b/component-library/src/assets/gebos/full-hero-section.avif new file mode 100644 index 0000000..f62f910 Binary files /dev/null and b/component-library/src/assets/gebos/full-hero-section.avif differ diff --git a/component-library/src/assets/gebos/full-hero-section.webp b/component-library/src/assets/gebos/full-hero-section.webp new file mode 100644 index 0000000..f5f9269 Binary files /dev/null and b/component-library/src/assets/gebos/full-hero-section.webp differ diff --git a/component-library/src/assets/gebos/house-small-isometric.avif b/component-library/src/assets/gebos/house-small-isometric.avif new file mode 100644 index 0000000..18eb94a Binary files /dev/null and b/component-library/src/assets/gebos/house-small-isometric.avif differ diff --git a/component-library/src/assets/gebos/house-small-isometric.webp b/component-library/src/assets/gebos/house-small-isometric.webp new file mode 100644 index 0000000..822e86f Binary files /dev/null and b/component-library/src/assets/gebos/house-small-isometric.webp differ diff --git a/component-library/src/assets/gebos/laptop-portfolio.avif b/component-library/src/assets/gebos/laptop-portfolio.avif new file mode 100644 index 0000000..c10b267 Binary files /dev/null and b/component-library/src/assets/gebos/laptop-portfolio.avif differ diff --git a/component-library/src/assets/gebos/laptop-portfolio.webp b/component-library/src/assets/gebos/laptop-portfolio.webp new file mode 100644 index 0000000..3119a28 Binary files /dev/null and b/component-library/src/assets/gebos/laptop-portfolio.webp differ diff --git a/component-library/src/assets/gebos/refresh-loop-cloud.avif b/component-library/src/assets/gebos/refresh-loop-cloud.avif new file mode 100644 index 0000000..3278a52 Binary files /dev/null and b/component-library/src/assets/gebos/refresh-loop-cloud.avif differ diff --git a/component-library/src/assets/gebos/refresh-loop-cloud.webp b/component-library/src/assets/gebos/refresh-loop-cloud.webp new file mode 100644 index 0000000..4265e86 Binary files /dev/null and b/component-library/src/assets/gebos/refresh-loop-cloud.webp differ diff --git a/component-library/src/assets/gebos/refresh-loop.avif b/component-library/src/assets/gebos/refresh-loop.avif new file mode 100644 index 0000000..921aa36 Binary files /dev/null and b/component-library/src/assets/gebos/refresh-loop.avif differ diff --git a/component-library/src/assets/gebos/refresh-loop.webp b/component-library/src/assets/gebos/refresh-loop.webp new file mode 100644 index 0000000..6050bb6 Binary files /dev/null and b/component-library/src/assets/gebos/refresh-loop.webp differ diff --git a/component-library/src/assets/gebos/technician-pipe.avif b/component-library/src/assets/gebos/technician-pipe.avif new file mode 100644 index 0000000..a0b3d8f Binary files /dev/null and b/component-library/src/assets/gebos/technician-pipe.avif differ diff --git a/component-library/src/assets/gebos/technician-pipe.webp b/component-library/src/assets/gebos/technician-pipe.webp new file mode 100644 index 0000000..095bd7b Binary files /dev/null and b/component-library/src/assets/gebos/technician-pipe.webp differ diff --git a/component-library/src/assets/gebos/worker-drilling-gateway-2.avif b/component-library/src/assets/gebos/worker-drilling-gateway-2.avif new file mode 100644 index 0000000..87d3838 Binary files /dev/null and b/component-library/src/assets/gebos/worker-drilling-gateway-2.avif differ diff --git a/component-library/src/assets/gebos/worker-drilling-gateway-2.webp b/component-library/src/assets/gebos/worker-drilling-gateway-2.webp new file mode 100644 index 0000000..6f073ea Binary files /dev/null and b/component-library/src/assets/gebos/worker-drilling-gateway-2.webp differ diff --git a/component-library/src/assets/gebos/worker-drilling-gateway.avif b/component-library/src/assets/gebos/worker-drilling-gateway.avif new file mode 100644 index 0000000..c63c770 Binary files /dev/null and b/component-library/src/assets/gebos/worker-drilling-gateway.avif differ diff --git a/component-library/src/assets/gebos/worker-drilling-gateway.webp b/component-library/src/assets/gebos/worker-drilling-gateway.webp new file mode 100644 index 0000000..693e3a6 Binary files /dev/null and b/component-library/src/assets/gebos/worker-drilling-gateway.webp differ diff --git a/component-library/src/components/gebos/benefit-grid.tsx b/component-library/src/components/gebos/benefit-grid.tsx new file mode 100644 index 0000000..11a9e3e --- /dev/null +++ b/component-library/src/components/gebos/benefit-grid.tsx @@ -0,0 +1,56 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" +import { Card } from "@/components/ui/card" + +type BenefitGridItem = { + icon: React.ReactNode + title: React.ReactNode + text: React.ReactNode +} + +/** + * Benefit cards ringing a product render: three across the top row, two + * flanking the render below. Built for five items — the render claims the + * bottom row's middle cell from `lg`, and grid auto-placement then pushes the + * fifth card past it into the last column, so no card needs explicit coords. + * + * The middle cell is handed to the caller as a positioned, never-clipped + * containing block, so the visual can fill it or lap out of it as its own + * matte requires — sizing it is the caller's job. Below `lg` the placement is + * dropped and the visual simply trails the cards. + */ +function BenefitGrid({ + items, + visual, + className, +}: { + items: BenefitGridItem[] + visual: React.ReactNode + className?: string +}) { + return ( +
+ {items.map((item, i) => ( + + {item.icon} +

+ {item.title} +

+

+ {item.text} +

+
+ ))} +
{visual}
+
+ ) +} + +export { BenefitGrid } +export type { BenefitGridItem } diff --git a/component-library/src/components/gebos/comparison-table.tsx b/component-library/src/components/gebos/comparison-table.tsx new file mode 100644 index 0000000..f61d277 --- /dev/null +++ b/component-library/src/components/gebos/comparison-table.tsx @@ -0,0 +1,207 @@ +import * as React from "react" +import { CircleCheck } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Card } from "@/components/ui/card" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +type ComparisonColumn = { + title: React.ReactNode + /** lifts the column onto a tinted panel and fills in its checks */ + highlight?: boolean + /** check beside every value in this column, cell overrides win (default true) */ + check?: boolean +} + +/** a cell that opts out of — or into — the check its column prescribes */ +type ComparisonCell = { + value?: React.ReactNode + check?: boolean +} + +type ComparisonRow = { + label: React.ReactNode + /** one entry per column, in column order */ + cells: (React.ReactNode | ComparisonCell)[] +} + +/** hairline between rows, painted per cell (see `ComparisonTable`) */ +const HAIRLINE = "border-b border-b-border/70" +/** flanks of the highlighted column's panel */ +const PANEL = "border-x border-x-brand-200 bg-brand-50/70" + +/** `{ value?, check? }` is a cell spec, anything else is renderable content */ +function isCellSpec( + cell: React.ReactNode | ComparisonCell +): cell is ComparisonCell { + return ( + typeof cell === "object" && + cell !== null && + !Array.isArray(cell) && + !React.isValidElement(cell) + ) +} + +/** solid teal badge for the highlighted column, thin mint ring for the rest */ +function CheckMark({ solid }: { solid?: boolean }) { + return ( + circle]:fill-primary [&>circle]:stroke-primary [&>path]:stroke-primary-foreground [&>path]:stroke-[2.5]" + : "text-brand-300" + )} + /> + ) +} + +/** + * Value over the check gutter. A checked column reserves the gutter in every + * cell — head included — so its text keeps one centre axis whether or not a + * given row carries a check. + */ +function ValueSlot({ + children, + gutter, + check, +}: { + children: React.ReactNode + gutter: boolean + check?: React.ReactNode +}) { + return ( +
+ {children} + {gutter ? ( + + {check} + + ) : null} +
+ ) +} + +/** + * Variant comparison ("Welche Variante passt zu mir?"): row labels down the + * left, one column per variant, every value trailed by a check. `highlight` + * lifts the recommended column onto a tinted panel running from the head to + * the last row and turns its checks solid. + * + * That panel is why the table is `border-separate`: only cells can carry the + * rounded corners a `` cannot — so the row hairlines are painted per + * cell, since separated borders drop the ones on ``. + */ +function ComparisonTable({ + columns, + rows, + labelHead, + labelWidth = "26%", + className, +}: { + columns: ComparisonColumn[] + rows: ComparisonRow[] + /** content for the otherwise empty top-left cell */ + labelHead?: React.ReactNode + /** width of the label column, the rest split the remainder evenly */ + labelWidth?: string + className?: string +}) { + const cells = rows.map((row) => + columns.map((_, c) => { + const cell = row.cells[c] + return isCellSpec(cell) ? cell : { value: cell } + }) + ) + /* one gutter decision per column, so heads and values agree */ + const gutters = columns.map((column, c) => + cells.some((row) => row[c].check ?? column.check ?? true) + ) + + return ( + + + + + + {labelHead} + + {columns.map((column, c) => ( + + {column.title} + + ))} + + + + {rows.map((row, r) => { + const last = r === rows.length - 1 + + return ( + + + {row.label} + + {columns.map((column, c) => { + const cell = cells[r][c] + + return ( + + + ) : null + } + > + {cell.value} + + + ) + })} + + ) + })} + +
+
+ ) +} + +export { ComparisonTable } +export type { ComparisonCell, ComparisonColumn, ComparisonRow } diff --git a/component-library/src/components/gebos/feature-band.tsx b/component-library/src/components/gebos/feature-band.tsx new file mode 100644 index 0000000..8dc0731 --- /dev/null +++ b/component-library/src/components/gebos/feature-band.tsx @@ -0,0 +1,60 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +type FeatureBandItem = { + icon?: React.ReactNode + title: React.ReactNode + sub?: React.ReactNode +} + +/** + * Feature row as one white card: hairline-divided columns (2 up on mobile, + * 4 from `sm`), each an oversized teal line icon over a navy title and a + * muted sub. Dividers are inset because the card pads around the columns. + */ +function FeatureBand({ + items, + className, +}: { + items: FeatureBandItem[] + className?: string +}) { + return ( +
+ {items.map((item, i) => ( +
= 2 && "border-t", + i % 4 !== 0 ? "sm:border-l" : "sm:border-l-0", + i >= 4 ? "sm:border-t" : "sm:border-t-0" + )} + > + {item.icon ? ( + {item.icon} + ) : null} +
+ {item.title} +
+ {item.sub ? ( +

+ {item.sub} +

+ ) : null} +
+ ))} +
+ ) +} + +export { FeatureBand } +export type { FeatureBandItem } diff --git a/component-library/src/components/gebos/line-icons.tsx b/component-library/src/components/gebos/line-icons.tsx new file mode 100644 index 0000000..740a835 --- /dev/null +++ b/component-library/src/components/gebos/line-icons.tsx @@ -0,0 +1,125 @@ +import * as React from "react" + +/** + * GebOS line icons drawn on the lucide grid (24×24, currentColor stroke) for + * the motifs lucide has no equivalent of. Stroke is a hair lighter than + * lucide's 2 because these render large (≈48px) in feature bands. + */ +function LineIcon({ + strokeWidth = 1.6, + children, + ...props +}: React.ComponentProps<"svg">) { + return ( + + ) +} + +/** + * Euro glyph inside a ring: costs stay transparent and under control. + * lucide has the bare glyph only, so it is shrunk about the icon centre and + * its stroke width divided by the same factor to keep the ring's weight. + */ +function EuroCircleIcon({ + strokeWidth = 1.6, + ...props +}: React.ComponentProps<"svg">) { + const glyphScale = 0.52 + return ( + + + + + + + + + ) +} + +/** Cycle arrows around a radio dot: hardware that arrives ready to run. */ +function PreconfiguredIcon(props: React.ComponentProps<"svg">) { + return ( + + + + + + + + + + ) +} + +/** + * Wrench crossed with a screwdriver: install it yourself or hand it to a + * trade. Wrench contour is lucide's, mirrored onto the "\" diagonal so the + * jaw sits top-left; the screwdriver is drawn along +x and rotated onto "/", + * nudged clear of the jaw and filled with the card surface so it reads as + * lying on top of the wrench. + */ +function CrossedToolsIcon(props: React.ComponentProps<"svg">) { + return ( + + + + + ) +} + +/** Cloud with a bolt: metering data flows and is processed without anyone. */ +function CloudAutomationIcon(props: React.ComponentProps<"svg">) { + return ( + + + + + ) +} + +/** Dashboard gauge on a screen: every building and meter in one portal. */ +function PortalGaugeIcon(props: React.ComponentProps<"svg">) { + return ( + + + + + + + + ) +} + +export { + CloudAutomationIcon, + CrossedToolsIcon, + EuroCircleIcon, + LineIcon, + PortalGaugeIcon, + PreconfiguredIcon, +} diff --git a/component-library/src/components/gebos/option-card.tsx b/component-library/src/components/gebos/option-card.tsx new file mode 100644 index 0000000..aed173a --- /dev/null +++ b/component-library/src/components/gebos/option-card.tsx @@ -0,0 +1,48 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" +import { Card } from "@/components/ui/card" + +/** + * One of two mutually exclusive delivery routes (Selbst installieren / + * Komplettlösung): centred copy over a bold takeaway, the 3D scene below, + * CTA pinned to the card foot so side-by-side cards line their buttons up. + */ +function OptionCard({ + title, + lead, + takeaway, + visual, + action, + className, +}: { + title: React.ReactNode + lead: React.ReactNode + /** bold navy claim that closes the copy block */ + takeaway?: React.ReactNode + visual: React.ReactNode + action: React.ReactNode + className?: string +}) { + return ( + +

+ {title} +

+

+ {lead} +

+ {takeaway ? ( +

+ {takeaway} +

+ ) : null} +
+ {visual} +
+
{action}
+
+ ) +} + +export { OptionCard } diff --git a/component-library/src/components/gebos/process-steps.tsx b/component-library/src/components/gebos/process-steps.tsx index 1a1dfaa..ccc2686 100644 --- a/component-library/src/components/gebos/process-steps.tsx +++ b/component-library/src/components/gebos/process-steps.tsx @@ -94,4 +94,70 @@ function NumberedList({ ) } -export { ProcessSteps, NumberedList } +/** + * Step rail on a card (Selbstinstallation, steps 1–5): numbered rings threaded + * on a solid teal rail, hairline-divided columns beneath, each an asset render + * over title and copy. Each column draws its own half of the rail, so the run + * ends at the first and last ring whatever the step count. Rail and dividers + * appear only once the columns sit in one row. + */ +function StepRail({ + steps, + className, +}: { + steps: ProcessStep[] + className?: string +}) { + return ( +
    = 5 ? "lg:grid-cols-5" : "lg:grid-cols-4", + className + )} + > + {steps.map((step, i) => ( +
  1. +
  2. + ))} +
+ ) +} + +export { ProcessSteps, NumberedList, StepRail } diff --git a/component-library/src/components/gebos/site-assets.tsx b/component-library/src/components/gebos/site-assets.tsx index 378719b..b76561f 100644 --- a/component-library/src/components/gebos/site-assets.tsx +++ b/component-library/src/components/gebos/site-assets.tsx @@ -22,6 +22,8 @@ import cubesRoundWebp from "@/assets/gebos/cubes-round.webp" import drillToolAvif from "@/assets/gebos/drill-tool.avif" import drillToolWebp from "@/assets/gebos/drill-tool.webp" import euroOrb from "@/assets/gebos/euro-orb.svg" +import fullHeroSectionAvif from "@/assets/gebos/full-hero-section.avif" +import fullHeroSectionWebp from "@/assets/gebos/full-hero-section.webp" import gatewayAvif from "@/assets/gebos/gateway.avif" import gatewayWebp from "@/assets/gebos/gateway.webp" import glassCubes from "@/assets/gebos/glass-cubes.svg" @@ -29,6 +31,8 @@ import glassCubesStackAvif from "@/assets/gebos/glass-cubes-stack.avif" import glassCubesStackWebp from "@/assets/gebos/glass-cubes-stack.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 iconBilling from "@/assets/gebos/icon-billing.svg" import iconPortal from "@/assets/gebos/icon-portal.svg" import iconRadio from "@/assets/gebos/icon-radio.svg" @@ -36,7 +40,13 @@ import iconSmokeAlarm from "@/assets/gebos/icon-smoke-alarm.svg" import iconUvi from "@/assets/gebos/icon-uvi.svg" import laptopAvif from "@/assets/gebos/laptop-dashboard.avif" import laptopWebp from "@/assets/gebos/laptop-dashboard.webp" +import laptopPortfolioAvif from "@/assets/gebos/laptop-portfolio.avif" +import laptopPortfolioWebp from "@/assets/gebos/laptop-portfolio.webp" import packageBox from "@/assets/gebos/package-box.svg" +import refreshLoopCloudAvif from "@/assets/gebos/refresh-loop-cloud.avif" +import refreshLoopCloudWebp from "@/assets/gebos/refresh-loop-cloud.webp" +import refreshLoopAvif from "@/assets/gebos/refresh-loop.avif" +import refreshLoopWebp from "@/assets/gebos/refresh-loop.webp" 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" @@ -44,12 +54,18 @@ import shippingBoxMetersWebp from "@/assets/gebos/shipping-box-meters.webp" 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 technicianPipeAvif from "@/assets/gebos/technician-pipe.avif" +import technicianPipeWebp from "@/assets/gebos/technician-pipe.webp" import uviBadge from "@/assets/gebos/uvi-badge.svg" import wirelessOrb from "@/assets/gebos/wireless-orb.svg" import wordmarkDarkAvif from "@/assets/gebos/wordmark-cubes-dark.avif" import wordmarkDarkWebp from "@/assets/gebos/wordmark-cubes-dark.webp" import wordmarkGlowAvif from "@/assets/gebos/wordmark-cubes-glow.avif" import wordmarkGlowWebp from "@/assets/gebos/wordmark-cubes-glow.webp" +import workerDrillingGateway2Avif from "@/assets/gebos/worker-drilling-gateway-2.avif" +import workerDrillingGateway2Webp from "@/assets/gebos/worker-drilling-gateway-2.webp" +import workerDrillingGatewayAvif from "@/assets/gebos/worker-drilling-gateway.avif" +import workerDrillingGatewayWebp from "@/assets/gebos/worker-drilling-gateway.webp" import workerTabletAvif from "@/assets/gebos/worker-tablet.avif" import workerTabletWebp from "@/assets/gebos/worker-tablet.webp" @@ -112,6 +128,24 @@ const renders = { alt: "Objektbezogen vorbereitete Hardware", matte: "white", }, + technicianPipe: { + avif: technicianPipeAvif, + webp: technicianPipeWebp, + alt: "Techniker prüft Rohrleitung mit Messgerät", + matte: "white", + }, + workerDrillingGateway: { + avif: workerDrillingGatewayAvif, + webp: workerDrillingGatewayWebp, + alt: "Monteur montiert GebOS Gateway an der Wand", + matte: "white", + }, + workerDrillingGateway2: { + avif: workerDrillingGateway2Avif, + webp: workerDrillingGateway2Webp, + alt: "Monteur montiert GebOS Gateway an Trockenbauwand", + matte: "white", + }, /* --- transparent renders (free placement, any background) ---------- */ /** same subject as `building`, but with genuine alpha – safe on any surface */ @@ -128,6 +162,27 @@ const renders = { alt: "Modernes Mehrfamilienhaus", matte: "alpha", }, + /** compact single-family house, isometric, genuine alpha */ + houseSmallIso: { + avif: houseSmallIsoAvif, + webp: houseSmallIsoWebp, + alt: "Einfamilienhaus, isometrisch", + matte: "alpha", + }, + /** full hero scene: building, gateway, consumption bars & ring on a glass platform */ + fullHeroSection: { + avif: fullHeroSectionAvif, + webp: fullHeroSectionWebp, + alt: "GebOS Systemübersicht: Gebäude, Gateway und Verbrauchsdaten", + matte: "alpha", + }, + /** same laptop mockup as `laptopDashboard`, portfolio/room screen, genuine alpha */ + laptopPortfolio: { + avif: laptopPortfolioAvif, + webp: laptopPortfolioWebp, + alt: "GebOS Portfolio-Ansicht auf dem Laptop", + matte: "alpha", + }, brandMark3d: { avif: brandMark3dAvif, webp: brandMark3dWebp, @@ -176,6 +231,18 @@ const renders = { alt: "Vorkonfigurierte Lieferung", matte: "alpha", }, + refreshLoop: { + avif: refreshLoopAvif, + webp: refreshLoopWebp, + alt: "", + matte: "alpha", + }, + refreshLoopCloud: { + avif: refreshLoopCloudAvif, + webp: refreshLoopCloudWebp, + alt: "", + matte: "alpha", + }, /** baked light glow – use on white/near-white surfaces */ wordmarkGlow: { avif: wordmarkGlowAvif, @@ -216,8 +283,10 @@ const icons = { /** * Renders a kit asset as AVIF→WebP . White-matte masters get - * mix-blend-multiply so their matte disappears on light surfaces; - * transparent assets composite normally. + * mix-blend-multiply so their matte disappears on light surfaces, plus a + * hairline brightness lift: the masters matte out at 252–254, which multiply + * would otherwise leave as a visible 1 % box on a white card. Transparent + * assets composite normally. */ function AssetRender({ render, @@ -242,7 +311,7 @@ function AssetRender({ loading={loading} className={cn( "object-contain", - render.matte === "white" && "mix-blend-multiply", + render.matte === "white" && "mix-blend-multiply brightness-[1.015]", imgClassName )} /> diff --git a/component-library/src/components/gebos/site-footer.tsx b/component-library/src/components/gebos/site-footer.tsx index 749a498..a55fc79 100644 --- a/component-library/src/components/gebos/site-footer.tsx +++ b/component-library/src/components/gebos/site-footer.tsx @@ -34,42 +34,44 @@ const COLUMNS: { title: string; links: { to: string; label: string }[] }[] = [ function SiteFooter() { return ( -
- -
- -

- Messdienstleistungen einfach gemacht – von der vorkonfigurierten - Hardware bis zum laufenden Betrieb. -

-
- {COLUMNS.map((col) => ( - - ))} -
-
- - © 2026 GebOS. Alle Rechte vorbehalten. - Impressum · Datenschutz · AGB +
+
+ +
+ +

+ Messdienstleistungen einfach gemacht – von der vorkonfigurierten + Hardware bis zum laufenden Betrieb. +

+
+ {COLUMNS.map((col) => ( + + ))}
-
-
+
+ + © 2026 GebOS. Alle Rechte vorbehalten. + Impressum · Datenschutz · AGB + +
+ + ) } diff --git a/component-library/src/components/ui/button.tsx b/component-library/src/components/ui/button.tsx index 80cc1f3..360d771 100644 --- a/component-library/src/components/ui/button.tsx +++ b/component-library/src/components/ui/button.tsx @@ -4,22 +4,33 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" +/** + * Glass physics shared by the filled variants: the backdrop is blurred and + * saturated the way the cube renders bend what is behind them, the lit rim + * and the caustic ride the inset-shadow slot, the cast bloom the outer one. + * Tint and edge colour come from the `glass-*` utilities in globals.css. + */ +const glass = + "backdrop-blur-md backdrop-saturate-150 inset-shadow-glass shadow-glass hover:shadow-glass-lg" + +const glassOnHover = + "hover:backdrop-blur-md hover:backdrop-saturate-150 hover:inset-shadow-glass hover:shadow-glass" + const buttonVariants = cva( "inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg text-sm font-semibold whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: - "bg-primary text-primary-foreground shadow-sm hover:bg-brand-700", - outline: - "border border-border bg-card text-foreground shadow-pill hover:border-brand-200 hover:bg-brand-50", - secondary: "bg-secondary text-secondary-foreground hover:bg-brand-100", - ghost: "text-foreground hover:bg-secondary", + default: `${glass} glass-teal text-primary-foreground hover:glass-lit`, + outline: `${glass} glass-clear text-foreground hover:glass-lit`, + secondary: `${glass} glass-mint text-secondary-foreground hover:glass-lit`, + ghost: `${glassOnHover} text-foreground hover:glass-clear`, link: "text-primary underline-offset-4 hover:underline", - /* white button sitting on the teal gradient band */ - inverse: "bg-white text-brand-800 shadow-sm hover:bg-brand-50", - destructive: - "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + /* frosted white button sitting on the teal gradient band */ + inverse: `${glass} glass-frost text-brand-800 hover:glass-lit`, + /* its quieter neighbour: the band still reads through the glass */ + inverseOutline: `${glass} glass-veil text-white hover:glass-lit`, + destructive: `${glass} glass-danger text-destructive-foreground hover:glass-lit`, }, size: { default: "h-10 px-5 py-2", diff --git a/component-library/src/pages/audiences/audience-page.tsx b/component-library/src/pages/audiences/audience-page.tsx index 3ac9701..0458339 100644 --- a/component-library/src/pages/audiences/audience-page.tsx +++ b/component-library/src/pages/audiences/audience-page.tsx @@ -3,6 +3,7 @@ import * as React from "react" import { Container, Section, SectionHeading } from "@/components/gebos/section" import { PageBreadcrumb } from "@/components/gebos/page-breadcrumb" import { PageHero } from "@/components/gebos/page-hero" +import { FeatureBand } from "@/components/gebos/feature-band" import { FeatureTile } from "@/components/gebos/icon-tile" type AudienceFeature = { @@ -34,6 +35,7 @@ function AudiencePage({ actions, visual, features, + featureLayout = "tiles", children, closing, }: { @@ -45,6 +47,11 @@ function AudiencePage({ actions?: React.ReactNode visual?: React.ReactNode features: AudienceFeature[] + /** + * `tiles` — bare icon-over-label tiles on the page background. + * `band` — one divided white card (larger line icons, two-line subs). + */ + featureLayout?: "tiles" | "band" /** additional full-width spec sections between feature row and closing */ children?: React.ReactNode closing?: React.ReactNode @@ -69,17 +76,21 @@ function AudiencePage({ />
-
- {features.map((feature, i) => ( - - ))} -
+ {featureLayout === "band" ? ( + + ) : ( +
+ {features.map((feature, i) => ( + + ))} +
+ )}
{children} {closing} diff --git a/component-library/src/pages/audiences/hauseigentuemer.tsx b/component-library/src/pages/audiences/hauseigentuemer.tsx index 0e44a0c..deeedfc 100644 --- a/component-library/src/pages/audiences/hauseigentuemer.tsx +++ b/component-library/src/pages/audiences/hauseigentuemer.tsx @@ -1,19 +1,27 @@ import { Link } from "react-router-dom" -import { Euro, Handshake, ReceiptText, ShieldCheck, Wrench } from "lucide-react" +import { Clock, Euro, HardHat, Network } from "lucide-react" import { Button } from "@/components/ui/button" -import { Card } from "@/components/ui/card" import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/components/ui/table" + BenefitGrid, + type BenefitGridItem, +} from "@/components/gebos/benefit-grid" +import { + ComparisonTable, + type ComparisonColumn, + type ComparisonRow, +} from "@/components/gebos/comparison-table" import { Section, SectionHeading } from "@/components/gebos/section" import { CtaBanner } from "@/components/gebos/cta-banner" -import { NumberedList } from "@/components/gebos/process-steps" +import { StepRail } from "@/components/gebos/process-steps" +import { + CloudAutomationIcon, + CrossedToolsIcon, + EuroCircleIcon, + PortalGaugeIcon, + PreconfiguredIcon, +} from "@/components/gebos/line-icons" +import { OptionCard } from "@/components/gebos/option-card" import { AssetRender, renders } from "@/components/gebos/site-assets" import { AudiencePage } from "@/pages/audiences/audience-page" @@ -31,7 +39,7 @@ function HomeownerArt() { imgClassName="w-full" loading="eager" /> - */} + + ) +} + +/** + * Page-local scene for the full-service route: the fitter drilling the gateway + * onto the drywall panel. 4:3 frame to match the shipping-box render next door. + * The render is a self-contained scene whose subject occupies the middle 79% + * of its 4:5 canvas, so it is scaled past the frame and dropped below it: the + * subject then fills the frame's height while the surrounding matte — invisible + * on the card — is what overflows. + */ +function FullServiceArt() { + return ( +
+
) } +/** + * Portal laptop for the benefit grid's centre cell. Measured on the master, + * the laptop occupies 11.0–88.5 % of the canvas height and 7.7–83.9 % of its + * width, i.e. 77.4 % of each with clear air around it. Fitting the *canvas* + * to the cell would therefore leave the laptop floating small in the middle, + * so the canvas is inflated past the cell by exactly that air (11.04 % / + * 0.7744 above, 11.52 % / 0.7744 below): the laptop then fills the cell's + * height, flush with the flanking cards' floor and a grid gap clear of the + * row above — it never crosses into a card. `object-contain` keeps that true + * whatever height the row settles at; the wider right inset cancels the + * laptop's own leftward bias inside the canvas so it reads centred. + */ +function PortalLaptopArt() { + return ( + + ) +} + const SELF_INSTALL_STEPS = [ { + media: ( + + ), title: "Gebäude angeben", description: - "Sie teilen uns die grundlegenden Informationen zum Gebäude und den Wohnungen mit.", + "Daten zu Ihrem Gebäude und den Wohneinheiten in wenigen Schritten erfassen.", }, { + media: ( + <> + + + + ), title: "Passende Ausstattung zusammenstellen", - description: - "Die benötigten Zähler, Kommunikationskomponenten und gegebenenfalls Rauchwarnmelder werden zusammengestellt.", + description: "Wir empfehlen die benötigte Ausstattung – Sie wählen aus.", }, { + media: ( + + ), title: "Vorkonfiguriert erhalten", - description: - "Die Geräte werden bereits für Ihr Gebäude vorbereitet geliefert.", + description: "Ihre Technik kommt vorkonfiguriert und einsatzbereit an.", }, { + media: , title: "Montieren", description: - "Sie selbst, Ihr Hausmeister oder Ihr Fachbetrieb installiert die Geräte.", + "Sie montieren mit Ihrem Handwerker oder selbst – flexibel und unkompliziert.", }, { + media: ( + + ), title: "GebOS übernimmt", description: - "Nach der Inbetriebnahme laufen Datenerfassung und die gebuchten Dienstleistungen über GebOS.", + "Wir übernehmen Ablesung, Übertragung und Auswertung automatisch.", }, ] -const WHY_GEBOS = [ +const WHY_GEBOS: BenefitGridItem[] = [ { + icon: , title: "Kosten kontrollieren", text: "Entscheiden Sie selbst, welche Arbeiten Sie übernehmen und welche Sie abgeben möchten.", }, { + icon: , title: "Keine unnötige technische Komplexität", text: "Die Hardware wird für das jeweilige Gebäude vorbereitet.", }, { + icon: , title: "Bestehenden Handwerker nutzen", text: "Sie benötigen keinen speziellen GebOS-Installateur.", }, { + icon: , title: "Alles an einem Ort", text: "UVI, Abrechnung, Geräte und weitere Gebäudedienstleistungen werden zentral verwaltet.", }, { + icon: , title: "Weniger laufender Aufwand", text: "Nach der Einrichtung werden Verbrauchsdaten automatisch erfasst und verarbeitet.", }, ] -const VARIANT_ROWS = [ - ["Planung", "gemeinsam mit GebOS", "GebOS"], - ["Hardware", "GebOS", "GebOS"], - ["Installation", "Kunde / eigener Fachbetrieb", "organisiert durch GebOS"], - ["Softwareeinrichtung", "vorbereitet durch GebOS", "GebOS"], - ["Laufender Betrieb", "GebOS", "GebOS"], - ["Kosten", "möglichst niedrig", "höhere Planungssicherheit und Komfort"], +const VARIANT_COLUMNS: ComparisonColumn[] = [ + { title: "Selbst installieren", highlight: true }, + { title: "Komplettlösung" }, +] + +const VARIANT_ROWS: ComparisonRow[] = [ + { label: "Planung", cells: ["gemeinsam mit GebOS", "GebOS"] }, + { label: "Hardware", cells: ["GebOS", "GebOS"] }, + { + label: "Installation", + cells: ["Kunde / eigener Fachbetrieb", "organisiert durch GebOS"], + }, + { label: "Softwareeinrichtung", cells: ["vorbereitet durch GebOS", "GebOS"] }, + { label: "Laufender Betrieb", cells: ["GebOS", "GebOS"] }, + { + label: "Kosten", + cells: ["möglichst niedrig", "höhere Planungssicherheit und Komfort"], + }, ] export default function HauseigentuemerPage() { @@ -131,26 +210,27 @@ export default function HauseigentuemerPage() { } visual={} + featureLayout="band" features={[ { - icon: , - title: "Selbst installieren", - sub: "Kosten sparen, flexibel bleiben", + icon: , + title: "Vorkonfigurierte Technik", + sub: "Einsatzbereit, perfekt aufeinander abgestimmt.", }, { - icon: , - title: "Komplettlösung", - sub: "Wir kümmern uns um alles", + icon: , + title: "Flexibel installieren", + sub: "Selbst übernehmen oder vom Profi montieren lassen.", }, { - icon: , - title: "Transparente Preise", - sub: "Keine versteckten Kosten", + icon: , + title: "Automatischer Betrieb", + sub: "GebOS übernimmt Ablesung, Übertragung und Auswertung.", }, { - icon: , - title: "Bestehende Partner", - sub: "Ihre Handwerker bleiben", + icon: , + title: "Alles zentral verwaltet", + sub: "Alle Daten, Geräte und Gebäude im Blick.", }, ]} closing={ @@ -181,51 +261,45 @@ export default function HauseigentuemerPage() { title="Zwei Wege zu GebOS" />
- - -

- Selbst installieren -

-

- Sie möchten Kosten niedrig halten und können die Installation - selbst oder über einen vertrauten Heizungsbauer, Hausmeister - oder anderen Fachbetrieb organisieren? GebOS liefert die - benötigte Technik passend zu Ihrem Gebäude vorkonfiguriert. -

-

- Sie organisieren die Montage. GebOS übernimmt den digitalen - Betrieb. -

-
- -
-
- - -

- Komplett installieren lassen -

-

- Sie möchten sich möglichst wenig mit Planung, Beschaffung und - Installation beschäftigen? GebOS organisiert die komplette - Umsetzung für Sie. Von der Ermittlung der benötigten Geräte - über die Installation bis zum laufenden Betrieb erhalten Sie - eine durchgängige Lösung. -

-
- -
-
+ } + />
@@ -234,7 +308,7 @@ export default function HauseigentuemerPage() { eyebrow="Schritt für Schritt" title="So funktioniert die Selbstinstallation" /> - +
@@ -242,16 +316,11 @@ export default function HauseigentuemerPage() { eyebrow="Ihre Vorteile" title="Warum GebOS für Hauseigentümer?" /> -
- {WHY_GEBOS.map((item) => ( - -

{item.title}

-

- {item.text} -

-
- ))} -
+ } + />
@@ -259,32 +328,11 @@ export default function HauseigentuemerPage() { eyebrow="Im Vergleich" title="Welche Variante passt zu mir?" /> - - - - - - Selbst installieren - Komplettlösung - - - - {VARIANT_ROWS.map(([label, self, full]) => ( - - - {label} - - - {self} - - - {full} - - - ))} - -
-
+
) diff --git a/component-library/src/pages/configurator.tsx b/component-library/src/pages/configurator.tsx index ac110b0..a605f7e 100644 --- a/component-library/src/pages/configurator.tsx +++ b/component-library/src/pages/configurator.tsx @@ -37,8 +37,6 @@ const HEAT_OPTIONS: { value: Heat; label: string }[] = [ const INITIAL_BUILDINGS: Building[] = [ { id: 1, adresse: "", heat: "heizkoerper", wohnungen: 12, zimmer: 3 }, - { id: 2, adresse: "", heat: "heizkoerper", wohnungen: 8, zimmer: 3 }, - { id: 3, adresse: "", heat: "heizkoerper", wohnungen: 6, zimmer: 3 }, ] const UVI_MONTHLY = 10.5 @@ -357,8 +355,7 @@ export default function ConfiguratorPage() {
- {totalBuildings}{" "} - {totalBuildings === 1 ? "Gebäude" : "Gebäude"} + {totalBuildings} Gebäude
{totalUnits} {totalUnits === 1 ? "Wohnung" : "Wohnungen"} diff --git a/component-library/src/pages/home.tsx b/component-library/src/pages/home.tsx index 4f31e0c..8e0a3b3 100644 --- a/component-library/src/pages/home.tsx +++ b/component-library/src/pages/home.tsx @@ -572,12 +572,7 @@ export default function HomePage() { - diff --git a/component-library/src/styles/globals.css b/component-library/src/styles/globals.css index 66fd927..5962fb0 100644 --- a/component-library/src/styles/globals.css +++ b/component-library/src/styles/globals.css @@ -56,6 +56,18 @@ --brand-900: #06474e; --navy: #071a3f; + /* glass — sampled off the cube renders (glass-cubes-stack, brand-cube) */ + --glass-spec: #dffcfc; /* specular top face */ + --glass-edge: #97f1f4; /* light piped along the lit edge */ + --glass-deep: #176163; /* shaded face, and the bloom it casts */ + + /* per-surface slots, retuned by the glass-* utilities below */ + --glass-rim: color-mix(in srgb, var(--glass-spec) 55%, transparent); + --glass-ring: rgb(255 255 255 / 0.2); + --glass-ring-lit: rgb(255 255 255 / 0.35); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 50%, transparent); + --glass-bloom: color-mix(in srgb, var(--glass-deep) 38%, transparent); + --radius: 0.75rem; } @@ -112,6 +124,21 @@ 0 2px 4px rgb(19 41 60 / 0.04), 0 16px 48px rgb(19 41 60 / 0.1); --shadow-band: 0 16px 40px rgb(10 100 89 / 0.28); --shadow-pill: 0 1px 2px rgb(19 41 60 / 0.06); + + /* glass: lit top edge, piped rim, caustic pooling at the base */ + --inset-shadow-glass: + inset 0 1px 0 0 var(--glass-rim), inset 0 0 0 1px var(--glass-ring), + inset 0 -9px 12px -9px var(--glass-caustic); + --shadow-glass: + 0 1px 2px rgb(19 41 60 / 0.05), 0 10px 22px -10px var(--glass-bloom); + --shadow-glass-lg: + 0 2px 4px rgb(19 41 60 / 0.06), 0 18px 34px -12px var(--glass-bloom); + /* the same optics at slab scale, for surfaces the size of the footer */ + --inset-shadow-glass-slab: + inset 0 2px 0 0 var(--glass-rim), inset 0 0 0 1px var(--glass-ring), + inset 0 -30px 44px -30px var(--glass-caustic); + --shadow-glass-slab: + 0 2px 6px rgb(19 41 60 / 0.05), 0 30px 60px -24px var(--glass-bloom); } @layer base { @@ -167,3 +194,165 @@ border-color: var(--brand-300); border-style: dotted; } + +/* ------------------------------------------------------------------ */ +/* Glass surfaces */ +/* Read off the cube renders: light falls in from the top left, the */ +/* body deepens towards the base, the edge pipes light around the */ +/* silhouette and pools into a caustic underneath. */ +/* Same colour space as the tokens above – the bodies are brand */ +/* tokens thinned `in srgb`, and hand-written gradients interpolate */ +/* in srgb too (Tailwind's own gradient utilities would go oklab). */ +/* ------------------------------------------------------------------ */ + +/* primary actions: teal glass, lit face → shaded base */ +@utility glass-teal { + --glass-rim: color-mix(in srgb, var(--glass-spec) 45%, transparent); + --glass-ring: rgb(255 255 255 / 0.2); + --glass-ring-lit: rgb(255 255 255 / 0.38); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 55%, transparent); + --glass-bloom: color-mix(in srgb, var(--brand-800) 45%, transparent); + background-image: + linear-gradient( + 135deg, + color-mix(in srgb, var(--glass-spec) 20%, transparent) 0%, + color-mix(in srgb, var(--glass-spec) 5%, transparent) 32%, + transparent 58% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--brand-600) 96%, transparent) 0%, + color-mix(in srgb, var(--brand-700) 98%, transparent) 52%, + var(--brand-800) 100% + ); +} + +/* secondary actions: thin mint glass over light surfaces */ +@utility glass-mint { + --glass-rim: rgb(255 255 255 / 0.85); + --glass-ring: rgb(255 255 255 / 0.7); + --glass-ring-lit: rgb(255 255 255 / 0.9); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 40%, transparent); + --glass-bloom: color-mix(in srgb, var(--glass-deep) 22%, transparent); + background-image: + linear-gradient( + 135deg, + rgb(255 255 255 / 0.55) 0%, + rgb(255 255 255 / 0.15) 40%, + transparent 66% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--brand-100) 82%, transparent) 0%, + color-mix(in srgb, var(--brand-200) 74%, transparent) 100% + ); +} + +/* quiet actions: clear glass, only the edge and a teal cast give it away */ +@utility glass-clear { + --glass-rim: rgb(255 255 255 / 0.9); + --glass-ring: var(--input); + --glass-ring-lit: color-mix(in srgb, var(--brand-300) 80%, transparent); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 35%, transparent); + --glass-bloom: color-mix(in srgb, var(--glass-deep) 16%, transparent); + background-image: + linear-gradient( + 135deg, + rgb(255 255 255 / 0.7) 0%, + rgb(255 255 255 / 0.3) 42%, + transparent 68% + ), + linear-gradient( + 180deg, + rgb(255 255 255 / 0.72) 0%, + color-mix(in srgb, var(--brand-50) 66%, transparent) 100% + ); +} + +/* frosted white glass for the teal gradient bands */ +@utility glass-frost { + --glass-rim: rgb(255 255 255 / 0.95); + --glass-ring: rgb(255 255 255 / 0.8); + --glass-ring-lit: rgb(255 255 255 / 0.95); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 45%, transparent); + --glass-bloom: color-mix(in srgb, var(--brand-900) 45%, transparent); + background-image: + linear-gradient( + 135deg, + rgb(255 255 255 / 0.4) 0%, + rgb(255 255 255 / 0.1) 42%, + transparent 66% + ), + linear-gradient( + 180deg, + rgb(255 255 255 / 0.94) 0%, + color-mix(in srgb, var(--brand-50) 88%, transparent) 100% + ); +} + +/* the same band, second action: glass with the band still showing through */ +@utility glass-veil { + --glass-rim: rgb(255 255 255 / 0.55); + --glass-ring: rgb(255 255 255 / 0.45); + --glass-ring-lit: rgb(255 255 255 / 0.7); + --glass-caustic: rgb(255 255 255 / 0.3); + --glass-bloom: color-mix(in srgb, var(--brand-900) 35%, transparent); + background-image: + linear-gradient( + 135deg, + rgb(255 255 255 / 0.22) 0%, + rgb(255 255 255 / 0.06) 40%, + transparent 64% + ), + linear-gradient(180deg, rgb(255 255 255 / 0.16) 0%, rgb(255 255 255 / 0.07) 100%); +} + +/* cast navy glass, thick enough to read as a slab (footer) */ +@utility glass-navy { + --glass-rim: color-mix(in srgb, var(--glass-edge) 42%, transparent); + --glass-ring: rgb(255 255 255 / 0.12); + --glass-ring-lit: rgb(255 255 255 / 0.2); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 34%, transparent); + --glass-bloom: color-mix(in srgb, var(--navy) 32%, transparent); + background-image: + linear-gradient( + 135deg, + color-mix(in srgb, var(--glass-spec) 13%, transparent) 0%, + color-mix(in srgb, var(--glass-spec) 3%, transparent) 26%, + transparent 52% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--navy) 88%, transparent) 0%, + color-mix(in srgb, var(--navy) 96%, transparent) 72%, + color-mix(in srgb, var(--navy) 98%, transparent) 100% + ); +} + +@utility glass-danger { + --glass-rim: rgb(255 255 255 / 0.4); + --glass-ring: rgb(255 255 255 / 0.25); + --glass-ring-lit: rgb(255 255 255 / 0.42); + --glass-caustic: rgb(255 215 216 / 0.45); + --glass-bloom: color-mix(in srgb, var(--destructive) 45%, transparent); + background-image: + linear-gradient( + 135deg, + rgb(255 255 255 / 0.28) 0%, + rgb(255 255 255 / 0.06) 34%, + transparent 62% + ), + linear-gradient( + 180deg, + color-mix(in srgb, var(--destructive) 94%, transparent) 0%, + color-mix(in srgb, var(--destructive) 99%, transparent) 100% + ); +} + +/* hover/active: the face turns further into the light */ +@utility glass-lit { + --glass-rim: color-mix(in srgb, var(--glass-spec) 90%, transparent); + --glass-ring: var(--glass-ring-lit); + --glass-caustic: color-mix(in srgb, var(--glass-edge) 92%, transparent); + background-color: color-mix(in srgb, var(--glass-spec) 16%, transparent); +}