Zum Inhalt springen
UI Kit
Accent

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

3 Variants

Variants

Landingpage

Fünf Sections in Lesereihenfolge. Jede hebt ab, fliegt, landet mit einem Aufleuchten und erscheint von oben.

App-Screen

Sechs Widgets bauen ein Dashboard, die Sidebar zuerst, die Tabelle zuletzt.

Mit dem Scrollen

Jede Component landet in ihrem eigenen Stück des Scrollwegs und fliegt zurück, wenn du hochscrollst.

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

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

  • blocksoptional
    AssemblyBlock[]

    Your own blocks instead of the preset

  • modeoptional
    'time' | 'scroll'

    time: plays on its own (default). scroll: each block lands as you scroll.

  • urloptional
    string

    Address in the browser bar

  • labeloptional
    string

Eingebaute Texte

Diese Texte schreibt Seitenaufbau selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 13 Texte anzeigen
paletteEnglisch ComponentsDeutsch Components
publishedEnglisch PublishedDeutsch Veröffentlicht
navEnglisch NavbarDeutsch Navbar
heroEnglisch HeroDeutsch Hero
featuresEnglisch FeaturesDeutsch Features
pricingEnglisch PricingDeutsch Pricing
footerEnglisch FooterDeutsch Footer
sidebarEnglisch SidebarDeutsch Sidebar
headerEnglisch HeaderDeutsch Header
kpisEnglisch StatsDeutsch Stats
chartEnglisch ChartDeutsch Chart
activityEnglisch ActivityDeutsch Aktivität
tableEnglisch TableDeutsch 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
components/ui/page-assembly.tsx
'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.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert