feat(atlas): list view toggle + Mermaid export button
List view groups nodes by type with colored left border, status dot, description, connection count, URL link. Click card → switches back to graph view and opens detail panel. Mermaid export copies flowchart syntax to clipboard for pasting in Notion/GitHub/docs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0b9902b495
commit
2bad7274a6
250
index.html
250
index.html
|
|
@ -430,6 +430,121 @@
|
||||||
background: var(--border);
|
background: var(--border);
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
}
|
}
|
||||||
|
.tb-btn.active {
|
||||||
|
background: var(--accent-dim);
|
||||||
|
border-color: rgba(34,211,238,0.35);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── List view ─────────────────────────────────────────── */
|
||||||
|
#list-view {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 52px 0 0 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 5;
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
#list-view.visible { display: block; }
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.list-group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.list-group-dot {
|
||||||
|
width: 8px; height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.list-group-label {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text3);
|
||||||
|
}
|
||||||
|
.list-group-count {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.list-card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-left: 3px solid transparent;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.list-card:hover {
|
||||||
|
background: var(--surface2);
|
||||||
|
}
|
||||||
|
.list-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.list-card-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.list-card-status {
|
||||||
|
width: 6px; height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.list-card-status.active { background: var(--ok); box-shadow: 0 0 5px var(--ok); }
|
||||||
|
.list-card-status.inactive { background: var(--crit); }
|
||||||
|
.list-card-status.unknown { background: var(--text3); }
|
||||||
|
.list-card-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text3);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list-card-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.list-card-url {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 0.7;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.list-card-url:hover { opacity: 1; }
|
||||||
|
.list-card-conns {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text3);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Modal ─────────────────────────────────────────── */
|
/* ── Modal ─────────────────────────────────────────── */
|
||||||
#modal-overlay {
|
#modal-overlay {
|
||||||
|
|
@ -1246,6 +1361,9 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- List view container -->
|
||||||
|
<div id="list-view" role="main" aria-label="ノード一覧"></div>
|
||||||
|
|
||||||
<!-- Toolbar -->
|
<!-- Toolbar -->
|
||||||
<div id="toolbar" role="toolbar" aria-label="グラフ操作">
|
<div id="toolbar" role="toolbar" aria-label="グラフ操作">
|
||||||
<button class="tb-btn accent-btn" id="btn-ai" aria-label="AI Context を生成" title="AI Context">
|
<button class="tb-btn accent-btn" id="btn-ai" aria-label="AI Context を生成" title="AI Context">
|
||||||
|
|
@ -1254,6 +1372,16 @@
|
||||||
<button class="tb-btn" id="btn-share" aria-label="URLでシェア" title="URLでシェア">
|
<button class="tb-btn" id="btn-share" aria-label="URLでシェア" title="URLでシェア">
|
||||||
<i data-lucide="share-2" style="width:16px;height:16px;stroke-width:1.75"></i>
|
<i data-lucide="share-2" style="width:16px;height:16px;stroke-width:1.75"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="tb-btn" id="btn-mermaid" aria-label="Mermaid としてコピー" title="Mermaid export">
|
||||||
|
<i data-lucide="workflow" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||||||
|
</button>
|
||||||
|
<div class="tb-divider"></div>
|
||||||
|
<button class="tb-btn" id="btn-view-graph" aria-label="グラフビュー" title="グラフビュー">
|
||||||
|
<i data-lucide="network" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||||||
|
</button>
|
||||||
|
<button class="tb-btn" id="btn-view-list" aria-label="リストビュー" title="リストビュー">
|
||||||
|
<i data-lucide="layout-list" style="width:15px;height:15px;stroke-width:1.75"></i>
|
||||||
|
</button>
|
||||||
<div class="tb-divider"></div>
|
<div class="tb-divider"></div>
|
||||||
<button class="tb-btn" id="btn-add-node" aria-label="ノードを追加" title="ノードを追加">
|
<button class="tb-btn" id="btn-add-node" aria-label="ノードを追加" title="ノードを追加">
|
||||||
<i data-lucide="plus" style="width:17px;height:17px;stroke-width:1.75"></i>
|
<i data-lucide="plus" style="width:17px;height:17px;stroke-width:1.75"></i>
|
||||||
|
|
@ -2295,10 +2423,131 @@ async function runVercelScan() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── List view ──────────────────────────────────────────────────
|
||||||
|
let currentView = 'graph'; // 'graph' | 'list'
|
||||||
|
|
||||||
|
function renderListView() {
|
||||||
|
const el = document.getElementById('list-view');
|
||||||
|
const groups = {};
|
||||||
|
for (const node of atlasData.nodes) {
|
||||||
|
if (!groups[node.type]) groups[node.type] = [];
|
||||||
|
groups[node.type].push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeOrder = ['device','server','service','app','cloud','network'];
|
||||||
|
const html = typeOrder
|
||||||
|
.filter(t => groups[t]?.length)
|
||||||
|
.map(type => {
|
||||||
|
const nodes = groups[type];
|
||||||
|
const color = TYPE_COLORS[type] || '#9CA3AF';
|
||||||
|
const label = TYPE_LABELS[type] || type;
|
||||||
|
|
||||||
|
const cards = nodes.map(node => {
|
||||||
|
const connCount = atlasData.edges.filter(
|
||||||
|
e => e.from === node.id || e.to === node.id
|
||||||
|
).length;
|
||||||
|
const statusCls = node.status === 'active' ? 'active'
|
||||||
|
: node.status === 'inactive' ? 'inactive' : 'unknown';
|
||||||
|
const urlDisplay = node.url
|
||||||
|
? node.url.replace(/^https?:\/\//, '').replace(/\/$/, '')
|
||||||
|
: '';
|
||||||
|
return `
|
||||||
|
<div class="list-card" style="border-left-color:${color}"
|
||||||
|
onclick="switchToGraph('${node.id}')">
|
||||||
|
<div class="list-card-top">
|
||||||
|
<div class="list-card-status ${statusCls}"></div>
|
||||||
|
<span class="list-card-name">${node.label}</span>
|
||||||
|
</div>
|
||||||
|
${node.description
|
||||||
|
? `<div class="list-card-desc">${node.description}</div>` : ''}
|
||||||
|
<div class="list-card-footer">
|
||||||
|
${urlDisplay
|
||||||
|
? `<a class="list-card-url" href="${node.url}" target="_blank"
|
||||||
|
rel="noopener" onclick="event.stopPropagation()">${urlDisplay}</a>`
|
||||||
|
: '<span></span>'}
|
||||||
|
<span class="list-card-conns">${connCount} 接続</span>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="list-group">
|
||||||
|
<div class="list-group-header">
|
||||||
|
<div class="list-group-dot" style="background:${color}"></div>
|
||||||
|
<span class="list-group-label">${label}</span>
|
||||||
|
<span class="list-group-count">${nodes.length}</span>
|
||||||
|
</div>
|
||||||
|
<div class="list-cards">${cards}</div>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToGraph(nodeId) {
|
||||||
|
setView('graph');
|
||||||
|
if (nodeId) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (currentSimNodes.length) {
|
||||||
|
showDetail(nodeId, currentSimNodes, currentSimEdges);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setView(view) {
|
||||||
|
currentView = view;
|
||||||
|
const graphWrap = document.getElementById('graph-wrap');
|
||||||
|
const listEl = document.getElementById('list-view');
|
||||||
|
const btnGraph = document.getElementById('btn-view-graph');
|
||||||
|
const btnList = document.getElementById('btn-view-list');
|
||||||
|
|
||||||
|
if (view === 'list') {
|
||||||
|
renderListView();
|
||||||
|
listEl.classList.add('visible');
|
||||||
|
graphWrap.style.display = 'none';
|
||||||
|
btnList.classList.add('active');
|
||||||
|
btnGraph.classList.remove('active');
|
||||||
|
} else {
|
||||||
|
listEl.classList.remove('visible');
|
||||||
|
graphWrap.style.display = '';
|
||||||
|
btnGraph.classList.add('active');
|
||||||
|
btnList.classList.remove('active');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mermaid export ─────────────────────────────────────────────
|
||||||
|
function generateMermaid() {
|
||||||
|
const lines = ['flowchart LR'];
|
||||||
|
// Node definitions
|
||||||
|
for (const node of atlasData.nodes) {
|
||||||
|
const safe = node.id.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
|
const label = node.label.replace(/"/g, "'");
|
||||||
|
lines.push(` ${safe}["${label}"]`);
|
||||||
|
}
|
||||||
|
lines.push('');
|
||||||
|
// Edges
|
||||||
|
for (const edge of atlasData.edges) {
|
||||||
|
const from = edge.from.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
|
const to = edge.to.replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
|
const lbl = edge.label ? ` |${edge.label}|` : '';
|
||||||
|
lines.push(` ${from} -->${lbl} ${to}`);
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// ── Event bindings ─────────────────────────────────────────────
|
// ── Event bindings ─────────────────────────────────────────────
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
document.getElementById('btn-fit').addEventListener('click', fitGraph);
|
document.getElementById('btn-fit').addEventListener('click', fitGraph);
|
||||||
document.getElementById('btn-add-node').addEventListener('click', openAddModal);
|
document.getElementById('btn-add-node').addEventListener('click', openAddModal);
|
||||||
|
document.getElementById('btn-view-graph').addEventListener('click', () => setView('graph'));
|
||||||
|
document.getElementById('btn-view-list').addEventListener('click', () => setView('list'));
|
||||||
|
document.getElementById('btn-mermaid').addEventListener('click', () => {
|
||||||
|
const text = generateMermaid();
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
.then(() => showToast('Mermaid をクリップボードにコピーしました'))
|
||||||
|
.catch(() => prompt('Mermaid をコピーしてください', text));
|
||||||
|
});
|
||||||
document.getElementById('btn-share').addEventListener('click', () => {
|
document.getElementById('btn-share').addEventListener('click', () => {
|
||||||
const url = generateShareURL();
|
const url = generateShareURL();
|
||||||
navigator.clipboard.writeText(url)
|
navigator.clipboard.writeText(url)
|
||||||
|
|
@ -2648,6 +2897,7 @@ function handleImportFile(file, isWizard) {
|
||||||
// ── Init ───────────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────────
|
||||||
loadData().then(() => {
|
loadData().then(() => {
|
||||||
bindEvents();
|
bindEvents();
|
||||||
|
document.getElementById('btn-view-graph').classList.add('active');
|
||||||
// 1. Check for shared atlas in URL hash (read-only mode)
|
// 1. Check for shared atlas in URL hash (read-only mode)
|
||||||
if (checkShareURL()) {
|
if (checkShareURL()) {
|
||||||
buildFilterBar();
|
buildFilterBar();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue