<!DOCTYPE html>
<html lang="__CNX_LOCALE__" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>__CNX_TITLE__</title>
<style>
:root, html[data-theme="dark"] {
--bg: #101014; --panel: #1a1a22; --panel-2: #22222c; --border: #33333f;
--text: #e8e8ee; --muted: #9a9aad; --accent: #6ea8fe;
--edge: #5a5a72; --edge-async: #b48ead; --edge-error: #e06c75;
--iface: #7aa2f7; --service: #9ece6a; --storage: #e0af68; --model: #bb9af7;
--hl: #ffcc55; --dim: 0.18;
}
html[data-theme="light"] {
--bg: #f7f7fa; --panel: #ffffff; --panel-2: #eef0f5; --border: #d5d8e2;
--text: #1d1f27; --muted: #6a6d80; --accent: #2563eb;
--edge: #a8adc2; --edge-async: #8d5fa8; --edge-error: #c8323e;
--iface: #2563eb; --service: #15803d; --storage: #b45309; --model: #7c3aed;
--hl: #d97706; --dim: 0.18;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--bg); color: var(--text);
font: 14px/1.5 -apple-system, "Segoe UI", "Noto Sans SC", sans-serif;
display: flex; flex-direction: column; height: 100vh;
}
header {
display: flex; align-items: center; gap: 12px; padding: 10px 16px;
background: var(--panel); border-bottom: 1px solid var(--border);
flex: none;
}
header h1 { font-size: 16px; margin: 0; flex: 1; min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
header .hint { color: var(--muted); font-size: 12px; }
button, .btn {
background: var(--panel-2); color: var(--text); border: 1px solid var(--border);
border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 13px;
}
button:hover { border-color: var(--accent); }
main { flex: 1; display: flex; min-height: 0; }
#stage { flex: 1; overflow: hidden; position: relative; }
#stage svg.arch-diagram { width: 100%; height: 100%; display: block;
cursor: grab; user-select: none; }
#stage svg.arch-diagram.dragging { cursor: grabbing; }
aside {
width: 300px; flex: none; background: var(--panel);
border-left: 1px solid var(--border); padding: 14px; overflow-y: auto;
display: none;
}
aside.open { display: block; }
aside h2 { font-size: 14px; margin: 0 0 8px; }
aside .row { margin: 4px 0; }
aside .row .k { color: var(--muted); margin-right: 6px; }
aside .srcs { margin-top: 10px; }
aside .srcs a { color: var(--accent); text-decoration: none; font-size: 12px;
display: block; padding: 2px 0; word-break: break-all; }
aside .actions { margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
.badge { display: inline-block; border: 1px solid var(--border);
border-radius: 4px; padding: 0 6px; font-size: 11px; color: var(--muted); }
footer {
flex: none; padding: 6px 16px; color: var(--muted); font-size: 12px;
background: var(--panel); border-top: 1px solid var(--border);
display: flex; gap: 16px; justify-content: space-between; flex-wrap: wrap;
}
#finder {
position: absolute; top: 10px; left: 50%; transform: translateX(-50%);
background: var(--panel); border: 1px solid var(--border); border-radius: 8px;
min-width: 320px; max-width: 480px; display: none; z-index: 10;
box-shadow: 0 8px 24px rgba(0,0,0,.35);
}
#finder.open { display: block; }
#finder input {
width: 100%; padding: 8px 10px; background: var(--panel-2); color: var(--text);
border: none; border-bottom: 1px solid var(--border); border-radius: 8px 8px 0 0;
outline: none; font-size: 14px;
}
#finder ul { list-style: none; margin: 0; padding: 4px; max-height: 240px; overflow-y: auto; }
#finder li { padding: 5px 8px; border-radius: 5px; cursor: pointer; font-size: 13px; }
#finder li:hover, #finder li.active { background: var(--panel-2); }
#probe-tip {
position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
background: var(--panel); border: 1px solid var(--accent); color: var(--text);
padding: 6px 14px; border-radius: 8px; display: none; z-index: 10; font-size: 13px;
}
#probe-tip.on { display: block; }
svg.arch-diagram .node rect { fill: var(--panel-2); stroke: var(--border); stroke-width: 1.5; }
svg.arch-diagram .node:hover rect { stroke: var(--accent); }
svg.arch-diagram .node-interface rect { stroke: var(--iface); }
svg.arch-diagram .node-service rect { stroke: var(--service); }
svg.arch-diagram .node-storage rect { stroke: var(--storage); }
svg.arch-diagram .node-model rect { stroke: var(--model); }
svg.arch-diagram .node { cursor: pointer; transition: opacity .15s ease; }
svg.arch-diagram text { fill: var(--text); font-size: 13px;
font-family: inherit; text-anchor: middle; }
svg.arch-diagram .node-sublabel, svg.arch-diagram .node-tag,
svg.arch-diagram .legend text { fill: var(--muted); font-size: 11px; }
svg.arch-diagram .legend-entry rect {
fill: var(--panel-2); stroke: var(--border); stroke-width: 1;
}
svg.arch-diagram .legend-entry text { text-anchor: start; }
svg.arch-diagram .legend-node-interface rect { stroke: var(--iface); }
svg.arch-diagram .legend-node-service rect { stroke: var(--service); }
svg.arch-diagram .legend-node-storage rect { stroke: var(--storage); }
svg.arch-diagram .legend-node-model rect { stroke: var(--model); }
svg.arch-diagram .edge polyline { fill: none; stroke: var(--edge); stroke-width: 1.6; }
svg.arch-diagram .edge-async polyline { stroke: var(--edge-async); stroke-dasharray: 6 4; }
svg.arch-diagram .edge-error polyline { stroke: var(--edge-error); stroke-width: 2; }
svg.arch-diagram .edge text { fill: var(--muted); font-size: 10px; }
svg.arch-diagram .edge, svg.arch-diagram .node { transition: opacity .15s ease; }
svg.arch-diagram.has-focus .node:not(.focused):not(.reached):not(.adj),
svg.arch-diagram.has-focus .edge:not(.focused):not(.adj) { opacity: var(--dim); }
svg.arch-diagram .node.focused rect { stroke: var(--hl); stroke-width: 2.5; }
svg.arch-diagram .edge.focused polyline, svg.arch-diagram .edge.adj polyline {
stroke: var(--hl); stroke-width: 2.2; }
svg.arch-diagram .edge.route polyline { stroke: var(--accent); stroke-width: 2.6; }
svg.arch-diagram .node.route rect { stroke: var(--accent); stroke-width: 2.5; }
.src-badge {
position: absolute; pointer-events: none;
}
@media (prefers-reduced-motion: reduce) {
svg.arch-diagram .node, svg.arch-diagram .edge { transition: none; }
}
</style>
</head>
<body>
<header>
<h1>__CNX_TITLE__</h1>
<span class="hint" data-i18n="hint"></span>
<button id="btn-search" title="/">⌕</button>
<button id="btn-theme" title="T">◐</button>
<button id="btn-reset" title="0">⤢</button>
<button id="btn-export" title="E">⇩</button>
</header>
<main>
<div id="stage">
<div id="finder">
<input id="finder-input" type="text" autocomplete="off">
<ul id="finder-list"></ul>
</div>
<div id="probe-tip"></div>
__CNX_SVG__
</div>
<aside id="panel">
<h2 id="panel-title">—</h2>
<div class="row"><span class="k" data-i18n="type"></span><span class="badge" id="panel-type"></span></div>
<div class="row"><span class="k" data-i18n="tag"></span><span id="panel-tag"></span></div>
<div class="row"><span class="k" data-i18n="files"></span><span id="panel-sub"></span></div>
<div class="row"><span class="k" data-i18n="deps"></span><span id="panel-degrees"></span></div>
<div class="actions">
<button id="btn-upstream" data-i18n="upstream"></button>
<button id="btn-downstream" data-i18n="downstream"></button>
<button id="btn-clear" data-i18n="clear"></button>
</div>
<div class="srcs" id="panel-srcs"></div>
</aside>
</main>
<footer>
<span id="truth-boundary"></span>
<span id="reach-status"></span>
</footer>
<script id="cnx-data" type="application/json">__CNX_DATA__</script>
<script id="cnx-evidence" type="application/json">__CNX_EVIDENCE__</script>
<script>
(function () {
"use strict";
var LOCALE = "__CNX_LOCALE__";
var MESSAGES = {
"en": {
"hint": "/ search · R route · T theme · 0 reset · E export",
"type": "Type", "tag": "Layer", "files": "Files", "deps": "In/Out",
"upstream": "Upstream", "downstream": "Downstream", "clear": "Clear",
"search-ph": "Find node…", "no-results": "No matches",
"probe-1": "Route probe: pick the start node",
"probe-2": "Route probe: pick the end node (Esc to cancel)",
"probe-none": "No directed route between those nodes",
"route": "Route", "reach-up": "Upstream reach", "reach-down": "Downstream reach",
"depth": "depth", "verified": "verified source",
"truth": "Edges reflect static index facts (CALLS / HTTP), not a runtime call graph.",
"export-svg": "Download SVG", "export-png": "Download PNG"
},
"zh-CN": {
"hint": "/ 搜索 · R 路由 · T 主题 · 0 重置 · E 导出",
"type": "类型", "tag": "层级", "files": "文件", "deps": "入/出",
"upstream": "上游", "downstream": "下游", "clear": "清除",
"search-ph": "查找节点…", "no-results": "无匹配",
"probe-1": "路由探测:点击起点节点",
"probe-2": "路由探测:点击终点节点(Esc 取消)",
"probe-none": "两节点间无有向路径",
"route": "路径", "reach-up": "上游可达", "reach-down": "下游可达",
"depth": "深度", "verified": "已验证源码",
"truth": "连线反映静态索引事实(CALLS / HTTP),并非运行时调用图。",
"export-svg": "下载 SVG", "export-png": "下载 PNG"
}
};
function t(key) { return (MESSAGES[LOCALE] || MESSAGES.en)[key] || MESSAGES.en[key] || key; }
function parseJSON(id, fallback) {
var el = document.getElementById(id);
if (!el) return fallback;
try { return JSON.parse(el.textContent); } catch (e) { return fallback; }
}
var data = parseJSON("cnx-data", { nodes: [], edges: [] });
var evidence = parseJSON("cnx-evidence", null);
var svg = document.querySelector("#stage svg.arch-diagram");
var stage = document.getElementById("stage");
var nodes = {};
var outgoing = {}; var incoming = {};
data.nodes.forEach(function (n) { nodes[n.id] = n; outgoing[n.id] = []; incoming[n.id] = []; });
var edgeGroups = svg ? svg.querySelectorAll(".edge") : [];
var nodeGroups = svg ? svg.querySelectorAll(".node") : [];
edgeGroups.forEach(function (g) {
var from = g.getAttribute("data-edge-from");
var to = g.getAttribute("data-edge-to");
if (!nodes[from] || !nodes[to]) return;
var conn = { g: g, from: from, to: to };
outgoing[from].push(conn);
incoming[to].push(conn);
});
var baseView = svg ? svg.viewBox.baseVal : null;
var view = { x: baseView ? baseView.x : 0, y: baseView ? baseView.y : 0,
w: baseView ? baseView.width : 1, h: baseView ? baseView.height : 1 };
function applyView() {
if (!svg) return;
svg.setAttribute("viewBox", view.x + " " + view.y + " " + view.w + " " + view.h);
}
function zoomBy(factor) {
var cx = view.x + view.w / 2, cy = view.y + view.h / 2;
view.w *= factor; view.h *= factor;
view.x = cx - view.w / 2; view.y = cy - view.h / 2;
applyView();
}
function centerOn(px, py) {
view.x = px - view.w / 2; view.y = py - view.h / 2;
applyView();
}
var reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
function panToNode(id) {
var g = svg.querySelector('[data-node-id="' + id + '"] rect');
if (!g) return;
var b = g.getBBox();
centerOn(b.x + b.width / 2, b.y + b.height / 2);
}
var drag = null;
if (svg) {
svg.addEventListener("pointerdown", function (e) {
if (e.target.closest(".node")) return;
drag = { x: e.clientX, y: e.clientY, vx: view.x, vy: view.y };
svg.classList.add("dragging");
});
window.addEventListener("pointermove", function (e) {
if (!drag) return;
var rect = svg.getBoundingClientRect();
var sx = view.w / rect.width, sy = view.h / rect.height;
view.x = drag.vx - (e.clientX - drag.x) * sx;
view.y = drag.vy - (e.clientY - drag.y) * sy;
applyView();
});
window.addEventListener("pointerup", function () { drag = null; svg.classList.remove("dragging"); });
svg.addEventListener("wheel", function (e) {
e.preventDefault();
zoomBy(e.deltaY > 0 ? 1.15 : 1 / 1.15);
}, { passive: false });
}
var svgEl = svg;
function clearHighlights() {
if (!svgEl) return;
svgEl.classList.remove("has-focus");
svgEl.querySelectorAll(".focused,.reached,.adj,.route").forEach(function (el) {
el.classList.remove("focused", "reached", "adj", "route");
});
edgeGroups.forEach(function (g) { g.classList.remove("focused", "adj", "route"); });
document.getElementById("reach-status").textContent = "";
}
function clearFocus() {
focusId = null;
clearHighlights();
document.getElementById("panel").classList.remove("open");
}
function bfs(start, adjacency) {
var depth = {}; depth[start] = 0;
var order = [];
var queue = [start];
while (queue.length) {
var id = queue.shift();
(adjacency[id] || []).forEach(function (conn) {
var next = conn.to;
if (depth[next] === undefined) {
depth[next] = depth[id] + 1;
order.push({ from: conn.from, to: conn.to, g: conn.g, depth: depth[next] });
queue.push(next);
}
});
}
return { depth: depth, order: order };
}
function showReach(direction) {
if (!focusId || !svgEl) return;
clearHighlights();
svgEl.classList.add("has-focus");
svgEl.querySelector('[data-node-id="' + focusId + '"]').classList.add("focused");
var adjacency = direction === "upstream" ? incoming : outgoing;
var result = bfs(focusId, adjacency);
result.order.forEach(function (step) {
step.g.classList.add("adj");
var target = svgEl.querySelector('[data-node-id="' + step.to + '"]');
if (target) {
target.classList.add("reached");
target.setAttribute("data-reach-depth", step.depth);
}
});
svgEl.querySelector('[data-node-id="' + focusId + '"]').classList.add("focused");
var label = direction === "upstream" ? t("reach-up") : t("reach-down");
document.getElementById("reach-status").textContent =
label + ": " + result.order.length + " (" + t("depth") + " ≤ " +
(result.order.reduce(function (m, s) { return Math.max(m, s.depth); }, 0)) + ")";
}
var focusId = null;
function focusNode(id) {
focusId = id;
var node = nodes[id];
if (!node) return;
var panel = document.getElementById("panel");
panel.classList.add("open");
document.getElementById("panel-title").textContent = node.label;
document.getElementById("panel-type").textContent = node.type;
document.getElementById("panel-tag").textContent = node.tag || "—";
document.getElementById("panel-sub").textContent = node.sublabel || "—";
document.getElementById("panel-degrees").textContent =
incoming[id].length + " / " + outgoing[id].length;
var srcs = document.getElementById("panel-srcs");
srcs.textContent = "";
(evidence && evidence.node_sources && evidence.node_sources[id] || []).forEach(function (ref) {
var a = document.createElement("a");
a.textContent = "SRC " + ref.path + (ref.line ? ":" + ref.line : "");
if (ref.href) { a.href = ref.href; a.target = "_blank"; a.rel = "noopener"; }
else { a.textContent += " (" + t("verified") + ")"; }
srcs.appendChild(a);
});
}
if (svg) {
nodeGroups.forEach(function (g) {
g.addEventListener("click", function () {
var id = g.getAttribute("data-node-id");
if (probe.from === null) { probePick(id); return; }
if (probe.from !== null && probe.to === null) { probePick(id); return; }
clearAll();
focusNode(id);
svgEl.classList.add("has-focus");
g.classList.add("focused");
(incoming[id] || []).concat(outgoing[id] || []).forEach(function (c) {
c.g.classList.add("adj");
});
});
});
}
var probe = { active: false, from: null, to: null };
function probeTip(msg) {
var tip = document.getElementById("probe-tip");
if (!msg) { tip.classList.remove("on"); return; }
tip.textContent = msg;
tip.classList.add("on");
}
function probeStart() {
probe.active = true; probe.from = null; probe.to = null;
clearHighlights();
probeTip(t("probe-1"));
}
function probePick(id) {
if (!probe.active) return;
if (probe.from === null) {
probe.from = id;
var g = svgEl.querySelector('[data-node-id="' + id + '"]');
if (g) g.classList.add("route");
probeTip(t("probe-2"));
return;
}
if (probe.to === null && id !== probe.from) {
probe.to = id;
probeTip(null);
probe.active = false;
var prev = {}; var seen = {}; seen[probe.from] = true;
var queue = [probe.from];
while (queue.length) {
var cur = queue.shift();
if (cur === probe.to) break;
(outgoing[cur] || []).forEach(function (c) {
if (!seen[c.to]) { seen[c.to] = true; prev[c.to] = { node: cur, conn: c }; queue.push(c.to); }
});
}
if (!seen[probe.to]) { probeTip(t("probe-none")); probe.from = null; probe.to = null; return; }
var path = [];
var cur = probe.to;
while (cur !== probe.from) {
var step = prev[cur];
path.push(step);
cur = step.node;
}
path.reverse();
svgEl.classList.add("has-focus");
path.forEach(function (step) {
step.conn.g.classList.add("route");
var g = svgEl.querySelector('[data-node-id="' + step.node + '"]');
if (g) g.classList.add("route");
});
var last = svgEl.querySelector('[data-node-id="' + probe.to + '"]');
if (last) last.classList.add("route");
document.getElementById("reach-status").textContent =
t("route") + ": " + probe.from + " → " + probe.to + " (" + path.length + ")";
}
}
function clearAll() {
probe.active = false; probe.from = null; probe.to = null;
probeTip(null);
clearFocus();
}
var finder = document.getElementById("finder");
var finderInput = document.getElementById("finder-input");
var finderList = document.getElementById("finder-list");
var finderActive = -1;
function finderOpen() {
finder.classList.add("open");
finderInput.value = "";
finderList.textContent = "";
finderActive = -1;
finderInput.placeholder = t("search-ph");
finderInput.focus();
}
function finderClose() { finder.classList.remove("open"); }
function finderResults(q) {
var ql = q.toLowerCase();
return data.nodes.filter(function (n) {
return n.id.toLowerCase().indexOf(ql) >= 0 || n.label.toLowerCase().indexOf(ql) >= 0;
});
}
finderInput.addEventListener("input", function () {
var results = finderResults(finderInput.value);
finderList.textContent = "";
finderActive = -1;
if (!results.length) {
var li = document.createElement("li");
li.textContent = t("no-results");
finderList.appendChild(li);
return;
}
results.slice(0, 20).forEach(function (n) {
var li = document.createElement("li");
li.textContent = n.label + " (" + n.id + ")";
li.addEventListener("click", function () {
finderClose(); focusNode(n.id); panToNode(n.id);
});
finderList.appendChild(li);
});
});
finderInput.addEventListener("keydown", function (e) {
var items = Array.prototype.slice.call(finderList.querySelectorAll("li"));
if (e.key === "Escape") { finderClose(); return; }
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
if (!items.length) return;
finderActive = e.key === "ArrowDown"
? Math.min(finderActive + 1, items.length - 1)
: Math.max(finderActive - 1, 0);
items.forEach(function (el, i) { el.classList.toggle("active", i === finderActive); });
return;
}
if (e.key === "Enter") {
var target = items[finderActive] || items[0];
if (target && finderActive >= 0 || (target && finderResults(finderInput.value).length)) {
var result = finderResults(finderInput.value)[finderActive >= 0 ? finderActive : 0];
if (result) { finderClose(); focusNode(result.id); panToNode(result.id); }
}
}
});
function themeInit() {
var prefersLight = window.matchMedia("(prefers-color-scheme: light)").matches;
document.documentElement.setAttribute("data-theme", prefersLight ? "light" : "dark");
}
function themeToggle() {
var el = document.documentElement;
var next = el.getAttribute("data-theme") === "dark" ? "light" : "dark";
el.setAttribute("data-theme", next);
}
function serializeCanonicalSVG() {
var clone = svg.cloneNode(true);
clone.removeAttribute("class");
clone.querySelectorAll("[class]").forEach(function (el) { el.removeAttribute("class"); });
clone.querySelectorAll("[data-reach-depth]").forEach(function (el) {
el.removeAttribute("data-reach-depth");
});
clone.setAttribute("viewBox",
baseView.x + " " + baseView.y + " " + baseView.width + " " + baseView.height);
clone.setAttribute("xmlns", "http://www.w3.org/2000/svg");
return '<?xml version="1.0" encoding="UTF-8"?>\n' +
new XMLSerializer().serializeToString(clone);
}
function download(name, blob) {
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = name;
document.body.appendChild(a);
a.click();
setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 500);
}
function exportSVG() {
download(document.title.replace(/\s+/g, "-") + ".svg",
new Blob([serializeCanonicalSVG()], { type: "image/svg+xml" }));
}
function exportPNG() {
var scale = 2;
var b = baseView;
var canvas = document.createElement("canvas");
canvas.width = Math.min(b.width * scale, 16384);
canvas.height = Math.min(b.height * scale, 16384);
var img = new Image();
img.onload = function () {
var ctx = canvas.getContext("2d");
var bg = getComputedStyle(document.documentElement).getPropertyValue("--bg");
ctx.fillStyle = bg.trim() || "#ffffff";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
canvas.toBlob(function (blob) {
if (blob) download(document.title.replace(/\s+/g, "-") + ".png", blob);
}, "image/png");
};
img.src = "data:image/svg+xml;base64," +
btoa(unescape(encodeURIComponent(serializeCanonicalSVG())));
}
document.getElementById("btn-theme").addEventListener("click", themeToggle);
document.getElementById("btn-reset").addEventListener("click", function () {
view = { x: baseView.x, y: baseView.y, w: baseView.width, h: baseView.height };
applyView();
});
document.getElementById("btn-search").addEventListener("click", finderOpen);
document.getElementById("btn-export").addEventListener("click", function () {
exportPNG();
});
document.getElementById("btn-upstream").addEventListener("click", function () { showReach("upstream"); });
document.getElementById("btn-downstream").addEventListener("click", function () { showReach("downstream"); });
document.getElementById("btn-clear").addEventListener("click", clearAll);
document.addEventListener("keydown", function (e) {
if (e.target.tagName === "INPUT") return;
if (e.key === "/") { e.preventDefault(); finderOpen(); }
else if (e.key === "t" || e.key === "T") { themeToggle(); }
else if (e.key === "+" || e.key === "=") { zoomBy(1 / 1.25); }
else if (e.key === "-") { zoomBy(1.25); }
else if (e.key === "0") {
view = { x: baseView.x, y: baseView.y, w: baseView.width, h: baseView.height };
applyView();
}
else if (e.key === "r" || e.key === "R") { probeStart(); }
else if (e.key === "Escape") { clearAll(); finderClose(); }
});
document.querySelectorAll("[data-i18n]").forEach(function (el) {
el.textContent = t(el.getAttribute("data-i18n"));
});
document.getElementById("truth-boundary").textContent = t("truth");
function applyHash() {
var hash = window.location.hash.replace(/^#/, "");
if (!hash) return;
var params = {};
hash.split("&").forEach(function (pair) {
var kv = pair.split("=");
params[kv[0]] = decodeURIComponent(kv[1] || "");
});
if (params.focus && nodes[params.focus]) {
focusNode(params.focus);
svgEl.classList.add("has-focus");
var g = svgEl.querySelector('[data-node-id="' + params.focus + '"]');
if (g) g.classList.add("focused");
panToNode(params.focus);
if (params.reach === "upstream" || params.reach === "downstream") {
showReach(params.reach);
}
}
}
themeInit();
applyHash();
})();
</script>
</body>
</html>