Seitenaufbau
Components heben aus einer Palette ab, fliegen an ihren Platz und landen, bis die Seite fertig ist und veröffentlicht wird. Von selbst oder mit dem Scrollen.
Eine von 4 Components in Animations
Details
Was Seitenaufbau mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Animations
- Bereich
- Visuals
- Variants
- 3
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/page-assembly.tsx
- Bringt mit
- utilskitmotionmotion-scene
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 13 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle PageAssemblyProps aus components/ui/page-assembly.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von SceneMotionProps an.
presetoptional'landing' | 'app'A ready set: a landing page or an app screen
blocksoptionalAssemblyBlock[]Your own blocks instead of the preset
modeoptional'time' | 'scroll'time: plays on its own (default). scroll: each block lands as you scroll.
urloptionalstringAddress in the browser bar
labeloptionalstring
Eingebaute Texte
Diese Texte schreibt Seitenaufbau selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.
Alle 13 Texte anzeigen
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| palette | Englisch Components | Deutsch Components |
| published | Englisch Published | Deutsch Veröffentlicht |
| nav | Englisch Navbar | Deutsch Navbar |
| hero | Englisch Hero | Deutsch Hero |
| features | Englisch Features | Deutsch Features |
| pricing | Englisch Pricing | Deutsch Pricing |
| footer | Englisch Footer | Deutsch Footer |
| sidebar | Englisch Sidebar | Deutsch Sidebar |
| header | Englisch Header | Deutsch Header |
| kpis | Englisch Stats | Deutsch Stats |
| chart | Englisch Chart | Deutsch Chart |
| activity | Englisch Activity | Deutsch Aktivität |
| table | Englisch Table | Deutsch Tabelle |
Barrierefreiheit
Aus dem Quellcode der Component abgeleitet.
- Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
- Nutzt die Rolle label.
- Kommt ohne Client-State aus. Der Server schreibt sie vollständig, sie ist auch ohne JavaScript lesbar.
Bewegung
- Ohne Animationen, sie steht sofort ruhig da.
Quellcode
Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.
Datei anzeigen, 405 Zeilen
'use client';
import { cn, round, useTexts } from '@/lib/kit';
import { useScrollProgress } from '@/components/ui/motion';
import { FitCanvas, PlayToggle, SCENE_CSS, SceneTimeline, Track, useSceneId, useScenePlay, type SceneMotionProps } from '@/components/ui/motion-scene';
/*
* Components put themselves together into a page. A palette of components
* on the left, an empty browser on the right. One after another a component
* lifts, flies to its place and lands, the section appears where it lands,
* and at the end the page is published. Either on its own timeline or tied
* to the scroll. Drawn at a design size of 760 × 500 and scaled as one
* picture. The finished page is the still picture.
*/
export interface AssemblySlot {
x: number;
y: number;
w: number;
h: number;
}
export interface AssemblyBlock {
id: string;
/** Name on the chip in the palette */
label: string;
/** Small mark on the chip */
icon?: React.ReactNode;
/** Where it lands, in design px of the 760 × 500 canvas */
slot: AssemblySlot;
/** What appears in the slot */
content: React.ReactNode;
}
const TEXTS = {
en: { palette: 'Components', published: 'Published', nav: 'Navbar', hero: 'Hero', features: 'Features', pricing: 'Pricing', footer: 'Footer', sidebar: 'Sidebar', header: 'Header', kpis: 'Stats', chart: 'Chart', activity: 'Activity', table: 'Table' },
de: { palette: 'Components', published: 'Veröffentlicht', nav: 'Navbar', hero: 'Hero', features: 'Features', pricing: 'Pricing', footer: 'Footer', sidebar: 'Sidebar', header: 'Header', kpis: 'Stats', chart: 'Chart', activity: 'Aktivität', table: 'Tabelle' },
};
export interface PageAssemblyProps extends SceneMotionProps {
/** A ready set: a landing page or an app screen */
preset?: 'landing' | 'app';
/** Your own blocks instead of the preset */
blocks?: AssemblyBlock[];
/** time: plays on its own (default). scroll: each block lands as you scroll. */
mode?: 'time' | 'scroll';
/** Address in the browser bar */
url?: string;
label?: string;
labels?: Partial<typeof TEXTS.en>;
className?: string;
}
const CW = 760;
const CH = 500;
const BROWSER = { x: 214, y: 20, w: 526, h: 460 };
/* ---------- drawn sections ---------- */
const bar = (w: string, tone = 'bg-muted', h = 'h-2') => <span className={cn('block rounded-full', h, tone)} style={{ width: w }} />;
function NavSketch() {
return (
<div className="flex h-full items-center justify-between rounded-md border border-border bg-card px-3">
<span className="flex items-center gap-1.5"><span className="size-4 rounded-[5px] bg-primary" />{bar('44px', 'bg-foreground/70')}</span>
<span className="flex gap-3">{bar('26px')}{bar('30px')}{bar('24px')}</span>
<span className="h-5 w-14 rounded-md bg-primary" />
</div>
);
}
function HeroSketch() {
return (
<div className="grid h-full grid-cols-[1fr_0.9fr] gap-3 rounded-md border border-border bg-card p-3">
<div className="flex flex-col justify-center gap-2">
{bar('40%', 'bg-[color-mix(in_oklab,var(--primary)_60%,transparent)]', 'h-1.5')}
{bar('92%', 'bg-foreground/80', 'h-3.5')}
{bar('70%', 'bg-foreground/80', 'h-3.5')}
{bar('85%', 'bg-muted', 'h-1.5')}
<span className="mt-1 flex gap-1.5"><span className="h-5 w-14 rounded-md bg-primary" /><span className="h-5 w-12 rounded-md border border-border-strong" /></span>
</div>
<div className="relative overflow-hidden rounded-md bg-[linear-gradient(150deg,color-mix(in_oklab,var(--primary)_40%,var(--surface-2)),color-mix(in_oklab,var(--primary)_8%,var(--surface)))]">
<span className="absolute bottom-0 left-0 right-0 h-1/2 bg-[color-mix(in_oklab,var(--foreground)_10%,transparent)] [clip-path:polygon(0_60%,30%_20%,55%_55%,80%_10%,100%_40%,100%_100%,0_100%)]" />
<span className="absolute right-4 top-3 size-5 rounded-full bg-[color-mix(in_oklab,var(--primary)_55%,#fff)]" />
</div>
</div>
);
}
function CardsSketch({ n = 3, price = false }: { n?: number; price?: boolean }) {
return (
<div className="grid h-full gap-2" style={{ gridTemplateColumns: `repeat(${n}, minmax(0, 1fr))` }}>
{Array.from({ length: n }, (_, i) => (
<div key={i} className={cn('flex flex-col gap-1.5 rounded-md border bg-card p-2.5', price && i === 1 ? 'border-[color-mix(in_oklab,var(--primary)_55%,var(--border))]' : 'border-border')}>
{price ? (
<>
{bar('40%', 'bg-muted', 'h-1.5')}
{bar('55%', 'bg-foreground/80', 'h-3')}
<span className="mt-auto h-4 w-full rounded-[5px]" style={{ background: i === 1 ? 'var(--primary)' : 'var(--muted)' }} />
</>
) : (
<>
<span className="size-4 rounded-[5px] bg-[color-mix(in_oklab,var(--primary)_22%,transparent)]" />
{bar('70%', 'bg-foreground/70', 'h-1.5')}
{bar('90%', 'bg-muted', 'h-1.5')}
{bar('60%', 'bg-muted', 'h-1.5')}
</>
)}
</div>
))}
</div>
);
}
function FooterSketch() {
return (
<div className="flex h-full items-center justify-between rounded-md border border-border bg-surface px-3">
<span className="flex items-center gap-1.5"><span className="size-3 rounded-[4px] bg-foreground/60" />{bar('36px')}</span>
<span className="flex gap-2.5">{bar('22px')}{bar('26px')}{bar('20px')}</span>
</div>
);
}
function SidebarSketch() {
return (
<div className="flex h-full flex-col gap-2 rounded-md border border-border bg-surface p-2.5">
<span className="mb-1 flex items-center gap-1.5"><span className="size-4 rounded-[5px] bg-primary" />{bar('40px', 'bg-foreground/70')}</span>
{[70, 55, 62, 48, 58].map((w, i) => (
<span key={i} className={cn('flex items-center gap-1.5 rounded-[5px] px-1.5 py-1', i === 0 && 'bg-accent')}>
<span className="size-2.5 rounded-[3px] bg-foreground/40" />
{bar(`${w}%`, i === 0 ? 'bg-foreground/70' : 'bg-muted', 'h-1.5')}
</span>
))}
</div>
);
}
function HeaderSketch() {
return (
<div className="flex h-full items-center justify-between rounded-md border border-border bg-card px-3">
{bar('80px', 'bg-foreground/80', 'h-2.5')}
<span className="flex items-center gap-2"><span className="h-5 w-24 rounded-md border border-border" /><span className="size-5 rounded-full bg-[color-mix(in_oklab,var(--primary)_40%,var(--muted))]" /></span>
</div>
);
}
function KpiSketch() {
return (
<div className="grid h-full grid-cols-4 gap-2">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="flex flex-col justify-center gap-1.5 rounded-md border border-border bg-card px-2.5">
{bar('50%', 'bg-muted', 'h-1.5')}
{bar('70%', 'bg-foreground/80', 'h-3')}
{bar('30%', 'bg-[color-mix(in_oklab,var(--success)_60%,transparent)]', 'h-1.5')}
</div>
))}
</div>
);
}
function ChartSketch() {
return (
<div className="flex h-full flex-col rounded-md border border-border bg-card p-2.5">
{bar('34%', 'bg-foreground/70', 'h-1.5')}
<svg viewBox="0 0 200 90" preserveAspectRatio="none" className="mt-2 w-full flex-1">
<path d="M0 76C30 70 40 58 70 56S110 40 130 34S170 18 200 10V90H0Z" fill="color-mix(in oklab, var(--primary) 18%, transparent)" />
<path d="M0 76C30 70 40 58 70 56S110 40 130 34S170 18 200 10" fill="none" stroke="var(--primary)" strokeWidth="2" vectorEffect="non-scaling-stroke" />
</svg>
</div>
);
}
function ActivitySketch() {
return (
<div className="flex h-full flex-col gap-2 rounded-md border border-border bg-card p-2.5">
{bar('46%', 'bg-foreground/70', 'h-1.5')}
{[0, 1, 2, 3].map((i) => (
<span key={i} className="flex items-center gap-1.5">
<span className="size-3.5 rounded-full" style={{ background: ['#c9a27e', '#7d97b5', '#9cae84', '#b48aa8'][i] }} />
{bar(`${[70, 55, 64, 48][i]}%`, 'bg-muted', 'h-1.5')}
</span>
))}
</div>
);
}
function TableSketch() {
return (
<div className="flex h-full flex-col rounded-md border border-border bg-card">
{[0, 1, 2, 3].map((i) => (
<span key={i} className={cn('flex flex-1 items-center gap-3 px-3', i > 0 && 'border-t border-border')}>
{bar('14%', i === 0 ? 'bg-muted-foreground/50' : 'bg-foreground/60', 'h-1.5')}
{bar('30%', 'bg-muted', 'h-1.5')}
<span className="ml-auto">{i > 0 ? <span className="block h-3 w-10 rounded-[4px] bg-[color-mix(in_oklab,var(--primary)_16%,transparent)]" /> : bar('34px', 'bg-muted-foreground/50', 'h-1.5')}</span>
</span>
))}
</div>
);
}
const glyph = (d: string) => (
<svg viewBox="0 0 16 16" className="size-3.5" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d={d} />
</svg>
);
function presetBlocks(preset: 'landing' | 'app', t: typeof TEXTS.en): AssemblyBlock[] {
const X = 224;
const W = 506;
if (preset === 'app')
return [
{ id: 'sidebar', label: t.sidebar, icon: glyph('M2.5 3h11v10h-11ZM6 3v10'), slot: { x: X, y: 62, w: 104, h: 408 }, content: <SidebarSketch /> },
{ id: 'header', label: t.header, icon: glyph('M2.5 3.5h11M2.5 7h7'), slot: { x: 336, y: 62, w: 394, h: 36 }, content: <HeaderSketch /> },
{ id: 'kpis', label: t.kpis, icon: glyph('M3 12V8M6.5 12V4M10 12V6.5M13.5 12V9'), slot: { x: 336, y: 106, w: 394, h: 66 }, content: <KpiSketch /> },
{ id: 'chart', label: t.chart, icon: glyph('M2.5 12 6 8l3 2.5 4.5-6'), slot: { x: 336, y: 180, w: 236, h: 150 }, content: <ChartSketch /> },
{ id: 'activity', label: t.activity, icon: glyph('M2.5 8h2.5l2-4 2.5 8 2-4h2'), slot: { x: 580, y: 180, w: 150, h: 150 }, content: <ActivitySketch /> },
{ id: 'table', label: t.table, icon: glyph('M2.5 3h11v10h-11ZM2.5 6.5h11M2.5 10h11'), slot: { x: 336, y: 338, w: 394, h: 132 }, content: <TableSketch /> },
];
return [
{ id: 'nav', label: t.nav, icon: glyph('M2.5 4h11M2.5 8h11M2.5 12h11'), slot: { x: X, y: 62, w: W, h: 34 }, content: <NavSketch /> },
{ id: 'hero', label: t.hero, icon: glyph('M2.5 3h11v10h-11ZM5 9.5l2.5-2.5L10 9.5l1.5-1.5'), slot: { x: X, y: 104, w: W, h: 132 }, content: <HeroSketch /> },
{ id: 'features', label: t.features, icon: glyph('M2.5 3h4.5v4.5H2.5ZM9 3h4.5v4.5H9ZM2.5 9h4.5v4.5H2.5ZM9 9h4.5v4.5H9Z'), slot: { x: X, y: 244, w: W, h: 96 }, content: <CardsSketch /> },
{ id: 'pricing', label: t.pricing, icon: glyph('M8 2.5v11M10.5 5H7a1.75 1.75 0 0 0 0 3.5h2a1.75 1.75 0 0 1 0 3.5H5.5'), slot: { x: X, y: 348, w: W, h: 78 }, content: <CardsSketch price /> },
{ id: 'footer', label: t.footer, icon: glyph('M2.5 12h11M2.5 9h6'), slot: { x: X, y: 434, w: W, h: 36 }, content: <FooterSketch /> },
];
}
/**
* <PageAssembly preset="landing" url="atlas-outdoor.com" />
* <PageAssembly preset="app" mode="scroll" />
*/
export function PageAssembly({ preset = 'landing', blocks, mode = 'time', url = 'your-site.com', speed = 1, autoPlay = 'in-view', loop = true, controls = true, label, labels, className }: PageAssemblyProps) {
const sid = useSceneId('kvpa');
const play = useScenePlay<HTMLDivElement>(autoPlay);
const progress = useScrollProgress<HTMLDivElement>({ start: 0.15, end: 0.7 });
const t = useTexts(TEXTS, labels);
const list = blocks ?? presetBlocks(preset, t);
const sp = Math.max(0.1, speed);
const step = 1.05;
const t0 = 0.7;
const endAt = t0 + list.length * step;
const T = round((endAt + 2.8) / sp, 3);
const chipY = (i: number) => 56 + i * 64;
const scroll = mode === 'scroll';
const n = list.length;
// scroll: block i lands while the progress runs through its window
const win = (i: number) => ({ a: round((i / (n + 1)) * 0.95, 4), w: round(1 / (n + 1), 4) });
const local = (i: number) => `clamp(0, (var(--kit-progress, 1) - ${win(i).a}) / ${win(i).w}, 1)`;
const chips = list.map((b, i) => {
const cx = 20 + 85;
const cy = chipY(i) + 26;
const dx = round(b.slot.x + b.slot.w / 2 - cx, 1);
const dy = round(b.slot.y + b.slot.h / 2 - cy, 1);
const at = t0 + i * step;
return { b, i, dx, dy, at };
});
const scene = (
<FitCanvas width={CW} height={CH}>
<div key={play.cycle} className="relative size-full" aria-hidden>
{/* palette */}
<div className="absolute left-[12px] top-[12px] h-[476px] w-[186px] rounded-2xl border border-border bg-surface shadow-[var(--highlight)]" />
<p className="absolute left-[24px] top-[26px] text-[11px] font-semibold uppercase tracking-[0.08em] text-subtle-foreground">{t.palette}</p>
{chips.map(({ b, i }) => (
<div key={`slot${b.id}`} className="absolute left-[20px] flex h-[52px] w-[170px] items-center gap-2.5 rounded-xl border border-dashed border-border-strong px-3 text-[12.5px] text-subtle-foreground" style={{ top: chipY(i) }}>
<span className="grid size-7 place-items-center rounded-lg bg-muted">{b.icon}</span>
<span className="flex-1 truncate">{b.label}</span>
<svg viewBox="0 0 16 16" className="size-3.5 text-success" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3.5 8.5 6.5 11.5 12.5 5" /></svg>
</div>
))}
{/* browser */}
<div className="absolute overflow-hidden rounded-xl border border-border bg-background shadow-[var(--highlight),var(--shadow-lg)]" style={{ left: BROWSER.x, top: BROWSER.y, width: BROWSER.w, height: BROWSER.h }}>
<div className="flex h-8 items-center gap-2 border-b border-border bg-surface px-3">
<span className="flex gap-1.5">{[0, 1, 2].map((k) => <span key={k} className="size-2 rounded-full bg-border-strong" />)}</span>
<span className="mx-auto rounded-md bg-muted px-3 py-0.5 text-[10.5px] text-muted-foreground">{url}</span>
<span className="w-8" />
</div>
</div>
{/* empty slots */}
{list.map((b) => (
<div key={`e${b.id}`} className="absolute rounded-md border border-dashed border-border" style={{ left: b.slot.x, top: b.slot.y, width: b.slot.w, height: b.slot.h }} />
))}
{/* landed sections */}
{chips.map(({ b, i, at }) =>
scroll ? (
<div key={`c${b.id}`} className="absolute" style={{ left: b.slot.x, top: b.slot.y, width: b.slot.w, height: b.slot.h, opacity: `calc(${local(i)} * 1.6 - 0.6)`, transform: `scale(calc(0.96 + ${local(i)} * 0.04))` }}>
{b.content}
</div>
) : (
<Track
key={`c${b.id}`}
className="absolute"
style={{ left: b.slot.x, top: b.slot.y, width: b.slot.w, height: b.slot.h }}
frames={[
[0, 'opacity:0;clip-path:inset(0 0 100% 0 round 6px);transform:scale(.97)'],
[at + 0.62, 'opacity:0;clip-path:inset(0 0 100% 0 round 6px);transform:scale(.97);animation-timing-function:cubic-bezier(.2,.7,.2,1)'],
[at + 0.95, 'opacity:1;clip-path:inset(0 0 0 0 round 6px);transform:scale(1)'],
[endAt + 2.2, 'opacity:1;clip-path:inset(0 0 0 0 round 6px);transform:scale(1)'],
[endAt + 2.6, 'opacity:0;clip-path:inset(0 0 0 0 round 6px);transform:scale(1)'],
]}
end="opacity:1;clip-path:inset(0 0 0 0 round 6px);transform:none"
>
{b.content}
</Track>
),
)}
{/* landing flash */}
{!scroll &&
chips.map(({ b, at }) => (
<Track
key={`f${b.id}`}
className="pointer-events-none absolute rounded-md border-2 border-[var(--primary)] shadow-[0_0_0_4px_color-mix(in_oklab,var(--primary)_18%,transparent)]"
style={{ left: b.slot.x - 2, top: b.slot.y - 2, width: b.slot.w + 4, height: b.slot.h + 4 }}
frames={[[0, 'opacity:0'], [at + 0.6, 'opacity:0'], [at + 0.72, 'opacity:1'], [at + 1.5, 'opacity:0']]}
end="opacity:0"
/>
))}
{/* chips that fly */}
{chips.map(({ b, i, dx, dy, at }) => {
const chip = (
<div className="flex h-[52px] w-[170px] items-center gap-2.5 rounded-xl border border-border bg-card px-3 text-[12.5px] font-medium text-foreground shadow-[var(--highlight),var(--shadow-md)]">
<span className="grid size-7 place-items-center rounded-lg bg-[color-mix(in_oklab,var(--primary)_14%,transparent)] text-primary">{b.icon}</span>
<span className="flex-1 truncate">{b.label}</span>
<span className="flex flex-col gap-[3px]" aria-hidden>{[0, 1].map((k) => <span key={k} className="flex gap-[3px]">{[0, 1].map((m) => <span key={m} className="size-[3px] rounded-full bg-subtle-foreground" />)}</span>)}</span>
</div>
);
if (scroll)
return (
<div
key={`m${b.id}`}
className="absolute left-[20px] z-20"
style={{
top: chipY(i),
transform: `translate(calc(${dx}px * ${local(i)}), calc(${dy}px * ${local(i)})) scale(calc(1 - ${local(i)} * 0.12))`,
opacity: `calc(1.25 - ${local(i)} * 1.25)`,
}}
>
{chip}
</div>
);
return (
<Track
key={`m${b.id}`}
className="absolute left-[20px] z-20"
style={{ top: chipY(i) }}
frames={[
[0, 'opacity:1;transform:translate(0,0) scale(1) rotate(0deg)'],
[at, 'opacity:1;transform:translate(0,0) scale(1) rotate(0deg);animation-timing-function:cubic-bezier(.3,.7,.3,1)'],
[at + 0.16, 'opacity:1;transform:translate(4px,-4px) scale(1.05) rotate(-2deg);animation-timing-function:cubic-bezier(.6,0,.25,1)'],
[at + 0.66, `opacity:1;transform:translate(${dx}px,${dy}px) scale(.92) rotate(0deg)`],
[at + 0.82, `opacity:0;transform:translate(${dx}px,${dy}px) scale(.86) rotate(0deg)`],
[endAt + 2.5, `opacity:0;transform:translate(${dx}px,${dy}px) scale(.86) rotate(0deg)`],
[endAt + 2.51, 'opacity:0;transform:translate(0,0) scale(1) rotate(0deg)'],
[endAt + 2.8, 'opacity:1;transform:translate(0,0) scale(1) rotate(0deg)'],
]}
end={`opacity:0;transform:translate(${dx}px,${dy}px) scale(.86)`}
>
{chip}
</Track>
);
})}
{/* published */}
{scroll ? (
<div className="absolute right-[34px] top-[30px] z-30" style={{ opacity: `clamp(0, (var(--kit-progress, 1) - 0.94) * 16, 1)` }}>
<Published text={t.published} />
</div>
) : (
<Track
className="absolute right-[34px] top-[30px] z-30"
frames={[[0, 'opacity:0;transform:translateY(-8px) scale(.9)'], [endAt + 0.2, 'opacity:0;transform:translateY(-8px) scale(.9);animation-timing-function:cubic-bezier(.3,1.4,.5,1)'], [endAt + 0.6, 'opacity:1;transform:none'], [endAt + 2.2, 'opacity:1;transform:none'], [endAt + 2.5, 'opacity:0;transform:none']]}
end="opacity:1;transform:none"
>
<Published text={t.published} />
</Track>
)}
</div>
</FitCanvas>
);
return (
<SceneTimeline sid={sid} T={T} speed={sp} loop={loop}>
<div className={cn('relative w-full', className)}>
<div
ref={(el) => {
(play.ref as React.MutableRefObject<HTMLDivElement | null>).current = el;
(progress as React.MutableRefObject<HTMLDivElement | null>).current = el;
}}
data-kv={scroll ? 'off' : play.state}
role={label ? 'img' : undefined}
aria-label={label}
className={cn(sid, 'relative w-full')}
>
<style dangerouslySetInnerHTML={{ __html: SCENE_CSS }} />
{scene}
</div>
{controls && !scroll && (
<div className="mt-2 flex justify-end">
<PlayToggle inline state={play.state} reduced={play.reduced} onToggle={play.toggle} />
</div>
)}
</div>
</SceneTimeline>
);
}
function Published({ text }: { text: string }) {
return (
<span className="inline-flex items-center gap-1.5 rounded-full bg-success px-2.5 py-1 text-[11.5px] font-semibold text-[oklch(0.99_0_none)] shadow-[var(--shadow-md)]">
<svg viewBox="0 0 16 16" className="size-3.5" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" aria-hidden><path d="M3.5 8.5 6.5 11.5 12.5 5" /></svg>
{text}
</span>
);
}Häufige Fragen
Wie installiere ich Seitenaufbau?
Mit aiw add @ai-website/page-assembly. Der Befehl legt die Datei components/ui/page-assembly.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind motion und motion-scene. Weitere npm-Pakete braucht sie nicht.
Welche Varianten hat Seitenaufbau?
3 Varianten, Landingpage, App-Screen und Mit dem Scrollen.
Kann ich die Texte von Seitenaufbau übersetzen?
Ja. Seitenaufbau bringt 13 eigene Texte auf Deutsch und Englisch mit, etwa „Components“, „Veröffentlicht“ und „Navbar“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.
Kann ein KI-Assistent Seitenaufbau einbauen?
Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Seitenaufbau mit aiw_components und installieren sie mit aiw_add @ai-website/page-assembly.