Diagrams
Dependency-free, token-themed relationship diagrams — flow, network, org chart, indented tree, mind map and fishbone.
Flow Diagram — A dependency-free static SVG flow diagram: position box, pill and diamond nodes on a column/row grid and connect them with labelled arrowed edges.
Preview
A dependency-free static SVG flow chart. Pass `:nodes` (each `id`, `label`, 0-based `col`, `row`, optional `shape`, `tone`, `description`) and `:edges` (`from`, `to`, optional `label`, `style`, `arrow`, `tone`); arrows leave the right port for a forward edge, elbow into another lane, and loop back through the corridor for a back edge.
<div class="space-y-4">
<x-ui.flow-diagram :height="320" label="{{ __('Approval workflow') }}" />
<p class="text-sm text-muted-foreground">
{{ __('A dependency-free static SVG flow chart. Pass `:nodes` (each `id`, `label`, 0-based `col`, `row`, optional `shape`, `tone`, `description`) and `:edges` (`from`, `to`, optional `label`, `style`, `arrow`, `tone`); arrows leave the right port for a forward edge, elbow into another lane, and loop back through the corridor for a back edge.') }}
</p>
</div>
Installation
php artisan ui:add flow-diagram
Registry contract
php artisan ui:add flow-diagram
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/flow-diagram.blade.php
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Flow Diagram (`flow-diagram`)
A dependency-free static SVG flow diagram: position box, pill and diamond nodes on a column/row grid and connect them with labelled arrowed edges.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add flow-diagram
```
## Usage
```blade
<div class="space-y-4">
<x-ui.flow-diagram :height="320" label="{{ __('Approval workflow') }}" />
<p class="text-sm text-muted-foreground">
{{ __('A dependency-free static SVG flow chart. Pass `:nodes` (each `id`, `label`, 0-based `col`, `row`, optional `shape`, `tone`, `description`) and `:edges` (`from`, `to`, optional `label`, `style`, `arrow`, `tone`); arrows leave the right port for a forward edge, elbow into another lane, and loop back through the corridor for a back edge.') }}
</p>
</div>
```
## Props
- `nodes` (mixed|null, default `null`) — Steps to place: each an object with id, label, col (0-based column), row, and optional shape (box, pill, diamond, circle, parallelogram, hexagon, cylinder, document, subroutine, note), tone, description and dashed.
- `edges` (mixed|null, default `null`) — Connections between nodes: each an object with from, to, and optional label, style (solid, dashed, dotted), arrow (end, both, none) and tone.
- `groups` (array, default `[]`) — Swimlane or subprocess frames drawn around a set of nodes: each an object with label, a list of node ids, and an optional tone.
- `direction` (string, default `lr`) — lr lays columns out left to right; tb lays them top to bottom.
- `height` (int, default `320`) — Minimum drawing height in pixels; the frame grows to fit however many rows it needs.
- `label` (string, default `Flow diagram`) — Accessible name for the figure.
## Use when
- Use when hierarchy, causality, network structure, or process flow is the information itself.
- Documenting a process, decision flow or approval chain with distinct shapes for steps, decisions and terminators.
- Needing swimlane-style groups around related nodes, or labelled arrowed edges between steps.
## Avoid when
- Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
- The structure is a strict hierarchy of one parent to many children; use org-chart instead.
- The nodes have no meaningful order or direction; use network-graph or mind-map instead.
## Anti-patterns
- Using a specialized diagram when linear content is clearer
## Rules
- Use the `<brok:flow-diagram>` tag (or `<x-ui.flow-diagram>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/flow-diagram
- Registry JSON (files, props, contract): https://brokui.dev/r/open/flow-diagram.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- The default request lifecycle framed by chart-card with a step-count badge. --}}
<x-ui.chart-card heading="Approval workflow" subheading="Request lifecycle" trend="5 steps" trend-variant="neutral">
<x-ui.flow-diagram :height="280" label="{{ __('Approval workflow') }}" />
</x-ui.chart-card>
<x-ui.flow-diagram
:height="360"
:nodes="[
['id' => 'in', 'label' => __('Order form'), 'description' => __('input'), 'col' => 0, 'row' => 0, 'shape' => 'parallelogram'],
['id' => 'prep', 'label' => __('Prepare'), 'description' => __('preparation'), 'col' => 1, 'row' => 0, 'shape' => 'hexagon'],
['id' => 'sub', 'label' => __('Charge card'), 'description' => __('subroutine'), 'col' => 2, 'row' => 0, 'shape' => 'subroutine', 'tone' => 'info'],
['id' => 'db', 'label' => __('Orders DB'), 'description' => __('database'), 'col' => 2, 'row' => 1, 'shape' => 'cylinder', 'tone' => 'muted'],
['id' => 'doc', 'label' => __('Invoice'), 'description' => __('document'), 'col' => 3, 'row' => 0, 'shape' => 'document'],
['id' => 'note', 'label' => __('Audit note'), 'description' => __('note'), 'col' => 3, 'row' => 1, 'shape' => 'note', 'tone' => 'warning', 'dashed' => true],
['id' => 'end', 'label' => __('End'), 'description' => __('connector'), 'col' => 4, 'row' => 0, 'shape' => 'circle', 'tone' => 'primary'],
]"
:edges="[
['from' => 'in', 'to' => 'prep'],
['from' => 'prep', 'to' => 'sub'],
['from' => 'sub', 'to' => 'db', 'arrow' => 'both', 'style' => 'dotted', 'tone' => 'muted'],
['from' => 'sub', 'to' => 'doc', 'tone' => 'success'],
['from' => 'sub', 'to' => 'note', 'style' => 'dashed', 'arrow' => 'none', 'tone' => 'warning'],
['from' => 'doc', 'to' => 'end'],
]"
label="{{ __('Every node shape and edge style') }}"
/>
<x-ui.flow-diagram
direction="tb"
:nodes="[
['id' => 'req', 'label' => __('Request'), 'col' => 0, 'row' => 0, 'shape' => 'pill', 'tone' => 'primary'],
['id' => 'auth', 'label' => __('Authenticated?'), 'col' => 1, 'row' => 0, 'shape' => 'diamond'],
['id' => 'login', 'label' => __('Sign in'), 'col' => 1, 'row' => 1, 'shape' => 'box', 'tone' => 'muted'],
['id' => 'handle', 'label' => __('Handle'), 'col' => 2, 'row' => 0, 'shape' => 'box'],
['id' => 'resp', 'label' => __('Response'), 'col' => 3, 'row' => 0, 'shape' => 'pill', 'tone' => 'success'],
]"
:edges="[
['from' => 'req', 'to' => 'auth'],
['from' => 'auth', 'to' => 'handle', 'label' => __('Yes')],
['from' => 'auth', 'to' => 'login', 'label' => __('No')],
['from' => 'login', 'to' => 'auth', 'style' => 'dashed'],
['from' => 'handle', 'to' => 'resp'],
]"
:groups="[['label' => __('Middleware'), 'nodes' => ['auth', 'login']]]"
label="{{ __('Request lifecycle, top to bottom') }}"
/>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| nodes | mixed | null | null | Steps to place: each an object with id, label, col (0-based column), row, and optional shape (box, pill, diamond, circle, parallelogram, hexagon, cylinder, document, subroutine, note), tone, description and dashed. |
| edges | mixed | null | null | Connections between nodes: each an object with from, to, and optional label, style (solid, dashed, dotted), arrow (end, both, none) and tone. |
| groups | array | [] | Swimlane or subprocess frames drawn around a set of nodes: each an object with label, a list of node ids, and an optional tone. |
| direction | string | lr | lr lays columns out left to right; tb lays them top to bottom. |
| height | int | 320 | Minimum drawing height in pixels; the frame grows to fit however many rows it needs. |
| label | string | Flow diagram | Accessible name for the figure. |
Slots
default— Unused; the diagram renders from the nodes, edges and groups props only.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Renders without arguments using built-in demo data (a small approval process), so it previews immediately.
- Places nodes on a column/row grid and draws arrowed edges between them, supporting solid, dashed or dotted styles and end/both/no-arrow terminators.
- Dependency-free SVG: no chart library, computed and laid out entirely in Blade.
- Declares registry capability flags: a11y, responsive, rtl, darkMode, localized.
Guidance
Explain hierarchy, causality, network structure, or flow.
Use when
- Use when hierarchy, causality, network structure, or process flow is the information itself.
- Documenting a process, decision flow or approval chain with distinct shapes for steps, decisions and terminators.
- Needing swimlane-style groups around related nodes, or labelled arrowed edges between steps.
Avoid when
- Do not use specialized diagrams when a list, table, or short paragraph would be easier to interpret.
- The structure is a strict hierarchy of one parent to many children; use org-chart instead.
- The nodes have no meaningful order or direction; use network-graph or mind-map instead.
Use instead
- List, table, or prose explanation
Anti-patterns
- Using a specialized diagram when linear content is clearer
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- No component-owned keyboard interaction; native element behavior applies.
- Focus
none
- The figure carries role="img" with an aria-label from label, so a screen reader hears a named summary instead of parsing raw SVG.
- Every node and edge label renders as visible SVG text, never as a hover-only tooltip.
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Livewire can update this component through forwarded wire:* attributes.
Source
The exact, editable file ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
// Nodes: each ['id', 'label', 'col' (0-based), 'row', optional 'shape', 'tone',
// 'description', 'dashed'].
// shape: box | pill | diamond | circle | parallelogram | hexagon | cylinder |
// document | subroutine | note
// tone: default | primary | info | success | warning | destructive | muted
'nodes' => null,
// Edges: each ['from', 'to', optional 'label', 'style' (solid|dashed|dotted),
// 'arrow' (end|both|none), 'tone'].
'edges' => null,
// Groups (swimlanes / subprocess frames): each ['label', 'nodes' => [ids], optional 'tone'].
'groups' => [],
// lr lays columns out left → right; tb lays them top → bottom.
'direction' => 'lr',
// Minimum drawing height; the frame grows with the rows it has to fit.
'height' => 320,
'label' => 'Flow diagram',
])
@php
// ── Demo data (a small approval process) so it renders without args ──
$nodes ??= [
['id' => 'start', 'label' => __('Start'), 'col' => 0, 'row' => 0, 'shape' => 'pill', 'tone' => 'primary'],
['id' => 'validate', 'label' => __('Validate'), 'col' => 1, 'row' => 0, 'shape' => 'box'],
['id' => 'decision', 'label' => __('Approved?'), 'col' => 2, 'row' => 0, 'shape' => 'diamond'],
['id' => 'approve', 'label' => __('Approve'), 'col' => 3, 'row' => 0, 'shape' => 'box', 'tone' => 'success'],
['id' => 'reject', 'label' => __('Reject'), 'col' => 3, 'row' => 1, 'shape' => 'box', 'tone' => 'destructive'],
['id' => 'done', 'label' => __('Done'), 'col' => 4, 'row' => 0, 'shape' => 'pill'],
];
$edges ??= [
['from' => 'start', 'to' => 'validate'],
['from' => 'validate', 'to' => 'decision'],
['from' => 'decision', 'to' => 'approve', 'label' => __('Yes')],
['from' => 'decision', 'to' => 'reject', 'label' => __('No')],
['from' => 'approve', 'to' => 'done'],
['from' => 'reject', 'to' => 'validate', 'label' => __('Retry'), 'style' => 'dashed'],
];
$tb = $direction === 'tb';
// ── Grid ───────────────────────────────────────────────────────────────
$nodeW = 120;
$nodeH = 48;
$withDescription = collect($nodes)->contains(fn ($n) => ! empty($n['description']));
if ($withDescription) {
$nodeH = 60;
}
$pad = 16;
$maxCol = $maxRow = 0;
foreach ($nodes as $n) {
$maxCol = max($maxCol, (int) $n['col']);
$maxRow = max($maxRow, (int) $n['row']);
}
// Pitch between grid cells along the flow (major) and across it (minor).
// A back edge is routed in the lane between rows, so the minor pitch
// leaves room for one.
$majorPitch = ($tb ? $nodeH : $nodeW) + 64;
$minorPitch = ($tb ? $nodeW : $nodeH) + 56;
$majorSpan = $maxCol * $majorPitch + ($tb ? $nodeH : $nodeW);
$minorSpan = $maxRow * $minorPitch + ($tb ? $nodeW : $nodeH);
$groupPad = $groups ? 24 : 0;
$W = $tb ? max(320, $minorSpan + 2 * $pad + 2 * $groupPad) : max(760, $majorSpan + 2 * $pad + 2 * $groupPad);
$H = $tb ? max((int) $height, $majorSpan + 2 * $pad + 2 * $groupPad) : max((int) $height, $minorSpan + 2 * $pad + 2 * $groupPad);
// Centre the grid in the frame.
$offMajor = (($tb ? $H : $W) - $majorSpan) / 2;
$offMinor = (($tb ? $W : $H) - $minorSpan) / 2;
$geom = [];
foreach ($nodes as $n) {
$u = $offMajor + (int) $n['col'] * $majorPitch + ($tb ? $nodeH : $nodeW) / 2; // centre along the flow
$v = $offMinor + (int) $n['row'] * $minorPitch + ($tb ? $nodeW : $nodeH) / 2; // centre across it
$cx = $tb ? $v : $u;
$cy = $tb ? $u : $v;
$geom[$n['id']] = [
'cx' => $cx,
'cy' => $cy,
'x' => $cx - $nodeW / 2,
'y' => $cy - $nodeH / 2,
'col' => (int) $n['col'],
'row' => (int) $n['row'],
'shape' => $n['shape'] ?? 'box',
'tone' => $n['tone'] ?? 'default',
'label' => $n['label'],
'description' => $n['description'] ?? null,
'dashed' => ! empty($n['dashed']),
];
}
// ── Ports + routing ────────────────────────────────────────────────────
// Work in flow coordinates (u along the flow, v across it) and map back to
// x/y at the end, so the same routing serves both directions.
$toXY = fn (float $u, float $v) => $tb ? [$v, $u] : [$u, $v];
$halfU = ($tb ? $nodeH : $nodeW) / 2;
$halfV = ($tb ? $nodeW : $nodeH) / 2;
$uOf = fn ($g) => $tb ? $g['cy'] : $g['cx'];
$vOf = fn ($g) => $tb ? $g['cx'] : $g['cy'];
// Turn a polyline into a path with rounded corners.
$rounded = function (array $pts, float $r = 8): string {
$n = count($pts);
if ($n < 2) {
return '';
}
$d = sprintf('M%.2f %.2f', $pts[0][0], $pts[0][1]);
for ($i = 1; $i < $n - 1; $i++) {
[$px, $py] = $pts[$i - 1];
[$x, $y] = $pts[$i];
[$nx, $ny] = $pts[$i + 1];
$l1 = hypot($x - $px, $y - $py);
$l2 = hypot($nx - $x, $ny - $y);
$rr = min($r, $l1 / 2, $l2 / 2);
$ax = $x - ($x - $px) / ($l1 ?: 1) * $rr;
$ay = $y - ($y - $py) / ($l1 ?: 1) * $rr;
$bx = $x + ($nx - $x) / ($l2 ?: 1) * $rr;
$by = $y + ($ny - $y) / ($l2 ?: 1) * $rr;
$d .= sprintf(' L%.2f %.2f Q%.2f %.2f %.2f %.2f', $ax, $ay, $x, $y, $bx, $by);
}
$d .= sprintf(' L%.2f %.2f', $pts[$n - 1][0], $pts[$n - 1][1]);
return $d;
};
// A pair joined in both directions gets each edge nudged to its own side
// so the two arrows never sit on one line.
$pairs = [];
foreach ($edges as $e) {
$pairs[$e['from'].'→'.$e['to']] = true;
}
$paths = [];
foreach ($edges as $e) {
if (! isset($geom[$e['from']], $geom[$e['to']])) {
continue;
}
$s = $geom[$e['from']];
$t = $geom[$e['to']];
$su = $uOf($s); $sv = $vOf($s);
$tu = $uOf($t); $tv = $vOf($t);
$dc = $t['col'] - $s['col'];
$dr = $t['row'] - $s['row'];
$twoWay = isset($pairs[$e['to'].'→'.$e['from']]);
$nudge = $twoWay ? (($dc > 0 || ($dc === 0 && $dr > 0)) ? -8 : 8) : 0;
if ($dc > 0 && $dr === 0) {
// Forward, same lane: a straight run from the out port to the in port.
$pts = [[$su + $halfU, $sv + $nudge], [$tu - $halfU, $tv + $nudge]];
} elseif ($dc === 0 && $dr !== 0) {
// Same column: straight across the lanes.
$dir = $dr > 0 ? 1 : -1;
$pts = [[$su + $nudge, $sv + $dir * $halfV], [$tu + $nudge, $tv - $dir * $halfV]];
} elseif ($dc > 0) {
// Forward into another lane: out port → elbow midway → in port.
$mid = ($su + $halfU + $tu - $halfU) / 2;
$pts = [[$su + $halfU, $sv], [$mid, $sv], [$mid, $tv], [$tu - $halfU, $tv]];
} else {
// Back edge (loop): leave across the lane, run back along the
// corridor beside the source's lane, and enter the target across
// its own lane so it never crosses the forward path.
$dir = $sv >= $tv ? 1 : -1;
$corridor = $sv + $dir * ($halfV + 20);
$pts = [[$su, $sv + $dir * $halfV], [$su, $corridor], [$tu, $corridor], [$tu, $tv + $dir * $halfV]];
if ($dr === 0) {
// Same lane: enter the target from the corridor side as well.
$pts = [[$su, $sv + $halfV], [$su, $sv + $halfV + 20], [$tu, $sv + $halfV + 20], [$tu, $tv + $halfV]];
}
}
$xy = array_map(fn ($p) => $toXY($p[0], $p[1]), $pts);
// Label on the midpoint of the longest segment; a straight two-point
// edge carries it just beside the line so the chip never hides the
// arrowhead of a short run.
$best = 0; $lx = $ly = 0; $horizontal = false;
for ($i = 1; $i < count($xy); $i++) {
$len = hypot($xy[$i][0] - $xy[$i - 1][0], $xy[$i][1] - $xy[$i - 1][1]);
if ($len >= $best) {
$best = $len;
$lx = ($xy[$i][0] + $xy[$i - 1][0]) / 2;
$ly = ($xy[$i][1] + $xy[$i - 1][1]) / 2;
$horizontal = abs($xy[$i][1] - $xy[$i - 1][1]) < 0.5;
}
}
if (count($xy) === 2) {
if ($horizontal) {
$ly -= 14;
} else {
$lx += (mb_strlen((string) ($e['label'] ?? '')) * 6.4 + 14) / 2 + 6;
}
}
$style = $e['style'] ?? 'solid';
$paths[] = [
'd' => $rounded($xy),
'label' => $e['label'] ?? null,
'lx' => $lx,
'ly' => $ly,
'lw' => mb_strlen((string) ($e['label'] ?? '')) * 6.4 + 14,
'dash' => $style === 'dashed' ? '6 4' : ($style === 'dotted' ? '1.5 4' : null),
'arrow' => $e['arrow'] ?? 'end',
'tone' => $e['tone'] ?? 'default',
];
}
// ── Groups: a dashed frame around the bounding box of its members ──────
$frames = [];
foreach ((array) $groups as $g) {
$members = array_values(array_filter((array) ($g['nodes'] ?? []), fn ($id) => isset($geom[$id])));
if (! $members) {
continue;
}
$x0 = min(array_map(fn ($id) => $geom[$id]['x'], $members)) - 16;
$y0 = min(array_map(fn ($id) => $geom[$id]['y'], $members)) - 28;
$x1 = max(array_map(fn ($id) => $geom[$id]['x'] + $nodeW, $members)) + 16;
$y1 = max(array_map(fn ($id) => $geom[$id]['y'] + $nodeH, $members)) + 16;
$frames[] = ['x' => $x0, 'y' => $y0, 'w' => $x1 - $x0, 'h' => $y1 - $y0, 'label' => $g['label'] ?? '', 'tone' => $g['tone'] ?? 'muted'];
}
// ── Tone recipes (token utilities only) ────────────────────────────────
$nodeTones = [
'default' => ['shape' => 'fill-card stroke-border', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
'primary' => ['shape' => 'fill-primary stroke-primary', 'text' => 'fill-primary-foreground', 'sub' => 'fill-primary-foreground/70'],
'info' => ['shape' => 'fill-info/10 stroke-info', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
'success' => ['shape' => 'fill-success/10 stroke-success', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
'warning' => ['shape' => 'fill-warning/10 stroke-warning', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
'destructive' => ['shape' => 'fill-destructive/10 stroke-destructive', 'text' => 'fill-foreground', 'sub' => 'fill-muted-foreground'],
'muted' => ['shape' => 'fill-muted stroke-border', 'text' => 'fill-muted-foreground', 'sub' => 'fill-muted-foreground'],
];
$edgeTones = [
'default' => 'text-muted-foreground',
'primary' => 'text-primary',
'info' => 'text-info',
'success' => 'text-success',
'warning' => 'text-warning',
'destructive' => 'text-destructive',
'muted' => 'text-border',
];
$usedEdgeTones = array_values(array_unique(array_map(fn ($p) => array_key_exists($p['tone'], $edgeTones) ? $p['tone'] : 'default', $paths)));
$uid = substr(md5($label.$W.$H.count($nodes)), 0, 8);
// Shape outlines. Every shape is drawn inside the node's box so the ports
// (box edges) stay correct for all of them.
$shapePath = function (array $g) use ($nodeW, $nodeH): array {
$x = $g['x']; $y = $g['y']; $w = $nodeW; $h = $nodeH; $cx = $g['cx']; $cy = $g['cy'];
$f = fn (float $v) => round($v, 2);
return match ($g['shape']) {
'diamond' => ['polygon', sprintf('%s %s %s %s %s %s %s %s', $f($cx), $f($y), $f($x + $w), $f($cy), $f($cx), $f($y + $h), $f($x), $f($cy))],
'parallelogram' => ['polygon', sprintf('%s %s %s %s %s %s %s %s', $f($x + 14), $f($y), $f($x + $w), $f($y), $f($x + $w - 14), $f($y + $h), $f($x), $f($y + $h))],
'hexagon' => ['polygon', sprintf('%s %s %s %s %s %s %s %s %s %s %s %s', $f($x + 14), $f($y), $f($x + $w - 14), $f($y), $f($x + $w), $f($cy), $f($x + $w - 14), $f($y + $h), $f($x + 14), $f($y + $h), $f($x), $f($cy))],
'circle' => ['circle', ''],
'cylinder' => ['path', sprintf('M%s %s v%s a%s 7 0 0 0 %s 0 v-%s a%s 7 0 0 0 -%s 0 a%s 7 0 0 0 %s 0', $f($x), $f($y + 7), $f($h - 14), $f($w / 2), $f($w), $f($h - 14), $f($w / 2), $f($w), $f($w / 2), $f($w))],
'document' => ['path', sprintf('M%s %s h%s v%s q-%s -12 -%s 0 t-%s 0 z', $f($x), $f($y), $f($w), $f($h - 6), $f($w / 4), $f($w / 2), $f($w / 2))],
'note' => ['path', sprintf('M%s %s h%s l12 12 v%s h-%s z', $f($x), $f($y), $f($w - 12), $f($h - 12), $f($w))],
default => ['rect', ''],
};
};
@endphp
<div
data-slot="flow-diagram"
data-direction="{{ $tb ? 'tb' : 'lr' }}"
role="img"
aria-label="{{ $label }}"
{{ $attributes->merge(['class' => 'relative w-full text-foreground']) }}
>
{{-- A top-to-bottom chart keeps its natural width (centred, capped at the
container) instead of stretching a narrow column to full width. --}}
<svg
viewBox="0 0 {{ round($W, 2) }} {{ round($H, 2) }}"
width="{{ $tb ? round($W) : '100%' }}"
height="{{ round($H) }}"
preserveAspectRatio="xMidYMid meet"
class="{{ $tb ? 'mx-auto h-auto max-w-full' : 'h-auto w-full' }}"
fill="none"
aria-hidden="true"
>
<defs>
{{-- One arrowhead per edge tone. `userSpaceOnUse` keeps the head the
same size whatever the stroke; refX puts the tip on the port. --}}
@foreach ($usedEdgeTones as $tone)
<marker
id="flow-{{ $uid }}-{{ $tone }}"
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="10"
markerHeight="10"
markerUnits="userSpaceOnUse"
orient="auto-start-reverse"
class="{{ $edgeTones[$tone] }}"
>
<path d="M1 1 L9 5 L1 9 z" fill="currentColor" stroke="currentColor" stroke-width="1" stroke-linejoin="round" />
</marker>
@endforeach
</defs>
{{-- Group frames (behind everything). --}}
@foreach ($frames as $frame)
<g data-slot="flow-diagram-group">
<rect
class="{{ $frame['tone'] === 'muted' ? 'fill-muted/40 stroke-border' : ($nodeTones[$frame['tone']]['shape'] ?? 'fill-muted/40 stroke-border') }}"
x="{{ round($frame['x'], 2) }}"
y="{{ round($frame['y'], 2) }}"
width="{{ round($frame['w'], 2) }}"
height="{{ round($frame['h'], 2) }}"
rx="12"
stroke-width="1"
stroke-dasharray="4 4"
/>
<text
class="fill-muted-foreground text-2xs font-medium uppercase tracking-wide"
x="{{ round($frame['x'] + 12, 2) }}"
y="{{ round($frame['y'] + 14, 2) }}"
dominant-baseline="middle"
>{{ $frame['label'] }}</text>
</g>
@endforeach
{{-- Edges (behind the nodes). --}}
@foreach ($paths as $p)
@php($tone = array_key_exists($p['tone'], $edgeTones) ? $p['tone'] : 'default')
<path
data-slot="flow-diagram-edge"
class="{{ $edgeTones[$tone] }}"
d="{{ $p['d'] }}"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
fill="none"
@if ($p['dash']) stroke-dasharray="{{ $p['dash'] }}" @endif
@if ($p['arrow'] !== 'none') marker-end="url(#flow-{{ $uid }}-{{ $tone }})" @endif
@if ($p['arrow'] === 'both') marker-start="url(#flow-{{ $uid }}-{{ $tone }})" @endif
/>
@if ($p['label'])
<rect
class="fill-background stroke-border"
x="{{ round($p['lx'] - $p['lw'] / 2, 2) }}"
y="{{ round($p['ly'] - 10, 2) }}"
width="{{ round($p['lw'], 2) }}"
height="20"
rx="10"
stroke-width="1"
/>
<text
data-slot="flow-diagram-edge-label"
class="pointer-events-none fill-muted-foreground text-2xs font-medium"
x="{{ round($p['lx'], 2) }}"
y="{{ round($p['ly'], 2) }}"
text-anchor="middle"
dominant-baseline="middle"
>{{ $p['label'] }}</text>
@endif
@endforeach
{{-- Nodes. --}}
@foreach ($nodes as $n)
@php($g = $geom[$n['id']])
@php($tone = $nodeTones[$g['tone']] ?? $nodeTones['default'])
@php([$kind, $data] = $shapePath($g))
<g data-slot="flow-diagram-node" data-shape="{{ $g['shape'] }}" data-tone="{{ $g['tone'] }}">
@if ($kind === 'polygon')
<polygon class="{{ $tone['shape'] }}" points="{{ $data }}" stroke-width="1.5" stroke-linejoin="round" @if ($g['dashed']) stroke-dasharray="5 3" @endif />
@elseif ($kind === 'circle')
<circle class="{{ $tone['shape'] }}" cx="{{ round($g['cx'], 2) }}" cy="{{ round($g['cy'], 2) }}" r="{{ round($nodeH / 2, 2) }}" stroke-width="1.5" @if ($g['dashed']) stroke-dasharray="5 3" @endif />
@elseif ($kind === 'path')
<path class="{{ $tone['shape'] }}" d="{{ $data }}" stroke-width="1.5" stroke-linejoin="round" @if ($g['dashed']) stroke-dasharray="5 3" @endif />
@else
<rect
class="{{ $tone['shape'] }}"
x="{{ round($g['x'], 2) }}"
y="{{ round($g['y'], 2) }}"
width="{{ $nodeW }}"
height="{{ $nodeH }}"
rx="{{ $g['shape'] === 'pill' ? round($nodeH / 2, 2) : 8 }}"
stroke-width="1.5"
@if ($g['dashed']) stroke-dasharray="5 3" @endif
/>
@if ($g['shape'] === 'subroutine')
<line class="{{ $tone['shape'] }}" x1="{{ round($g['x'] + 10, 2) }}" y1="{{ round($g['y'], 2) }}" x2="{{ round($g['x'] + 10, 2) }}" y2="{{ round($g['y'] + $nodeH, 2) }}" stroke-width="1.5" />
<line class="{{ $tone['shape'] }}" x1="{{ round($g['x'] + $nodeW - 10, 2) }}" y1="{{ round($g['y'], 2) }}" x2="{{ round($g['x'] + $nodeW - 10, 2) }}" y2="{{ round($g['y'] + $nodeH, 2) }}" stroke-width="1.5" />
@endif
@endif
<text
class="pointer-events-none {{ $tone['text'] }} text-xs font-medium"
x="{{ round($g['cx'], 2) }}"
y="{{ round($g['description'] ? $g['cy'] - 7 : $g['cy'], 2) }}"
text-anchor="middle"
dominant-baseline="middle"
>{{ $g['label'] }}</text>
@if ($g['description'])
<text
class="pointer-events-none {{ $tone['sub'] }} text-2xs"
x="{{ round($g['cx'], 2) }}"
y="{{ round($g['cy'] + 9, 2) }}"
text-anchor="middle"
dominant-baseline="middle"
>{{ $g['description'] }}</text>
@endif
</g>
@endforeach
</svg>
</div>
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.1.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0