Zum Inhalt springen
UI Kit
Accent

Datenfluss

Knoten, verbunden durch Haarlinien, auf denen Lichtpakete laufen, mit einem Ring, wo sie ankommen. Hält außerhalb des Blickfelds an.

Eine von 3 Components in Daten-Animationen

3 Variants

Variants

Pipeline

Ein Ablauf von links nach rechts mit Verzweigung. Die Linien zeichnen sich beim Erscheinen, dann starten die Pakete.

Zentrale

Eine Mitte, die mit sechs Diensten spricht, manche in beide Richtungen. Gut für Anbindungen und Abgleich.

Sammeln und berichten

Vier Quellen fließen in einen Speicher, der Bericht und Hinweis speist. Die Reihenfolge der Pakete erzählt den Ablauf.

Details

Was Datenfluss mitbringt, aus Registry und Quellcode gelesen.

Bereich
Visuals
Variants
3
Version
1.0.0 · seit 1.0.0
Datei
components/ui/data-flow.tsx
Bringt mit
utilskitmotion-scene
npm-Pakete
Keine
Sprachen
Texte kommen über Props
Studio
Im Code anpassen

Props

Die Schnittstelle DataFlowProps aus components/ui/data-flow.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch. Nimmt außerdem alle Props von SceneMotionProps an.

  • nodesPflicht
    FlowNode[]
  • edgesPflicht
    FlowEdge[]
  • aspectoptional
    string

    Scene ratio, width / height. Default "16 / 9".

  • narrowAspectoptional
    string

    Ratio below 34rem width, where nodes need more height. Default "1 / 1".

  • labeloptional
    string

    Accessible description of what flows where. Without it the scene is decorative.

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, 208 Zeilen
components/ui/data-flow.tsx
'use client';

import { cn, round } from '@/lib/kit';
import { PlayToggle, SCENE_CSS, keyframes, useSceneId, useScenePlay, type SceneMotionProps } from '@/components/ui/motion-scene';

/*
 * Data that travels between nodes: hairline connections, light packets that
 * run along them and a soft ring where a packet arrives. One CSS timeline,
 * packets are a short dash moved with stroke-dashoffset. Server and reduced
 * motion show the calm map with small direction marks on every connection.
 */

export interface FlowNode {
  id: string;
  label: string;
  /** A second, quieter line under the label */
  meta?: string;
  /** An icon or mark in the tile */
  icon?: React.ReactNode;
  /** Position of the node center in percent of the scene */
  x: number;
  y: number;
  /** The main node: larger tile in the accent */
  emphasis?: boolean;
}

export interface FlowEdge {
  from: string;
  to: string;
  /** Packets run both ways */
  both?: boolean;
  /** Bend of the connection: horizontal leaves sideways, vertical leaves up or down, straight is a line */
  shape?: 'horizontal' | 'vertical' | 'straight';
}

export interface DataFlowProps extends SceneMotionProps {
  nodes: FlowNode[];
  edges: FlowEdge[];
  /** Scene ratio, width / height. Default "16 / 9". */
  aspect?: string;
  /** Ratio below 34rem width, where nodes need more height. Default "1 / 1". */
  narrowAspect?: string;
  /** Accessible description of what flows where. Without it the scene is decorative. */
  label?: string;
  className?: string;
}

const VW = 1000;

function geometry(a: FlowNode, b: FlowNode, shape: FlowEdge['shape'], vh: number) {
  const x1 = (a.x / 100) * VW;
  const y1 = (a.y / 100) * vh;
  const x2 = (b.x / 100) * VW;
  const y2 = (b.y / 100) * vh;
  const s = shape ?? (Math.abs(x2 - x1) >= Math.abs(y2 - y1) ? 'horizontal' : 'vertical');
  let c1: [number, number];
  let c2: [number, number];
  if (s === 'straight') {
    c1 = [x1 + (x2 - x1) / 3, y1 + (y2 - y1) / 3];
    c2 = [x1 + ((x2 - x1) * 2) / 3, y1 + ((y2 - y1) * 2) / 3];
  } else if (s === 'horizontal') {
    const k = (x2 - x1) * 0.55;
    c1 = [x1 + k, y1];
    c2 = [x2 - k, y2];
  } else {
    const k = (y2 - y1) * 0.55;
    c1 = [x1, y1 + k];
    c2 = [x2, y2 - k];
  }
  const d = `M${round(x1)} ${round(y1)} C${round(c1[0])} ${round(c1[1])} ${round(c2[0])} ${round(c2[1])} ${round(x2)} ${round(y2)}`;
  // point and tangent at t = 0.5 for the direction mark
  const t = 0.5;
  const mt = 1 - t;
  const px = mt ** 3 * x1 + 3 * mt * mt * t * c1[0] + 3 * mt * t * t * c2[0] + t ** 3 * x2;
  const py = mt ** 3 * y1 + 3 * mt * mt * t * c1[1] + 3 * mt * t * t * c2[1] + t ** 3 * y2;
  const dx = 3 * mt * mt * (c1[0] - x1) + 6 * mt * t * (c2[0] - c1[0]) + 3 * t * t * (x2 - c2[0]);
  const dy = 3 * mt * mt * (c1[1] - y1) + 6 * mt * t * (c2[1] - c1[1]) + 3 * t * t * (y2 - c2[1]);
  const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
  const dr = `M${round(x2)} ${round(y2)} C${round(c2[0])} ${round(c2[1])} ${round(c1[0])} ${round(c1[1])} ${round(x1)} ${round(y1)}`;
  return { d, dr, mx: round(px), my: round(py), angle: round(angle, 1) };
}

/**
 * Nodes connected by lines with packets of light running along them.
 *   <DataFlow nodes={[{ id: 'form', label: 'Form', x: 12, y: 50 }, …]} edges={[{ from: 'form', to: 'crm' }]} />
 */
export function DataFlow({ nodes, edges, aspect = '16 / 9', narrowAspect = '1 / 1', speed = 1, autoPlay = 'in-view', loop = true, controls = true, label, className }: DataFlowProps) {
  const sid = useSceneId('kvdf');
  const play = useScenePlay<HTMLDivElement>(autoPlay);
  const [aw, ah] = aspect.split('/').map((n) => Number(n.trim()) || 1);
  const VH = round((VW * ah) / aw);
  const byId = new Map(nodes.map((n) => [n.id, n]));
  const n = Math.max(1, edges.length);
  // One cycle lasts base seconds at speed 1, a packet travels 1.25 s of it
  const base = Math.max(3.4, n * 0.62);
  const T = round(base / Math.max(0.1, speed), 2);
  const f = round((1.25 / base) * 100, 2);
  const lanes = edges
    .map((e, i) => {
      const a = byId.get(e.from);
      const b = byId.get(e.to);
      if (!a || !b) return null;
      return { e, i, a, b, g: geometry(a, b, e.shape, VH), delay: round((i * T) / n, 2) };
    })
    .filter(Boolean) as { e: FlowEdge; i: number; a: FlowNode; b: FlowNode; g: ReturnType<typeof geometry>; delay: number }[];

  const ease = 'animation-timing-function:cubic-bezier(.5,0,.3,1)';
  const css = `${SCENE_CSS}
${keyframes(`${sid}-p`, [[0, `stroke-dashoffset:6;opacity:0;${ease}`], [1.5, 'opacity:1'], [f, 'stroke-dashoffset:-100;opacity:1'], [f + 1, 'opacity:0'], [100, 'stroke-dashoffset:-100;opacity:0']])}
${keyframes(`${sid}-t`, [[0, `stroke-dashoffset:22;opacity:0;${ease}`], [3, 'opacity:.5'], [f + 2, 'stroke-dashoffset:-100;opacity:.35'], [f + 4, 'opacity:0'], [100, 'stroke-dashoffset:-100;opacity:0']])}
${keyframes(`${sid}-r`, [[0, 'opacity:0;transform:scale(.92)'], [f - 1, 'opacity:0;transform:scale(.92)'], [f + 2, 'opacity:.7;transform:scale(1)'], [Math.min(99, f + 26), 'opacity:0;transform:scale(1.32)'], [100, 'opacity:0;transform:scale(1.32)']])}
${keyframes(`${sid}-in`, [[0, 'opacity:0;transform:translate(-50%,-50%) scale(.94)'], [100, 'opacity:1;transform:translate(-50%,-50%) scale(1)']])}
${keyframes(`${sid}-line`, [[0, 'stroke-dashoffset:1'], [100, 'stroke-dashoffset:0']])}
.${sid}:not([data-kv="off"]) .${sid}-p { animation: ${sid}-p ${T}s linear var(--d) ${loop ? 'infinite' : '1'} both; }
.${sid}:not([data-kv="off"]) .${sid}-t { animation: ${sid}-t ${T}s linear var(--d) ${loop ? 'infinite' : '1'} both; }
.${sid}:not([data-kv="off"]) .${sid}-r { animation: ${sid}-r ${T}s linear var(--d) ${loop ? 'infinite' : '1'} both; }
.${sid}:not([data-kv="off"]) .${sid}-n { animation: ${sid}-in .7s var(--ease-kit) var(--i) 1 both; }
.${sid}:not([data-kv="off"]) .${sid}-l { stroke-dasharray: 1 1; animation: ${sid}-line 1.1s var(--ease-kit) var(--i) 1 both; }`;

  return (
    <div className={cn('@container relative w-full', className)}>
    <div
      ref={play.ref}
      data-kv={play.state}
      role={label ? 'img' : undefined}
      aria-label={label}
      aria-hidden={label ? undefined : true}
      className={cn(sid, 'relative w-full select-none [aspect-ratio:var(--df-ar)] @max-[34rem]:[aspect-ratio:var(--df-ar-sm)]')}
      style={{ '--df-ar': aspect, '--df-ar-sm': narrowAspect } as React.CSSProperties}
    >
      <style dangerouslySetInnerHTML={{ __html: css }} />
      <svg key={play.cycle} viewBox={`0 0 ${VW} ${VH}`} preserveAspectRatio="none" className="absolute inset-0 size-full overflow-visible" aria-hidden>
        {lanes.map(({ g, i }) => (
          <g key={`l${i}`}>
            <path className={`${sid}-l`} pathLength={1} d={g.d} fill="none" stroke="color-mix(in oklab, var(--foreground) 17%, transparent)" strokeWidth="1.25" vectorEffect="non-scaling-stroke" style={{ '--i': `${round(0.15 + i * 0.08, 2)}s` } as React.CSSProperties} />
            <path
              className="@max-[34rem]:hidden"
              d="M-4 -4.5 L2 0 L-4 4.5"
              transform={`translate(${g.mx} ${g.my}) rotate(${g.angle})`}
              fill="none"
              stroke="color-mix(in oklab, var(--foreground) 30%, transparent)"
              strokeWidth="1.4"
              strokeLinecap="round"
              strokeLinejoin="round"
              vectorEffect="non-scaling-stroke"
            />
          </g>
        ))}
        {lanes.flatMap(({ g, e, i, delay }) =>
          (e.both ? [g.d, g.dr] : [g.d]).map((d, k) => {
            const style = { '--d': `${round(delay + k * (T / 2), 2)}s` } as React.CSSProperties;
            return (
              <g key={`p${i}-${k}`} style={style}>
                <path className={`${sid}-t`} pathLength={100} d={d} fill="none" stroke="var(--primary)" strokeWidth="2" strokeLinecap="round" strokeDasharray="16 200" strokeDashoffset="22" opacity="0" vectorEffect="non-scaling-stroke" />
                <path className={`${sid}-p`} pathLength={100} d={d} fill="none" stroke="var(--primary)" strokeWidth="9" strokeLinecap="round" strokeDasharray="2.2 200" strokeDashoffset="6" opacity="0" strokeOpacity="0.28" vectorEffect="non-scaling-stroke" />
                <path className={`${sid}-p`} pathLength={100} d={d} fill="none" stroke="color-mix(in oklab, var(--primary) 70%, #fff)" strokeWidth="3.2" strokeLinecap="round" strokeDasharray="2.2 200" strokeDashoffset="6" opacity="0" vectorEffect="non-scaling-stroke" />
              </g>
            );
          }),
        )}
      </svg>
      {nodes.map((node, k) => {
        const incoming = lanes.filter((l) => l.b.id === node.id || (l.e.both && l.a.id === node.id));
        return (
          <div
            key={node.id}
            className={`${sid}-n absolute z-10`}
            style={{ left: `${node.x}%`, top: `${node.y}%`, transform: 'translate(-50%,-50%)', '--i': `${round(k * 0.07, 2)}s` } as React.CSSProperties}
          >
            <div
              className={cn(
                'relative flex items-center gap-2.5 rounded-xl border bg-card text-left shadow-[var(--highlight),var(--shadow-md)] @max-[34rem]:flex-col @max-[34rem]:gap-1 @max-[34rem]:border-0 @max-[34rem]:bg-transparent @max-[34rem]:p-0 @max-[34rem]:shadow-none',
                node.emphasis ? 'border-[color-mix(in_oklab,var(--primary)_45%,var(--border))] p-2.5 pr-4 shadow-[var(--highlight),0_0_0_4px_color-mix(in_oklab,var(--primary)_10%,transparent),var(--shadow-lg)]' : 'border-border p-2 pr-3.5',
              )}
            >
              {incoming.map((l) => (
                <span
                  key={`${l.i}`}
                  aria-hidden
                  className={`${sid}-r pointer-events-none absolute -inset-1 rounded-[14px] border-2 border-[color-mix(in_oklab,var(--primary)_70%,transparent)] opacity-0 @max-[34rem]:inset-auto @max-[34rem]:left-1/2 @max-[34rem]:top-0 @max-[34rem]:size-10 @max-[34rem]:-translate-x-1/2`}
                  style={{ '--d': `${round(l.delay + (l.e.both && l.a.id === node.id ? T / 2 : 0), 2)}s` } as React.CSSProperties}
                />
              ))}
              <span
                className={cn(
                  'grid shrink-0 place-items-center rounded-lg [&_svg]:size-[55%] @max-[34rem]:shadow-[var(--highlight),var(--shadow-md)]',
                  node.emphasis ? 'size-10 bg-primary text-primary-foreground @max-[34rem]:size-11' : 'size-8 border border-border bg-surface text-foreground @max-[34rem]:size-10 @max-[34rem]:bg-card',
                )}
              >
                {node.icon ?? <span className="size-2 rounded-full bg-current" />}
              </span>
              <span className="min-w-0 @max-[34rem]:text-center">
                <span className={cn('block whitespace-nowrap font-semibold tracking-[-0.01em] text-foreground @max-[34rem]:max-w-[5.5rem] @max-[34rem]:whitespace-normal @max-[34rem]:leading-tight', node.emphasis ? 'text-[14px]' : 'text-[12.5px] @max-[34rem]:text-[10.5px] @max-[34rem]:font-medium')}>{node.label}</span>
                {node.meta && <span className="block whitespace-nowrap text-[11px] text-muted-foreground @max-[44rem]:hidden">{node.meta}</span>}
              </span>
            </div>
          </div>
        );
      })}
    </div>
    {controls && (
      <div className="mt-2 flex justify-end">
        <PlayToggle inline state={play.state} reduced={play.reduced} onToggle={play.toggle} />
      </div>
    )}
    </div>
  );
}

Häufige Fragen

Wie installiere ich Datenfluss?

Mit aiw add @ai-website/data-flow. Der Befehl legt die Datei components/ui/data-flow.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind motion-scene. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Datenfluss?

3 Varianten, Pipeline, Zentrale und Sammeln und berichten.

Kann ein KI-Assistent Datenfluss einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Datenfluss mit aiw_components und installieren sie mit aiw_add @ai-website/data-flow.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert