@cardenelabs/cdl
v0.14.0
Published
CDL (Chainome Diagram Language). Mermaid-like declarative DSL that compiles to animated SVG diagrams. Built for blockchain / Solidity flows, generic enough for sequence / flow / state / ER / topology diagrams.
Maintainers
Readme
@cardenelabs/cdl
Declarative TypeScript DSL for animated React + SVG diagrams. UML / ER / state machine / topology / sequence / flow を 1 行で書く OSS library。
Documentation
| 目的 | リンク |
|---|---|
| repo root | github.com/cardene777/cdl |
| 5 分で動かす | README quickstart (本 file 下部) |
| playground demo | pnpm dev → http://localhost:4321 (apps/playground) |
| 全 API + preset 一覧 | playground /docs/reference |
| Text DSL spec | apps/playground/src/content/cdl-docs/text-dsl-spec.md |
| LLM prompt guide | apps/playground/src/content/cdl-docs/text-dsl-llm-guide.md |
| contribute | CONTRIBUTING.md |
なぜ cdl
mermaid / PlantUML はテキストから静止画像を生成する。 cdl は TypeScript → animated React + SVG component で、 phase / state / tween / progress glow 等の動きを宣言的に書ける。
| 形式 | mermaid | cdl | |---|---|---| | sequence | 静止 | animated + phase 切替 | | ER 図 | 静止 | animated + cardinality + sub label | | state diagram | 静止 | animated + initial/final marker + guard | | flow | 静止 | animated + dotted-flow 進行点 glow | | 構成図 / deployment | 限定的 | animated + container group + connection | | 自由レイアウト | 限定的 | lane + node + edge で自由構成 |
Quickstart
pnpm add @cardenelabs/cdl react react-domdiagram declare
import { sequence } from "@cardenelabs/cdl";
export const authSeq = sequence({
id: "auth",
topic: "User Login",
actors: ["User", "API", "DB"],
})
.step({ from: "User", to: "API", label: "POST /login", sub: "email + password" })
.step({ from: "API", to: "DB", label: "SELECT credentials" })
.step({ from: "DB", to: "API", label: "rows", tone: "success", style: "dotted-flow" })
.step({ from: "API", to: "User", label: "200 OK", sub: "JWT token", tone: "success" })
.build();React で render
import { CdlDiagramView } from "@cardenelabs/cdl";
import { authSeq } from "./auth-seq";
export function AuthSequenceDemo() {
return <CdlDiagramView diagram={authSeq} />;
}これで UML sequence diagram が phase 切替 + state diff + 進行点 glow で animated に表示。
6 つの preset
低位 API (lane / node / edge 個別宣言) + 高位 preset (1 行で典型図生成)。
swimlane
横並び 3 lane 自動配置 + slug 自動。
swimlane({ id, topic, lanes: ["送信元", "Contract", "出力"] })
.node("alice", { lane: "送信元", stack: 0, kind: "actor", title: "Alice" })
.node("fn", { lane: "Contract", stack: 0, kind: "function", title: "transfer(...)" })
.edge("alice", "fn", { id: "call", label: "call", tone: "accent", style: "dotted-flow" })
.build()flow
1 lane 縦 stack、 前 step → 次 step 自動 edge。
flow({ id, topic, laneLabel: "Auth Flow", defaultTone: "teal" })
.step({ id: "user", kind: "person", title: "User" })
.step({ id: "api", kind: "api", title: "POST /login" }, "ログイン要求")
.step({ id: "db", kind: "database", title: "users 表" }, "credential 検証")
.build()sequence
UML sequence diagram (actor lifeline + 時系列 message)。
sequence({ id, topic, actors: ["User", "API", "DB"] })
.step({ from: "User", to: "API", label: "POST /login" })
.step({ from: "API", to: "DB", label: "SELECT" })
.step({ from: "API", to: "User", label: "200 OK", tone: "success" })
.build()topology
構成図 / deployment diagram (group + container + connection)。
topology({ id, topic })
.group("aws", { label: "AWS" })
.add({ id: "alb", kind: "service", title: "ALB" })
.add({ id: "ecs", kind: "service", title: "ECS Task" })
.add({ id: "rds", kind: "database", title: "RDS" })
.group("client", { label: "Client" })
.add({ id: "browser", kind: "frontend", title: "Browser" })
.connect("browser", "alb", { label: "HTTPS" })
.connect("ecs", "rds", { label: "TCP 5432", tone: "success" })
.build()er
ER 図 (entity + cardinality)。
er({ id, topic })
.entity({ id: "user", title: "User", rows: ["id: PK", "email: string", "createdAt: timestamp"] })
.entity({ id: "order", title: "Order", rows: ["id: PK", "userId: FK", "total: number"] })
.relation({ from: "user", to: "order", cardinality: "1:N", label: "places" })
.build()cardinality 6 種 ... "1:1" | "1:N" | "N:1" | "N:M" | "0..1" | "1..*"。
stateMachine
FSM / workflow (state + transition trigger + guard)。
stateMachine({ id, topic })
.state({ id: "idle", title: "Idle", initial: true })
.state({ id: "loading", title: "Loading" })
.state({ id: "done", title: "Done", final: true })
.state({ id: "error", title: "Error" })
.transition({ from: "idle", to: "loading", trigger: "submit" })
.transition({ from: "loading", to: "done", trigger: "success", tone: "success" })
.transition({ from: "loading", to: "error", trigger: "fail", tone: "error" })
.transition({ from: "error", to: "idle", trigger: "retry", guard: "if attempts < 3" })
.build()低位 API
preset で表現しきれない自由レイアウトは低位 API で。
import { diagram } from "@cardenelabs/cdl";
export const custom = diagram("custom", { topic: "Custom" })
.lane("left", { x: 0, width: 400 })
.lane("right", { x: 500, width: 400 })
.node("a", { lane: "left", stack: 0, kind: "actor", title: "Alice" })
.node("b", { lane: "right", stack: 0, kind: "function", title: "Bob" })
.edge("a", "b", { id: "e", label: "msg", tone: "accent", style: "solid" })
.state("counter", { initial: 0 })
.phase("p1", { duration: 1500, title: "Phase 1", body: "tween counter" },
(p) => p.activate("a", "b", "e").tween("counter", 0, 100).badge("running"))
.build();NodeKind 29 種
| 系統 | kinds |
|---|---|
| 基本 5 | actor / function / storage / event / card |
| 人系 5 | person / user-group / admin / developer / external-user |
| インフラ 6 | database / cache / queue / message-bus / cloud / cdn |
| アプリ系 6 | service / api / frontend / backend / webhook / microservice |
| blockchain 8 | wallet / validator / miner / blockchain-node / mempool / block / bridge-node / relayer |
| 暗号 / データ 4 | signer / oracle / merkle-tree / decision |
各 kind は shape / color / icon が異なる。 基本 5 は専用 component、 残り 24 種は GenericNode で汎用描画。
EdgeStyle + Tone
| EdgeStyle | 動き |
|---|---|
| solid | 実線 + 矢頭 marker、 progress 連動で path が伸びる |
| dotted-flow | 点線 + 進行点 glow (3 重円) が path 上を流れる、 node 貫通自動判定 |
| Tone | 色 (hex) | 用途 |
|---|---|---|
| accent | #c17f3e | default |
| teal | #4a8b7f | 補助動作 |
| success | #6b9e5a | 成功 path |
| error | #c15a4a | 失敗 path |
| warning | #c9a23e | 警告 |
| info | #5a8ec1 | 情報 |
phase / state / tween / set / badge
phase 内で state を tween / set し、 badge を出す。
.state("amount", { initial: 0 })
.state("status", { initial: "idle" })
.phase("p1", { duration: 1800, title: "Phase 1", body: "tween + set" },
(p) => p
.activate("a", "b")
.tween("amount", 0, 100) // 数値の線形補間
.set("status", "running") // 文字列の即時切替
.badge("processing")) // header に badge 表示state placeholder は node の title / subtitle / eyebrow / value 内で {stateId} 記法で参照。
.node("a", { lane: "l", stack: 0, kind: "function", title: "Process", subtitle: "status: {status}" })phase 切替に連動して status: idle → status: running のように描画される。
CdlDiagramView props
<CdlDiagramView
diagram={d}
hideHeader={false} // header (phase indicator + state diff) 表示
focusPhaseId="p1" // 特定 phase に固定 (autoplay は維持)
debug={false} // bbox + collision を半透明で重ね描き (開発用)
/>見た目を当てる (role selector)
cdl は形だけを描き、 色や影は持たない (geometry only、 style 不干渉、 CAR-643 SSOT)。
描かれる各部品には data-cdl-role が付くので、 consumer app 側が selector で見た目を当てる。
[data-cdl-role="node-body"] {
fill: #ffffff;
stroke: #57534b;
stroke-width: 1.75;
}明暗の切替は consumer 側の仕組み (html の class 等) で行う = cdl は明暗の概念を持たない。
CdlRole 型は実装にある role の全部ではない。 実装には 32 種の role があり、
型に載っているのは 12 種。 残り 20 種 (chart-line / tree-edge / funnel-stage /
edge-glow 等) は型を持たないが、 属性は付くので selector は書ける。
型の内外は「共通か kind 固有か」 では分かれていない (共通の描画で使う edge-glow が型の外に
あり、 枝分かれ図でしか使わない mind-edge も型の外にある)。 型に載っているかどうかは
selector を書けるかとは無関係なので、 一覧が要る時は source を data-cdl-role= で検索する。
role が付かない小さな部品もある (kind が自前で描く目盛や軸など)。 それらは CSS 変数で色を渡す。
:root {
--cdl-node-fill: #f2f1ec;
--cdl-text: #191714;
--cdl-tone-accent: #2c68ae;
}CdlDiagramThumbnail
thumbnail として表示、 click で viewport いっぱい (94vw x 94vh) のモーダルで拡大。
import { CdlDiagramThumbnail } from "@cardenelabs/cdl";
<CdlDiagramThumbnail diagram={d} hideHeader />close は Escape / 背景クリック / × の 3 経路。
Architecture
src/
├── builder.ts — DSL builder
├── compile.ts — DSL → CdlDiagram normalization
├── validate.ts — schema validation
├── layout/ — layout engine (6 file)
│ ├── tokens.ts — DESIGN TOKENS SSOT
│ ├── lanes.ts — lane 配置 + auto width
│ ├── nodes.ts — node cy/cx/w/h 確定
│ ├── edges.ts — orthogonal routing + label
│ ├── collisions.ts — bbox + clearance + Liang-Barsky
│ └── viewbox.ts — auto viewBox 計算
├── render/ — React + SVG (7 file)
│ ├── stage.tsx — SVG defs + 3 pass
│ ├── nodes.tsx — NodeKind 別 switch
│ ├── edges.tsx — solid / dotted-flow
│ ├── header.tsx — phase indicator + state diff
│ ├── utils.ts — interpolate / pathSubpath / shrinkPathEnd
│ └── tone.ts — TONE 6 色
├── kinds/ — 専用 component (actor/function/storage/event/card/generic)
├── presets.ts — 6 preset
├── thumbnail.tsx — モーダル拡大
└── types.ts — public typesTesting
pnpm testvitest で 36 test (builder / compile / layout / sequence preset / snapshot) を実行、 全 pass 確認。
Performance baseline
packages/cdl/test/bench.test.ts の vitest bench で 5 case (small / medium / large / huge / animation-heavy) × 3 stage (parse / compile / layout) を計測する。
pnpm --filter @cardenelabs/cdl exec vitest bench --runbaseline (mean ms、 Apple Silicon / node 24)。
| case (node / edge / phase) | parse | compile | layout (旧) | layout (最適化後) | | --- | --- | --- | --- | --- | | small (10 / 5 / 0) | 0.002 | 0.020 | 0.260 | 0.192 | | medium (100 / 50 / 5) | 0.022 | 1.488 | 25.014 | 0.807 | | large (500 / 200 / 20) | 0.092 | 29.953 | 492.96 | 11.63 | | huge (1000 / 500 / 50) | 0.200 | 129.88 | 2424.39 | 53.74 | | animation-heavy (100 node / 100 phase / 50 tween) | 0.105 | 1.582 | 24.586 | 0.819 |
最適化内容 ... detectCollisions / detectNearCollisions を全 pair O(n²) から spatial hash (cell size 200px、 max clearance 70px margin) に変更。 巨大 bbox (edge-path 等) は fallback 経路で全件比較。 huge case で約 46x 高速化、 1000 node でも 60ms 以下で完了。
stage 内訳。 parse = parseTextDslV05 (Text DSL → DslDocument)、 compile = compileToCdl (DslDocument → CdlDiagram)、 layout = layout (CdlDiagram → LaidDiagram)。 raw 出力は .context/scratch/bench-baseline.{txt,json} (vitest --outputJson 形式)。
License
MIT
