Teilen
Eine Seite per Link, E-Mail, Nachricht, Teilen-Menü des Geräts oder QR-Code teilen. Keine fremden Logos, eigene Kanäle als URL-Vorlage.
Eine von 3 Components in Blog
Variants
Details
Was Teilen mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Blog
- Bereich
- Content
- Variants
- 4
- Version
- 1.0.1 · seit 1.0.0
- Datei
- components/ui/share.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 22 eigene Texte
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle ShareProps aus components/ui/share.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
variantoptionalShareVariantinline-rowside-railarticle-endquote-select
urloptionalstringAddress to share, default the current page
titleoptionalstringTitle of the page, default the document title
textoptionalstringA short line sent with email and message
channelsoptionalShareChannel[]Your own channels after the built-in ones
hideoptional('copy' | 'email' | 'message' | 'native' | 'qr')[]Hide built-in ways, e.g. ['qr', 'message']
authoroptionalShareAuthorarticle-end: the author
paragraphsoptionalstring[]side-rail, quote-select: the article text, one string per paragraph
quotesoptionalstring[]quote-select: sentences shown as pull quotes
headingoptionalstringside-rail, quote-select: the article heading
onShareoptional(way: string) => voidCalled with the way something was shared, e.g. copy or email
Eingebaute Texte
Diese Texte schreibt Teilen selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 22 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| share | Englisch Share | Deutsch Teilen |
| shareArticle | Englisch Share this article | Deutsch Artikel teilen |
| copy | Englisch Copy link | Deutsch Link kopieren |
| copied | Englisch Link copied | Deutsch Link kopiert |
| Englisch Email | Deutsch E-Mail | |
| message | Englisch Message | Deutsch Nachricht |
| native | Englisch More options | Deutsch Weitere Optionen |
| qr | Englisch QR code | Deutsch QR-Code |
| qrHint | Englisch Point your phone camera here to open this page on your phone. | Deutsch Halte die Handykamera darauf, um die Seite auf dem Telefon zu öffnen. |
| close | Englisch Close | Deutsch Schließen |
| read | Englisch {n}% read | Deutsch {n} % gelesen |
| writtenBy | Englisch Written by | Deutsch Geschrieben von |
| helpful | Englisch Was this article helpful? | Deutsch War dieser Artikel hilfreich? |
| yes | Englisch Yes | Deutsch Ja |
| no | Englisch No | Deutsch Nein |
| thanks | Englisch Thank you. Your answer helps us write the next one. | Deutsch Danke. Deine Antwort hilft uns beim nächsten Artikel. |
| shareQuote | Englisch Share quote | Deutsch Zitat teilen |
| copyQuote | Englisch Copy quote | Deutsch Zitat kopieren |
| quoteCopied | Englisch Quote copied | Deutsch Zitat kopiert |
| selectHint | Englisch Select a sentence to share it | Deutsch Markiere einen Satz, um ihn zu teilen |
| qo | Englisch “ | Deutsch „ |
| qc | Englisch ” | Deutsch “ |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Teilen reagiert selbst auf die Tasten Escape.
- Setzt aria-describedby und aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rollen img, status, toolbar und tooltip.
- Meldet Änderungen über eine Live-Region, ohne den Fokus zu verschieben.
- 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).
- Bewegt sich mit dem Scrollen, der Fortschritt folgt der Position auf der Seite.
- Bringt eigene Animationen mit, ruhig getaktet und ohne Wackeln.
- Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 2 Stellen im Code schalten Bewegung dann ab.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 572 Zeilen
'use client';
import { useEffect, useId, useRef, useState } from 'react';
import { cn, prefersReducedMotion, useTexts } from '@/lib/kit';
import { useScrollProgress } from '@/components/ui/motion';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Reveal } from '@/components/ui/reveal';
import { Section } from '@/components/ui/section';
import { QrPattern } from '@/components/ui/graphics';
/*
* Sharing a page without a single foreign logo. The built-in ways are the
* ones every visitor has: copy the link, send it by email or message, the
* share sheet of the device and a QR code for the phone next to you. Add
* your own channels with a URL template ({url}, {title}, {text}).
*
* inline-row a row of round buttons under the article header
* side-rail a rail next to the text that follows the reading
* article-end author box, share and a short "was this helpful"
* quote-select select a sentence or press a pull quote to share it
*/
export type ShareVariant = 'inline-row' | 'side-rail' | 'article-end' | 'quote-select';
export interface ShareChannel {
label: string;
icon: IconName;
/** URL template with {url}, {title} and {text}, e.g. https://example.com/share?u={url} */
href: string;
}
export interface ShareAuthor {
name: string;
role?: string;
bio?: string;
avatar?: React.ReactNode;
links?: { label: string; href: string }[];
}
export interface ShareProps {
variant?: ShareVariant;
/** Address to share, default the current page */
url?: string;
/** Title of the page, default the document title */
title?: string;
/** A short line sent with email and message */
text?: string;
/** Your own channels after the built-in ones */
channels?: ShareChannel[];
/** Hide built-in ways, e.g. ['qr', 'message'] */
hide?: ('copy' | 'email' | 'message' | 'native' | 'qr')[];
/** article-end: the author */
author?: ShareAuthor;
/** side-rail, quote-select: the article text, one string per paragraph */
paragraphs?: string[];
/** quote-select: sentences shown as pull quotes */
quotes?: string[];
/** side-rail, quote-select: the article heading */
heading?: string;
/** Called with the way something was shared, e.g. copy or email */
onShare?: (way: string) => void;
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: {
share: 'Share',
shareArticle: 'Share this article',
copy: 'Copy link',
copied: 'Link copied',
email: 'Email',
message: 'Message',
native: 'More options',
qr: 'QR code',
qrHint: 'Point your phone camera here to open this page on your phone.',
close: 'Close',
read: '{n}% read',
writtenBy: 'Written by',
helpful: 'Was this article helpful?',
yes: 'Yes',
no: 'No',
thanks: 'Thank you. Your answer helps us write the next one.',
shareQuote: 'Share quote',
copyQuote: 'Copy quote',
quoteCopied: 'Quote copied',
selectHint: 'Select a sentence to share it',
qo: '“',
qc: '”',
},
de: {
share: 'Teilen',
shareArticle: 'Artikel teilen',
copy: 'Link kopieren',
copied: 'Link kopiert',
email: 'E-Mail',
message: 'Nachricht',
native: 'Weitere Optionen',
qr: 'QR-Code',
qrHint: 'Halte die Handykamera darauf, um die Seite auf dem Telefon zu öffnen.',
close: 'Schließen',
read: '{n} % gelesen',
writtenBy: 'Geschrieben von',
helpful: 'War dieser Artikel hilfreich?',
yes: 'Ja',
no: 'Nein',
thanks: 'Danke. Deine Antwort hilft uns beim nächsten Artikel.',
shareQuote: 'Zitat teilen',
copyQuote: 'Zitat kopieren',
quoteCopied: 'Zitat kopiert',
selectHint: 'Markiere einen Satz, um ihn zu teilen',
qo: '„',
qc: '“',
},
};
type Texts = (typeof TEXTS)['en'];
/* ------------------------------------------------------------------ */
/* Ways to share */
/* ------------------------------------------------------------------ */
const fill = (tpl: string, d: { url: string; title: string; text: string }) =>
tpl.replace(/\{url\}/g, encodeURIComponent(d.url)).replace(/\{title\}/g, encodeURIComponent(d.title)).replace(/\{text\}/g, encodeURIComponent(d.text));
function useShare(p: ShareProps) {
const [copied, setCopied] = useState<string | null>(null);
const timer = useRef(0);
useEffect(() => () => window.clearTimeout(timer.current), []);
const data = () => ({ url: p.url ?? window.location.href, title: p.title ?? document.title, text: p.text ?? '' });
const copy = async (value?: string, what = 'link') => {
try {
await navigator.clipboard.writeText(value ?? data().url);
} catch {
/* clipboard blocked, the confirmation still shows */
}
setCopied(what);
p.onShare?.(what === 'link' ? 'copy' : what);
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => setCopied(null), 1800);
};
const open = (tpl: string, way: string) => {
p.onShare?.(way);
window.location.href = fill(tpl, data());
};
const native = async () => {
const d = data();
p.onShare?.('native');
if (navigator.share) {
try {
await navigator.share({ url: d.url, title: d.title, text: d.text });
} catch {
/* closed by the visitor */
}
} else copy();
};
return { copied, copy, open, native, data };
}
interface Way {
id: string;
label: string;
icon: IconName;
run: () => void;
/** A real address where one is known on the server */
href?: string;
}
function useWays(p: ShareProps, t: Texts, share: ReturnType<typeof useShare>, onQr?: () => void): Way[] {
const hide = new Set(p.hide ?? []);
const ssr = { url: p.url ?? '', title: p.title ?? '', text: p.text ?? '' };
const ways: Way[] = [];
if (!hide.has('copy')) ways.push({ id: 'copy', label: share.copied === 'link' ? t.copied : t.copy, icon: share.copied === 'link' ? 'check' : 'link', run: () => share.copy() });
if (!hide.has('email')) ways.push({ id: 'email', label: t.email, icon: 'mail', href: p.url ? fill('mailto:?subject={title}&body={text}%0A%0A{url}', ssr) : undefined, run: () => share.open('mailto:?subject={title}&body={text}%0A%0A{url}', 'email') });
if (!hide.has('message')) ways.push({ id: 'message', label: t.message, icon: 'message-circle', href: p.url ? fill('sms:?&body={title}%20{url}', ssr) : undefined, run: () => share.open('sms:?&body={title}%20{url}', 'message') });
for (const c of p.channels ?? []) ways.push({ id: c.label, label: c.label, icon: c.icon, href: p.url ? fill(c.href, ssr) : undefined, run: () => share.open(c.href, c.label) });
if (!hide.has('qr') && onQr) ways.push({ id: 'qr', label: t.qr, icon: 'qr-code', run: onQr });
if (!hide.has('native')) ways.push({ id: 'native', label: t.native, icon: 'share-out', run: () => share.native() });
return ways;
}
/** A round button with a tooltip that rises on hover and focus */
function WayButton({ way, size = 'md', tip = 'top', active }: { way: Way; size?: 'md' | 'lg'; tip?: 'top' | 'right'; active?: boolean }) {
const cls = cn(
'group/way relative grid shrink-0 place-items-center rounded-full border border-border bg-card text-foreground shadow-[var(--highlight),var(--shadow-sm)] transition-[transform,background-color,border-color,color] duration-300 ease-[var(--ease-kit)] hover:-translate-y-0.5 hover:border-border-strong active:scale-95 motion-reduce:hover:translate-y-0',
size === 'lg' ? 'size-12' : 'size-11',
(way.icon === 'check' || active) && 'border-[color-mix(in_oklch,var(--success)_40%,var(--border))] text-success',
);
const inner = (
<>
<Icon name={way.icon} size={17} className={cn('transition-transform duration-300', way.icon === 'check' && 'scale-110')} />
<span
role="tooltip"
className={cn(
'pointer-events-none absolute z-20 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-[12px] font-medium text-background opacity-0 shadow-[var(--shadow-md)] transition-[opacity,transform] duration-200 group-hover/way:opacity-100 group-focus-visible/way:opacity-100 max-sm:hidden',
tip === 'top' ? 'bottom-[calc(100%+8px)] left-1/2 -translate-x-1/2 translate-y-1 group-hover/way:translate-y-0 group-focus-visible/way:translate-y-0' : 'left-[calc(100%+10px)] top-1/2 -translate-x-1 -translate-y-1/2 group-hover/way:translate-x-0 group-focus-visible/way:translate-x-0',
)}
>
{way.label}
</span>
</>
);
if (way.href)
return (
<a
href={way.href}
aria-label={way.label}
onClick={(e) => {
e.preventDefault();
way.run();
}}
className={cls}
>
{inner}
</a>
);
return (
<button type="button" aria-label={way.label} onClick={way.run} className={cls}>
{inner}
</button>
);
}
function QrPanel({ open, onClose, t, className }: { open: boolean; onClose: () => void; t: Texts; className?: string }) {
return (
<div className={cn('grid transition-[grid-template-rows,opacity] duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none', open ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0', className)}>
<div className="min-h-0 overflow-hidden" inert={!open}>
<div className="mt-4 flex items-center gap-4 rounded-2xl border border-border bg-surface p-4">
<span className={cn('block size-24 shrink-0 rounded-xl bg-white p-1.5 shadow-[var(--shadow-sm)] transition-transform duration-500 ease-[var(--ease-kit)]', open ? 'scale-100' : 'scale-90')}>
<QrPattern seed={11} className="size-full" />
</span>
<p className="flex-1 text-pretty text-[13.5px] leading-relaxed text-muted-foreground">{t.qrHint}</p>
<button type="button" onClick={onClose} aria-label={t.close} className="grid size-9 shrink-0 place-items-center self-start rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground max-sm:size-11">
<Icon name="x" size={16} />
</button>
</div>
</div>
</div>
);
}
function Status({ copied, t }: { copied: string | null; t: Texts }) {
return (
<span role="status" className="sr-only">
{copied === 'link' ? t.copied : copied === 'quote' ? t.quoteCopied : ''}
</span>
);
}
/* ------------------------------------------------------------------ */
/* inline-row */
/* ------------------------------------------------------------------ */
function InlineRow(p: ShareProps & { t: Texts }) {
const { t } = p;
const share = useShare(p);
const [qr, setQr] = useState(false);
const ways = useWays(p, t, share, () => setQr((v) => !v));
return (
<div className={p.className}>
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-5">
<p className="text-[14px] font-medium text-foreground">{t.shareArticle}</p>
<ul className="flex flex-wrap gap-2.5">
{ways.map((w, i) => (
<li key={w.id} className="kit-rise" style={{ '--kit-delay': `${i * 50}ms` } as React.CSSProperties}>
<WayButton way={w} active={w.id === 'qr' && qr} />
</li>
))}
</ul>
</div>
<QrPanel open={qr} onClose={() => setQr(false)} t={t} className="max-w-[28rem]" />
<Status copied={share.copied} t={t} />
</div>
);
}
/* ------------------------------------------------------------------ */
/* side-rail */
/* ------------------------------------------------------------------ */
function SideRail(p: ShareProps & { t: Texts }) {
const { t } = p;
const share = useShare(p);
const ways = useWays(p, t, share);
const ref = useScrollProgress<HTMLDivElement>({ start: 0.15, end: 0.85 });
const [pct, setPct] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
let frame = 0;
const read = () => {
frame = 0;
setPct(Math.round(Number(getComputedStyle(el).getPropertyValue('--kit-progress') || 0) * 100));
};
const on = () => {
if (!frame) frame = requestAnimationFrame(read);
};
read();
window.addEventListener('scroll', on, { passive: true });
return () => {
cancelAnimationFrame(frame);
window.removeEventListener('scroll', on);
};
}, [ref]);
return (
<Section spacing="sm" className={p.className}>
<div ref={ref} className="grid grid-cols-[minmax(0,1fr)] gap-8 lg:grid-cols-[4rem_minmax(0,44rem)] lg:justify-center lg:gap-16">
<aside className="hidden lg:block">
<div className="sticky top-28 flex flex-col items-center gap-2.5">
{/* The ring fills while the article is read */}
<div className="relative mb-2 grid size-12 place-items-center" aria-label={t.read.replace('{n}', String(pct))} role="img">
<svg viewBox="0 0 48 48" className="absolute inset-0 -rotate-90" aria-hidden>
<circle cx="24" cy="24" r="21" fill="none" stroke="var(--border)" strokeWidth="2" />
<circle cx="24" cy="24" r="21" fill="none" stroke="var(--primary)" strokeWidth="2" strokeLinecap="round" pathLength={100} strokeDasharray="100" strokeDashoffset={100 - pct} className="transition-[stroke-dashoffset] duration-200" />
</svg>
<span className="relative text-[11px] font-semibold tabular-nums text-foreground">{pct}</span>
</div>
<p className="text-[11.5px] font-medium text-muted-foreground">{t.share}</p>
{ways.map((w) => (
<WayButton key={w.id} way={w} tip="right" />
))}
</div>
</aside>
<article className="min-w-0">
{p.heading && <h2 className="mb-6 text-balance text-h2 font-semibold text-foreground">{p.heading}</h2>}
<div className="space-y-5 text-pretty text-[17px] leading-[1.75] text-[color-mix(in_oklab,var(--foreground)_84%,var(--background))]">
{(p.paragraphs ?? []).map((x, i) => (
<Reveal as="div" key={i} effect="fade">
<p>{x}</p>
</Reveal>
))}
</div>
{/* Phones and tablets: the same ways in a row at the end */}
<div className="mt-10 border-t border-border pt-6 lg:hidden">
<p className="mb-3 text-[14px] font-medium text-foreground">{t.shareArticle}</p>
<ul className="flex flex-wrap gap-2.5">
{ways.map((w) => (
<li key={w.id}>
<WayButton way={w} />
</li>
))}
</ul>
</div>
</article>
</div>
<Status copied={share.copied} t={t} />
</Section>
);
}
/* ------------------------------------------------------------------ */
/* article-end */
/* ------------------------------------------------------------------ */
function ArticleEnd(p: ShareProps & { t: Texts }) {
const { t } = p;
const share = useShare(p);
const [qr, setQr] = useState(false);
const ways = useWays(p, t, share, () => setQr((v) => !v));
const [vote, setVote] = useState<'yes' | 'no' | null>(null);
const a = p.author;
return (
<Section spacing="sm" className={p.className}>
<div className="mx-auto max-w-[46rem] space-y-4">
{a && (
<Reveal className="rounded-2xl border border-border bg-card p-5 shadow-[var(--highlight),var(--shadow-sm)] sm:p-7">
<div className="flex flex-col gap-5 sm:flex-row">
<span data-clip-ok className="relative grid size-16 shrink-0 place-items-center overflow-hidden rounded-full bg-muted text-[18px] font-semibold text-muted-foreground ring-1 ring-border sm:size-20 [&>*]:size-full">
{a.avatar ?? a.name.split(/\s+/).map((w) => w[0]).slice(0, 2).join('')}
</span>
<div className="min-w-0 flex-1">
<p className="text-[12.5px] font-medium text-muted-foreground">{t.writtenBy}</p>
<p className="mt-0.5 text-[18px] font-semibold text-foreground">{a.name}</p>
{a.role && <p className="text-[14px] text-muted-foreground">{a.role}</p>}
{a.bio && <p className="mt-3 text-pretty text-[15px] leading-relaxed text-[color-mix(in_oklab,var(--foreground)_84%,var(--background))]">{a.bio}</p>}
{a.links && a.links.length > 0 && (
<ul className="-ml-2 mt-3 flex flex-wrap gap-1">
{a.links.map((l) => (
<li key={l.label}>
<a href={l.href} className="group/al inline-flex min-h-10 items-center gap-1 rounded-md px-2 text-[14px] font-medium text-foreground transition-colors hover:bg-accent">
{l.label}
<Icon name="arrow-up-right" size={14} className="text-muted-foreground transition-transform duration-300 group-hover/al:-translate-y-0.5 group-hover/al:translate-x-0.5" />
</a>
</li>
))}
</ul>
)}
</div>
</div>
</Reveal>
)}
<Reveal delay={80} className="grid grid-cols-[minmax(0,1fr)] gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-border bg-card p-5 shadow-[var(--highlight),var(--shadow-sm)]">
<p className="mb-3 text-[14px] font-semibold text-foreground">{t.shareArticle}</p>
<ul className="flex flex-wrap gap-2">
{ways.map((w) => (
<li key={w.id}>
<WayButton way={w} active={w.id === 'qr' && qr} />
</li>
))}
</ul>
<QrPanel open={qr} onClose={() => setQr(false)} t={t} />
</div>
<div className="rounded-2xl border border-border bg-card p-5 shadow-[var(--highlight),var(--shadow-sm)]">
<p className="mb-3 text-[14px] font-semibold text-foreground">{t.helpful}</p>
{vote ? (
<p role="status" className="kit-rise flex gap-2.5 text-pretty text-[14px] leading-relaxed text-muted-foreground">
<Icon name="check-circle" size={18} className="mt-0.5 shrink-0 text-success" />
{t.thanks}
</p>
) : (
<div className="flex gap-2">
{(['yes', 'no'] as const).map((v) => (
<Button
key={v}
variant="outline"
iconLeft={v === 'yes' ? 'thumbs-up' : 'thumbs-down'}
onClick={() => {
setVote(v);
p.onShare?.(`helpful-${v}`);
}}
className="flex-1"
>
{t[v]}
</Button>
))}
</div>
)}
</div>
</Reveal>
</div>
<Status copied={share.copied} t={t} />
</Section>
);
}
/* ------------------------------------------------------------------ */
/* quote-select */
/* ------------------------------------------------------------------ */
function QuoteSelect(p: ShareProps & { t: Texts }) {
const { t } = p;
const share = useShare(p);
const boxRef = useRef<HTMLDivElement>(null);
const [bubble, setBubble] = useState<{ x: number; y: number; text: string } | null>(null);
const tipId = useId();
// A bubble above the selection, inside the article only
useEffect(() => {
const onUp = () => {
window.setTimeout(() => {
const sel = window.getSelection();
const box = boxRef.current;
const text = sel?.toString().trim() ?? '';
if (!sel || !box || !text || text.length < 12 || !sel.rangeCount || !box.contains(sel.anchorNode)) {
setBubble(null);
return;
}
const r = sel.getRangeAt(0).getBoundingClientRect();
const b = box.getBoundingClientRect();
const x = Math.min(Math.max(r.left + r.width / 2 - b.left, 110), b.width - 110);
setBubble({ x: Math.round(x), y: Math.round(r.top - b.top), text });
}, 0);
};
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setBubble(null);
document.addEventListener('pointerup', onUp);
document.addEventListener('keyup', onUp);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('pointerup', onUp);
document.removeEventListener('keyup', onUp);
document.removeEventListener('keydown', onKey);
};
}, []);
const shareQuote = (text: string) => {
const d = share.data();
p.onShare?.('quote');
if (navigator.share) navigator.share({ url: d.url, title: d.title, text: `${t.qo}${text}${t.qc}` }).catch(() => {});
else share.copy(`${t.qo}${text}${t.qc} ${d.url}`, 'quote');
};
const quotes = p.quotes ?? [];
return (
<Section spacing="sm" className={p.className}>
<div ref={boxRef} className="relative mx-auto max-w-[44rem]">
{p.heading && <h2 className="mb-6 text-balance text-h2 font-semibold text-foreground sm:mb-2">{p.heading}</h2>}
<p className="mb-8 inline-flex items-center gap-2 text-[13px] text-muted-foreground max-sm:hidden">
<Icon name="type" size={14} />
{t.selectHint}
</p>
<div className="space-y-6 text-pretty text-[17px] leading-[1.75] text-[color-mix(in_oklab,var(--foreground)_84%,var(--background))] selection:bg-[color-mix(in_oklab,var(--primary)_22%,transparent)]">
{(p.paragraphs ?? []).map((x, i) => (
<div key={i}>
<p>{x}</p>
{quotes[i] && (
<Reveal className="mt-6">
<figure className="group/q relative">
<blockquote className="border-l-2 border-primary pl-5 text-[21px] font-medium leading-snug tracking-[-0.015em] text-foreground sm:pl-6 sm:text-[24px]">{t.qo}{quotes[i]}{t.qc}</blockquote>
<div className="mt-3 flex gap-1 pl-[14px] sm:pl-[18px]">
<button type="button" onClick={() => shareQuote(quotes[i])} className="inline-flex min-h-10 items-center gap-1.5 rounded-md px-2 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground max-sm:min-h-11">
<Icon name="share-out" size={14} />
{t.shareQuote}
</button>
<button type="button" onClick={() => share.copy(`${t.qo}${quotes[i]}${t.qc}`, 'quote')} className="inline-flex min-h-10 items-center gap-1.5 rounded-md px-2 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground max-sm:min-h-11">
<Icon name={share.copied === 'quote' ? 'check' : 'copy'} size={14} />
{share.copied === 'quote' ? t.quoteCopied : t.copyQuote}
</button>
</div>
</figure>
</Reveal>
)}
</div>
))}
</div>
{bubble && (
<div
role="toolbar"
aria-describedby={tipId}
className="absolute z-30 flex -translate-x-1/2 -translate-y-[calc(100%+10px)] items-center gap-0.5 rounded-xl border border-border bg-popover p-1 shadow-[var(--highlight),var(--shadow-lg)]"
style={{ left: bubble.x, top: bubble.y, animation: prefersReducedMotion() ? undefined : 'kit-rise .35s var(--ease-kit) both' }}
onPointerUp={(e) => e.stopPropagation()}
>
<span id={tipId} className="sr-only">
{t.selectHint}
</span>
<button type="button" onClick={() => shareQuote(bubble.text)} className="inline-flex h-9 items-center gap-1.5 rounded-lg px-2.5 text-[13px] font-medium text-foreground transition-colors hover:bg-accent">
<Icon name="share-out" size={14} />
{t.shareQuote}
</button>
<button
type="button"
onClick={() => {
share.copy(`${t.qo}${bubble.text}${t.qc}`, 'quote');
setBubble(null);
}}
className="inline-flex h-9 items-center gap-1.5 rounded-lg px-2.5 text-[13px] font-medium text-foreground transition-colors hover:bg-accent"
>
<Icon name="copy" size={14} />
{t.copyQuote}
</button>
<span aria-hidden className="absolute -bottom-1.5 left-1/2 size-3 -translate-x-1/2 rotate-45 border-b border-r border-border bg-popover" />
</div>
)}
</div>
<Status copied={share.copied} t={t} />
</Section>
);
}
/* ------------------------------------------------------------------ */
/* Share */
/* ------------------------------------------------------------------ */
const VARIANTS: Record<ShareVariant, (p: ShareProps & { t: Texts }) => React.ReactNode> = {
'inline-row': InlineRow,
'side-rail': SideRail,
'article-end': ArticleEnd,
'quote-select': QuoteSelect,
};
/**
* Share a page by link, email, message, the device share sheet or a QR code.
* <Share variant="inline-row" title="…" text="…" />
* <Share variant="article-end" author={{ name, role, bio }} />
*/
export function Share({ variant = 'inline-row', labels, ...props }: ShareProps) {
const t = useTexts(TEXTS, labels);
const View = VARIANTS[variant] ?? InlineRow;
return <View {...props} t={t} />;
}Häufige Fragen
Wie installiere ich Teilen?
Mit aiw add @ai-website/share. Der Befehl legt die Datei components/ui/share.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind button, icon, reveal, motion, section und graphics. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Teilen?
4 Varianten, In einer Reihe, Seitenleiste, Artikelende und Zitat teilen. Du wählst sie mit der Prop variant, zum Beispiel variant="side-rail".
Kann ich die Texte von Teilen übersetzen?
Ja. Teilen bringt 22 eigene Texte auf Deutsch und Englisch mit, etwa „Teilen“, „Artikel teilen“ und „Link kopieren“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Teilen einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Teilen mit aiw_components und installieren sie mit aiw_add @ai-website/share. Nenn im Prompt die Variante, etwa variant="inline-row", dann schreibt der Assistent deine Inhalte direkt in die Props.
Kann ich Teilen ohne Code anpassen?
Ja, im Studio. 3 von 4 Varianten lassen sich dort bearbeiten, Texte, Listen und Schalter. Das Studio schreibt den passenden Share-Code zum Kopieren.