Pressemitteilung
Pressemitteilungen in der Form, die Redaktionen erwarten. Die Mitteilung mit Ortsmarke, Zitat, Unternehmenstext und einer Seitenspalte für Kontakt und Downloads, der Newsroom mit Filtern, ein Band der neuesten Mitteilungen und eine Mitteilung unter Sperrfrist.
Eine von 2 Components in Press und Awards
Variants
Details
Was Pressemitteilung mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Press und Awards
- Bereich
- Marketing
- Variants
- 4
- Version
- 1.0.0 · seit 1.1.0
- Datei
- components/ui/press-release.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 31 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle PressReleaseProps aus components/ui/press-release.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalPressReleaseVariantreleasenewsroomlatestembargo
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodecompanyoptionalstringThe company name, used in the boilerplate heading
releaseoptionalPressReleaseItemreleasesoptionalPressReleaseItem[]boilerplateoptionalstringAbout the company, the paragraph at the end of every release
contactoptionalPressContactdownloadsoptional{ label: string; meta?: string; href: string }[]embargoUntiloptionalstringembargo: ISO date and time the embargo lifts, e.g. 2026-11-03T09:00:00+01:00
onRequestoptional(data: { name: string; outlet: string; email: string }) => voidallHrefoptionalstringlatest: address of all releases, shows the link in the head
Eingebaute Texte
Diese Texte schreibt Pressemitteilung selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 31 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| release | Englisch Press release | Deutsch Pressemitteilung |
| about | Englisch About {c} | Deutsch Über {c} |
| contact | Englisch Press contact | Deutsch Pressekontakt |
| downloads | Englisch Downloads | Deutsch Downloads |
| copyLink | Englisch Copy link | Deutsch Link kopieren |
| linkCopied | Englisch Link copied | Deutsch Link kopiert |
| copyText | Englisch Copy boilerplate | Deutsch Unternehmenstext kopieren |
| textCopied | Englisch Boilerplate copied | Deutsch Unternehmenstext kopiert |
| all | Englisch All | Deutsch Alle |
| year | Englisch Year | Deutsch Jahr |
| allYears | Englisch All years | Deutsch Alle Jahre |
| topic | Englisch Topic | Deutsch Thema |
| read | Englisch Read the release | Deutsch Mitteilung lesen |
| allReleases | Englisch All press releases | Deutsch Alle Pressemitteilungen |
| count | Englisch {n} releases | Deutsch {n} Mitteilungen |
| count1 | Englisch 1 release | Deutsch 1 Mitteilung |
| end | Englisch End of release | Deutsch Ende der Mitteilung |
| embargo | Englisch Under embargo | Deutsch Sperrfrist |
| lifts | Englisch Embargo lifts on {d} | Deutsch Sperrfrist endet am {d} Uhr |
| days | Englisch days | Deutsch Tage |
| hours | Englisch hours | Deutsch Std. |
| minutes | Englisch min | Deutsch Min. |
| lifted | Englisch The embargo has lifted. | Deutsch Die Sperrfrist ist abgelaufen. |
| request | Englisch Request the release | Deutsch Mitteilung anfordern |
| requestText | Englisch Accredited journalists get the full release, pictures and a contact for interviews before the embargo lifts. | Deutsch Akkreditierte Journalistinnen und Journalisten erhalten die ganze Mitteilung, Bilder und einen Kontakt für Interviews vor Ablauf der Sperrfrist. |
| name | Englisch Name | Deutsch Name |
| outlet | Englisch Publication | Deutsch Medium |
| Englisch Work email | Deutsch Berufliche E-Mail | |
| send | Englisch Request access | Deutsch Zugang anfordern |
| sent | Englisch Thank you. We will send the release to {e} within the hour. | Deutsch Danke. Wir schicken die Mitteilung innerhalb einer Stunde an {e}. |
| terms | Englisch By requesting you agree not to publish before the embargo lifts. | Deutsch Mit der Anfrage sicherst du zu, nicht vor Ablauf der Sperrfrist zu veröffentlichen. |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, aria-live und aria-pressed, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen group, separator und status.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- Jedes Feld hat ein Label, sichtbar oder für Screenreader.
- Ergänzt Text für Screenreader, wo Icons und Grafiken allein stehen.
- Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.
Bewegung
- Erscheint beim ersten Sichtkontakt mit einem kurzen Auftritt (kit-rise, useInView).
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 547 Zeilen
'use client';
import { useEffect, useId, useMemo, useState } from 'react';
import { cn, useLocale, useTexts } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon } from '@/components/ui/icon';
import { Input } from '@/components/ui/input';
import { Magnetic, Stagger, useScrollFade } from '@/components/ui/motion';
import { Reveal } from '@/components/ui/reveal';
import { Eyebrow } from '@/components/ui/section';
/*
* Press releases of a company, in the form editors expect: dateline,
* headline, the facts first, a quote, the boilerplate and a contact.
*
* release the release itself, with a side column for contact, downloads
* and copy buttons for link and boilerplate
* newsroom all releases, filtered by topic and year, the newest large
* latest three latest releases as a band for the home or about page
* embargo a release under embargo: time left until it lifts and a
* request form for accredited journalists
*/
export type PressReleaseVariant = 'release' | 'newsroom' | 'latest' | 'embargo';
export interface PressReleaseItem {
title: string;
/** The subheading under the title */
subtitle?: string;
/** ISO date, e.g. 2026-10-21 */
date: string;
/** City of the dateline, e.g. Bremen */
place?: string;
topic?: string;
/** Paragraphs */
body?: string[];
quote?: { text: string; name: string; role?: string };
href?: string;
}
export interface PressContact {
name: string;
role?: string;
email?: string;
phone?: string;
avatar?: React.ReactNode;
}
export interface PressReleaseProps {
variant?: PressReleaseVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
/** The company name, used in the boilerplate heading */
company?: string;
release?: PressReleaseItem;
releases?: PressReleaseItem[];
/** About the company, the paragraph at the end of every release */
boilerplate?: string;
contact?: PressContact;
downloads?: { label: string; meta?: string; href: string }[];
/** embargo: ISO date and time the embargo lifts, e.g. 2026-11-03T09:00:00+01:00 */
embargoUntil?: string;
onRequest?: (data: { name: string; outlet: string; email: string }) => void;
/** latest: address of all releases, shows the link in the head */
allHref?: string;
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
release: 'Press release',
about: 'About {c}',
contact: 'Press contact',
downloads: 'Downloads',
copyLink: 'Copy link',
linkCopied: 'Link copied',
copyText: 'Copy boilerplate',
textCopied: 'Boilerplate copied',
all: 'All',
year: 'Year',
allYears: 'All years',
topic: 'Topic',
read: 'Read the release',
allReleases: 'All press releases',
count: '{n} releases',
count1: '1 release',
end: 'End of release',
embargo: 'Under embargo',
lifts: 'Embargo lifts on {d}',
days: 'days',
hours: 'hours',
minutes: 'min',
lifted: 'The embargo has lifted.',
request: 'Request the release',
requestText: 'Accredited journalists get the full release, pictures and a contact for interviews before the embargo lifts.',
name: 'Name',
outlet: 'Publication',
email: 'Work email',
send: 'Request access',
sent: 'Thank you. We will send the release to {e} within the hour.',
terms: 'By requesting you agree not to publish before the embargo lifts.',
},
de: {
release: 'Pressemitteilung',
about: 'Über {c}',
contact: 'Pressekontakt',
downloads: 'Downloads',
copyLink: 'Link kopieren',
linkCopied: 'Link kopiert',
copyText: 'Unternehmenstext kopieren',
textCopied: 'Unternehmenstext kopiert',
all: 'Alle',
year: 'Jahr',
allYears: 'Alle Jahre',
topic: 'Thema',
read: 'Mitteilung lesen',
allReleases: 'Alle Pressemitteilungen',
count: '{n} Mitteilungen',
count1: '1 Mitteilung',
end: 'Ende der Mitteilung',
embargo: 'Sperrfrist',
lifts: 'Sperrfrist endet am {d} Uhr',
days: 'Tage',
hours: 'Std.',
minutes: 'Min.',
lifted: 'Die Sperrfrist ist abgelaufen.',
request: 'Mitteilung anfordern',
requestText: 'Akkreditierte Journalistinnen und Journalisten erhalten die ganze Mitteilung, Bilder und einen Kontakt für Interviews vor Ablauf der Sperrfrist.',
name: 'Name',
outlet: 'Medium',
email: 'Berufliche E-Mail',
send: 'Zugang anfordern',
sent: 'Danke. Wir schicken die Mitteilung innerhalb einer Stunde an {e}.',
terms: 'Mit der Anfrage sicherst du zu, nicht vor Ablauf der Sperrfrist zu veröffentlichen.',
},
};
type Texts = (typeof TEXTS)['en'];
/* ------------------------------------------------------------------ */
/* Shared pieces */
/* ------------------------------------------------------------------ */
function useDate() {
const de = useLocale() === 'de';
const loc = de ? 'de-DE' : 'en-US';
const long = new Intl.DateTimeFormat(loc, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
// English times without a leading zero (9:00 AM), German with (09:00 Uhr)
const withTime = new Intl.DateTimeFormat(loc, { day: 'numeric', month: 'long', year: 'numeric', hour: de ? '2-digit' : 'numeric', minute: '2-digit', timeZone: 'Europe/Berlin' });
return { long: (iso: string) => long.format(new Date(`${iso.slice(0, 10)}T12:00:00Z`)), withTime: (iso: string) => withTime.format(new Date(iso)), year: (iso: string) => Number(iso.slice(0, 4)) };
}
function CopyButton({ value, label, done, icon = 'copy' }: { value: string; label: string; done: string; icon?: 'copy' | 'link' }) {
const [ok, setOk] = useState(false);
const copy = async () => {
try {
await navigator.clipboard.writeText(value);
setOk(true);
window.setTimeout(() => setOk(false), 1800);
} catch {
/* clipboard blocked */
}
};
return (
<Button size="sm" variant="outline" iconLeft={ok ? 'check' : icon} onClick={copy} aria-live="polite" className={cn('w-full justify-start max-sm:h-11', ok && 'border-[color-mix(in_oklab,var(--success)_45%,var(--border))] text-success-foreground')}>
{ok ? done : label}
</Button>
);
}
function ContactCard({ c, t }: { c?: PressContact; t: Texts }) {
if (!c) return null;
return (
<div>
<p className="text-[12.5px] font-semibold uppercase tracking-[0.07em] text-muted-foreground">{t.contact}</p>
<div className="mt-3 flex items-center gap-3">
{c.avatar && <span data-clip-ok className="size-11 shrink-0 overflow-hidden rounded-full border border-border [&>*]:size-full">{c.avatar}</span>}
<div className="min-w-0 text-[14px] leading-snug">
<p className="font-semibold text-foreground">{c.name}</p>
{c.role && <p className="text-muted-foreground">{c.role}</p>}
</div>
</div>
<div className="mt-3 space-y-1">
{c.email && (
<a href={`mailto:${c.email}`} className="flex min-h-11 items-center gap-2.5 text-[14px] text-foreground transition-colors hover:text-primary">
<Icon name="mail" size={16} className="text-muted-foreground" />
<span className="truncate">{c.email}</span>
</a>
)}
{c.phone && (
<a href={`tel:${c.phone.replace(/\s/g, '')}`} className="flex min-h-11 items-center gap-2.5 text-[14px] tabular-nums text-foreground transition-colors hover:text-primary">
<Icon name="phone" size={16} className="text-muted-foreground" />
{c.phone}
</a>
)}
</div>
</div>
);
}
function Head({ eyebrow, title, description, aside }: { eyebrow?: React.ReactNode; title?: React.ReactNode; description?: React.ReactNode; aside?: React.ReactNode }) {
if (!title && !eyebrow) return null;
return (
<div className="mb-10 flex flex-col gap-6 sm:mb-12 lg:flex-row lg:items-end lg:justify-between">
<div className="max-w-[42rem]">
{eyebrow && <Eyebrow className="kit-rise">{eyebrow}</Eyebrow>}
{title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>{title}</h2>}
{description && <p className="kit-rise mt-4 text-pretty text-lead text-muted-foreground" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{description}</p>}
</div>
{aside}
</div>
);
}
/* ------------------------------------------------------------------ */
/* release */
/* ------------------------------------------------------------------ */
function Release(p: PressReleaseProps & { t: Texts }) {
const { t } = p;
const d = useDate();
const r = p.release;
const [href, setHref] = useState('');
useEffect(() => setHref(window.location.href), []);
if (!r) return null;
const body = r.body ?? [];
return (
<article className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<div className="grid grid-cols-[minmax(0,1fr)] gap-12 lg:grid-cols-[minmax(0,37rem)_minmax(0,17rem)] lg:justify-center lg:gap-20">
<div className="min-w-0">
<p className="kit-rise flex flex-wrap items-center gap-x-3 gap-y-1 text-[13px] font-medium">
<span className="rounded-md bg-[color-mix(in_oklab,var(--primary)_14%,transparent)] px-2 py-0.5 text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">{t.release}</span>
{r.topic && <span className="text-muted-foreground">{r.topic}</span>}
</p>
<h1 className="kit-rise mt-5 text-balance text-[34px] font-semibold leading-[1.1] tracking-[-0.022em] text-foreground sm:text-[44px]" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>
{r.title}
</h1>
{r.subtitle && <p className="kit-rise mt-5 text-pretty text-[19px] leading-[1.55] text-muted-foreground sm:text-[20px]" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{r.subtitle}</p>}
<div className="mt-10 border-t border-border pt-8">
{body.map((para, i) => (
<Reveal key={i} effect="fade" className={cn(i > 0 && 'mt-5')}>
<p className="max-w-[37rem] text-pretty text-[17px] leading-[1.75] text-[color-mix(in_oklab,var(--foreground)_88%,var(--background))]">
{i === 0 && (
<span className="font-semibold text-foreground">
{[r.place, d.long(r.date)].filter(Boolean).join(', ')}.{' '}
</span>
)}
{para}
</p>
{i === 1 && r.quote && (
<figure className="my-10 border-l-2 border-primary pl-6">
<blockquote className="text-pretty text-[21px] font-medium leading-[1.45] tracking-[-0.01em] text-foreground sm:text-[23px]">{r.quote.text}</blockquote>
<figcaption className="mt-3 text-[14px] text-muted-foreground">
<span className="font-semibold text-foreground">{r.quote.name}</span>
{r.quote.role && `, ${r.quote.role}`}
</figcaption>
</figure>
)}
</Reveal>
))}
<p className="mt-8 text-center text-[15px] font-semibold tracking-[0.4em] text-muted-foreground" role="separator" aria-label={t.end}>
###
</p>
</div>
{p.boilerplate && (
<Reveal effect="rise" className="mt-10 rounded-2xl border border-border bg-surface p-5 sm:p-6">
<h2 className="text-[15px] font-semibold text-foreground">{t.about.replace('{c}', p.company ?? '')}</h2>
<p className="mt-2 max-w-[34rem] text-pretty text-[15px] leading-[1.65] text-muted-foreground">{p.boilerplate}</p>
</Reveal>
)}
</div>
<aside className="lg:pt-2">
<div className="space-y-8 lg:sticky lg:top-10">
<ContactCard c={p.contact} t={t} />
{p.downloads && p.downloads.length > 0 && (
<div>
<p className="text-[12.5px] font-semibold uppercase tracking-[0.07em] text-muted-foreground">{t.downloads}</p>
<Stagger as="ul" step={60} className="mt-3 divide-y divide-border border-y border-border">
{p.downloads.map((x) => (
<li key={x.label}>
<a href={x.href} download className="group/d flex min-h-14 items-center gap-3 py-2.5">
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-muted text-foreground transition-colors group-hover/d:bg-primary group-hover/d:text-primary-foreground">
<Icon name="download" size={16} />
</span>
<span className="min-w-0 text-[14px] leading-snug">
<span className="block font-medium text-foreground">{x.label}</span>
{x.meta && <span className="text-[12.5px] text-muted-foreground">{x.meta}</span>}
</span>
</a>
</li>
))}
</Stagger>
</div>
)}
<div className="space-y-2">
<CopyButton value={href} label={t.copyLink} done={t.linkCopied} icon="link" />
{p.boilerplate && <CopyButton value={p.boilerplate} label={t.copyText} done={t.textCopied} />}
</div>
</div>
</aside>
</div>
</div>
</article>
);
}
/* ------------------------------------------------------------------ */
/* newsroom */
/* ------------------------------------------------------------------ */
function Newsroom(p: PressReleaseProps & { t: Texts }) {
const { t } = p;
const yid = useId();
const d = useDate();
const all = p.releases ?? [];
const topics = useMemo(() => [...new Set(all.map((r) => r.topic).filter(Boolean) as string[])], [all]);
const years = useMemo(() => [...new Set(all.map((r) => d.year(r.date)))], [all]); // eslint-disable-line react-hooks/exhaustive-deps
const [topic, setTopic] = useState<string | null>(null);
const [year, setYear] = useState<number | null>(null);
const [rowRef, fade] = useScrollFade<HTMLDivElement>();
const shown = all.filter((r) => (!topic || r.topic === topic) && (!year || d.year(r.date) === year));
const [lead, ...rest] = shown;
const chip = (on: boolean) => cn('min-h-10 whitespace-nowrap rounded-lg border px-3.5 text-[14px] font-medium transition-colors max-sm:min-h-11', on ? 'border-transparent bg-foreground text-background' : 'border-border text-foreground hover:bg-accent');
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<Head eyebrow={p.eyebrow} title={p.title} description={p.description} />
<div className="mb-8 flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div ref={rowRef} className={cn('kit-no-scrollbar -mx-1 overflow-x-auto px-1 [&_a:focus-visible]:outline-offset-[-2px] [&_button:focus-visible]:outline-offset-[-2px]', fade)}>
<div role="group" aria-label={t.topic} className="flex w-max gap-2">
<button type="button" aria-pressed={!topic} onClick={() => setTopic(null)} className={chip(!topic)}>
{t.all}
</button>
{topics.map((x) => (
<button key={x} type="button" aria-pressed={topic === x} onClick={() => setTopic(topic === x ? null : x)} className={chip(topic === x)}>
{x}
</button>
))}
</div>
</div>
<div className="flex items-center gap-3">
<label className="sr-only" htmlFor={yid}>
{t.year}
</label>
<select id={yid} value={year ?? ''} onChange={(e) => setYear(e.target.value ? Number(e.target.value) : null)} className="h-11 rounded-lg border border-border bg-card px-3 text-[14px] text-foreground">
<option value="">{t.allYears}</option>
{years.map((y) => (
<option key={y} value={y}>
{y}
</option>
))}
</select>
<p className="text-[13px] tabular-nums text-muted-foreground" aria-live="polite">
{shown.length === 1 ? t.count1 : t.count.replace('{n}', String(shown.length))}
</p>
</div>
</div>
<div key={`${topic}-${year}`}>
{lead && (
<a href={lead.href ?? '#'} className="kit-rise group/l relative block overflow-hidden rounded-[calc(var(--radius)+12px)] border border-border bg-card p-6 shadow-[var(--highlight),var(--shadow-sm)] transition-[border-color,box-shadow] duration-300 hover:border-border-strong hover:shadow-[var(--highlight),var(--shadow-md)] sm:p-8">
<span className="kit-dots pointer-events-none absolute inset-y-0 right-0 hidden w-1/3 opacity-60 lg:block" aria-hidden />
<p className="relative text-[13px] tabular-nums text-muted-foreground">
{d.long(lead.date)}
{lead.topic && <span className="ml-2 rounded-md bg-muted px-1.5 py-0.5 text-[12px] font-medium text-foreground">{lead.topic}</span>}
</p>
<h3 className="relative mt-4 max-w-[44rem] text-balance text-[26px] font-semibold leading-[1.18] tracking-[-0.018em] text-foreground sm:text-[32px]">{lead.title}</h3>
{lead.subtitle && <p className="relative mt-3 max-w-[40rem] text-pretty text-[16.5px] leading-[1.6] text-muted-foreground">{lead.subtitle}</p>}
<span className="relative mt-6 inline-flex items-center gap-1.5 text-[14.5px] font-medium text-foreground">
{t.read}
<Icon name="arrow-right" size={16} className="transition-transform duration-300 group-hover/l:translate-x-1" />
</span>
</a>
)}
<ol className="mt-4 border-t border-border">
{rest.map((r, i) => (
<li key={r.title} className="kit-rise border-b border-border" style={{ '--kit-delay': `${80 + i * 50}ms`, animationDuration: '.6s' } as React.CSSProperties}>
<a href={r.href ?? '#'} className="group/r -mx-3 grid grid-cols-[minmax(0,1fr)] gap-x-6 gap-y-1 rounded-xl px-3 py-5 transition-colors hover:bg-surface sm:grid-cols-[9rem_minmax(0,1fr)_auto] sm:items-baseline">
<span className="text-[13.5px] tabular-nums text-muted-foreground">{d.long(r.date)}</span>
<span className="min-w-0">
<span className="block text-pretty text-[17px] font-semibold leading-[1.35] text-foreground">{r.title}</span>
{r.subtitle && <span className="mt-1 block line-clamp-2 text-pretty text-[14.5px] leading-[1.55] text-muted-foreground">{r.subtitle}</span>}
</span>
{r.topic && <span className="hidden text-[12.5px] font-medium text-muted-foreground sm:block">{r.topic}</span>}
</a>
</li>
))}
</ol>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* latest */
/* ------------------------------------------------------------------ */
function Latest(p: PressReleaseProps & { t: Texts }) {
const { t } = p;
const d = useDate();
const items = (p.releases ?? []).slice(0, 3);
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<Head
eyebrow={p.eyebrow}
title={p.title}
description={p.description}
aside={
p.allHref && (
<Button href={p.allHref} variant="outline" icon="arrow-right" className="self-start max-sm:w-full lg:self-end">
{t.allReleases}
</Button>
)
}
/>
<Stagger as="ol" step={90} className="grid grid-cols-[minmax(0,1fr)] gap-px overflow-hidden rounded-2xl border border-border bg-border lg:grid-cols-3 [&_a:focus-visible]:outline-offset-[-2px] [&_button:focus-visible]:outline-offset-[-2px]">
{items.map((r) => (
<li key={r.title} className="bg-background">
<a href={r.href ?? '#'} className="group/c relative flex h-full flex-col p-6 transition-colors duration-300 hover:bg-surface sm:p-7">
<span aria-hidden className="absolute inset-x-0 top-0 h-0.5 origin-left scale-x-0 bg-primary transition-transform duration-500 ease-[var(--ease-kit)] group-hover/c:scale-x-100 group-focus-visible/c:scale-x-100" />
<span className="flex items-center justify-between gap-3 text-[13px] text-muted-foreground">
<span className="tabular-nums">{d.long(r.date)}</span>
{r.topic && <span className="font-medium text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">{r.topic}</span>}
</span>
<span className="mt-4 text-balance text-[19px] font-semibold leading-[1.3] tracking-[-0.01em] text-foreground">{r.title}</span>
{r.subtitle && <span className="mt-2 line-clamp-3 text-pretty text-[14.5px] leading-[1.55] text-muted-foreground">{r.subtitle}</span>}
<span className="mt-auto inline-flex items-center gap-1.5 pt-6 text-[14px] font-medium text-foreground">
{t.read}
<Icon name="arrow-right" size={15} className="transition-transform duration-300 group-hover/c:translate-x-1" />
</span>
</a>
</li>
))}
</Stagger>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
/* embargo */
/* ------------------------------------------------------------------ */
function Embargo(p: PressReleaseProps & { t: Texts }) {
const { t } = p;
const uid = useId();
const d = useDate();
const r = p.release;
const until = p.embargoUntil ? new Date(p.embargoUntil).getTime() : 0;
// Server and first render show no numbers, the clock starts in the browser
const [now, setNow] = useState<number | null>(null);
useEffect(() => {
setNow(Date.now());
const id = window.setInterval(() => setNow(Date.now()), 30000);
return () => window.clearInterval(id);
}, []);
const left = now === null ? null : Math.max(0, until - now);
const parts = left === null ? null : [Math.floor(left / 86400000), Math.floor((left % 86400000) / 3600000), Math.floor((left % 3600000) / 60000)];
const [sent, setSent] = useState<string | null>(null);
const [form, setForm] = useState({ name: '', outlet: '', email: '' });
if (!r) return null;
return (
<section className={cn('py-16 sm:py-20 lg:py-24', p.className)}>
<div className="kit-container">
<div className="grid grid-cols-[minmax(0,1fr)] gap-10 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)] lg:gap-16">
<div>
<p className="kit-rise inline-flex items-center gap-2 rounded-lg bg-[color-mix(in_oklab,var(--warning)_18%,transparent)] px-2.5 py-1 text-[13px] font-semibold text-warning-foreground">
<Icon name="lock" size={14} />
{t.embargo}
</p>
<h2 className="kit-rise mt-5 text-balance text-[30px] font-semibold leading-[1.12] tracking-[-0.022em] text-foreground sm:text-[40px]" style={{ '--kit-delay': '60ms' } as React.CSSProperties}>
{r.title}
</h2>
{r.subtitle && <p className="kit-rise mt-4 text-pretty text-[17.5px] leading-[1.6] text-muted-foreground" style={{ '--kit-delay': '120ms' } as React.CSSProperties}>{r.subtitle}</p>}
{p.embargoUntil && (
<Reveal effect="rise" className="mt-8">
<p className="text-[14px] text-muted-foreground">{t.lifts.replace('{d}', d.withTime(p.embargoUntil))}</p>
<div className="mt-3 grid max-w-[24rem] grid-cols-3 gap-px overflow-hidden rounded-xl border border-border bg-border" aria-live="polite">
{[t.days, t.hours, t.minutes].map((label, i) => (
<div key={label} className="bg-card px-4 py-4 text-center">
<p className="text-[34px] font-semibold leading-none tracking-[-0.03em] text-foreground tabular-nums">{parts ? String(parts[i]).padStart(2, '0') : '00'}</p>
<p className="mt-1.5 text-[12.5px] text-muted-foreground">{label}</p>
</div>
))}
</div>
{left === 0 && <p className="mt-3 text-[14px] font-medium text-success-foreground">{t.lifted}</p>}
</Reveal>
)}
<Reveal effect="fade" className="mt-10 hidden lg:block">
<ContactCard c={p.contact} t={t} />
</Reveal>
</div>
<Reveal effect="rise" delay={120} className="kit-card relative self-start overflow-hidden p-6 sm:p-8">
<span className="kit-beam pointer-events-none absolute inset-0 rounded-[inherit]" aria-hidden />
<h3 className="text-[20px] font-semibold text-foreground">{t.request}</h3>
<p className="mt-2 text-pretty text-[15px] leading-[1.6] text-muted-foreground">{t.requestText}</p>
{sent ? (
<p role="status" className="kit-rise mt-6 flex items-start gap-3 rounded-xl border border-[color-mix(in_oklab,var(--success)_40%,var(--border))] bg-[color-mix(in_oklab,var(--success)_8%,transparent)] p-4 text-[14.5px] leading-[1.5] text-foreground">
<span className="grid size-6 shrink-0 place-items-center rounded-full bg-success text-white">
<Icon name="check" size={14} strokeWidth={2.4} />
</span>
{t.sent.replace('{e}', sent)}
</p>
) : (
<form
className="mt-6 space-y-4"
onSubmit={(e) => {
e.preventDefault();
p.onRequest?.(form);
setSent(form.email);
}}
>
<Input id={`${uid}-n`} label={t.name} required autoComplete="name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
<Input id={`${uid}-o`} label={t.outlet} required autoComplete="organization" value={form.outlet} onChange={(e) => setForm({ ...form, outlet: e.target.value })} />
<Input id={`${uid}-e`} label={t.email} type="email" required autoComplete="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} />
<Magnetic className="flex w-full">
<Button type="submit" className="w-full" icon="arrow-right">
{t.send}
</Button>
</Magnetic>
<p className="text-[12.5px] leading-[1.5] text-muted-foreground">{t.terms}</p>
</form>
)}
</Reveal>
<div className="lg:hidden">
<ContactCard c={p.contact} t={t} />
</div>
</div>
</div>
</section>
);
}
/* ------------------------------------------------------------------ */
export function PressRelease(props: PressReleaseProps) {
const t = useTexts(TEXTS, props.labels);
const { variant = 'release' } = props;
if (variant === 'newsroom') return <Newsroom {...props} t={t} />;
if (variant === 'latest') return <Latest {...props} t={t} />;
if (variant === 'embargo') return <Embargo {...props} t={t} />;
return <Release {...props} t={t} />;
}Häufige Fragen
Wie installiere ich Pressemitteilung?
Mit aiw add @ai-website/press-release. Der Befehl legt die Datei components/ui/press-release.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, input, reveal, motion und section. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Pressemitteilung?
4 Varianten, Mitteilung, Newsroom, Neueste Mitteilungen und Unter Sperrfrist. Du wählst sie mit der Prop variant, zum Beispiel variant="newsroom".
Kann ich die Texte von Pressemitteilung übersetzen?
Ja. Pressemitteilung bringt 31 eigene Texte auf Deutsch und Englisch mit, etwa „Pressemitteilung“, „Pressekontakt“ und „Downloads“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Pressemitteilung einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Pressemitteilung mit aiw_components und installieren sie mit aiw_add @ai-website/press-release. Nenn im Prompt die Variante, etwa variant="release", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Pressemitteilung ohne Code anpassen?
Ja, im Studio. Alle 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden PressRelease-Code zum Kopieren.