feat: URL share / edge delete / AI context hint
This commit is contained in:
parent
a61ebf7a02
commit
0252ee9315
138
index.html
138
index.html
|
|
@ -680,6 +680,51 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── Share banner ──────────────────────────────────── */
|
||||
#share-banner {
|
||||
position: fixed;
|
||||
top: 52px;
|
||||
left: 0; right: 0;
|
||||
z-index: 15;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 7px 16px;
|
||||
background: rgba(34,211,238,0.08);
|
||||
border-bottom: 1px solid rgba(34,211,238,0.18);
|
||||
font-size: 12px;
|
||||
color: var(--accent);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
}
|
||||
#share-banner[hidden] { display: none; }
|
||||
.has-banner #graph-wrap { top: 88px; }
|
||||
.has-banner #filter-bar { top: 96px; }
|
||||
|
||||
/* ── Edge delete button ─────────────────────────────── */
|
||||
.conn-delete-btn {
|
||||
flex-shrink: 0;
|
||||
width: 20px; height: 20px;
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text3);
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s, background 0.15s, color 0.15s;
|
||||
padding: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
.conn-item:hover .conn-delete-btn { opacity: 1; }
|
||||
.conn-delete-btn:hover {
|
||||
background: rgba(239,68,68,0.15);
|
||||
color: #F87171;
|
||||
}
|
||||
|
||||
/* ── Edge label tooltip ────────────────────────────── */
|
||||
#edge-tooltip {
|
||||
position: fixed;
|
||||
|
|
@ -740,6 +785,13 @@
|
|||
</aside>
|
||||
<div class="overlay" id="overlay" aria-hidden="true"></div>
|
||||
|
||||
<!-- Share banner (read-only mode) -->
|
||||
<div id="share-banner" hidden>
|
||||
<i data-lucide="eye" style="width:13px;height:13px;stroke-width:1.75"></i>
|
||||
<span>シェアビュー(読み取り専用)</span>
|
||||
<button id="share-banner-own-btn" style="margin-left:12px;font-size:11px;padding:3px 10px;border-radius:8px;background:var(--accent-dim);border:1px solid rgba(34,211,238,0.3);color:var(--accent);cursor:pointer">自分のデータを開く</button>
|
||||
</div>
|
||||
|
||||
<!-- Aurora -->
|
||||
<div class="aurora-bg" aria-hidden="true">
|
||||
<div class="aurora-blob aurora-blob-1"></div>
|
||||
|
|
@ -909,6 +961,9 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding:8px 16px;font-size:11px;color:var(--text3);border-bottom:1px solid var(--border);line-height:1.6">
|
||||
Claude や Gemini のチャットに貼り付けると、AI があなたの環境を即座に把握します
|
||||
</div>
|
||||
<pre id="ai-context-text"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -918,6 +973,9 @@
|
|||
<button class="tb-btn accent-btn" id="btn-ai" aria-label="AI Context を生成" title="AI Context">
|
||||
<i data-lucide="brain-circuit" style="width:17px;height:17px;stroke-width:1.5"></i>
|
||||
</button>
|
||||
<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>
|
||||
</button>
|
||||
<div class="tb-divider"></div>
|
||||
<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>
|
||||
|
|
@ -1017,6 +1075,9 @@ let hiddenTypes = new Set();
|
|||
let simulation = null;
|
||||
let svgZoom = null;
|
||||
let svgG = null;
|
||||
let isReadOnly = false;
|
||||
let currentSimNodes = [];
|
||||
let currentSimEdges = [];
|
||||
|
||||
async function loadData() {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
|
|
@ -1073,12 +1134,14 @@ function initGraph() {
|
|||
|
||||
// Clone objects for simulation (D3 mutates them)
|
||||
const simNodes = nodes.map(n => ({ ...n }));
|
||||
currentSimNodes = simNodes;
|
||||
const nodeMap = new Map(simNodes.map(n => [n.id, n]));
|
||||
const simEdges = edges.map(e => ({
|
||||
...e,
|
||||
source: nodeMap.get(e.from) || e.from,
|
||||
target: nodeMap.get(e.to) || e.to,
|
||||
}));
|
||||
currentSimEdges = simEdges;
|
||||
|
||||
// Simulation
|
||||
simulation = d3.forceSimulation(simNodes)
|
||||
|
|
@ -1272,11 +1335,16 @@ function showDetail(id, simNodes, simEdges) {
|
|||
const otherId = isOut ? e.to : e.from;
|
||||
const other = atlasData.nodes.find(n => n.id === otherId);
|
||||
const color = TYPE_COLORS[other?.type] || '#9CA3AF';
|
||||
const delBtn = isReadOnly ? '' :
|
||||
`<button class="conn-delete-btn" onclick="deleteEdge('${e.from}','${e.to}')" aria-label="接続を削除" title="削除">
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>`;
|
||||
return `<div class="conn-item">
|
||||
<span class="conn-dot" style="background:${color}"></span>
|
||||
<span>${other?.label || otherId}</span>
|
||||
${e.label ? `<span style="font-size:10px;color:var(--text3)">${e.label}</span>` : ''}
|
||||
<span class="conn-dir">${isOut ? 'out' : 'in'}</span>
|
||||
<span class="conn-dir" style="margin-left:auto;margin-right:4px">${isOut ? 'out' : 'in'}</span>
|
||||
${delBtn}
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
|
@ -1505,6 +1573,19 @@ function exportJson() {
|
|||
function bindEvents() {
|
||||
document.getElementById('btn-fit').addEventListener('click', fitGraph);
|
||||
document.getElementById('btn-add-node').addEventListener('click', openAddModal);
|
||||
document.getElementById('btn-share').addEventListener('click', () => {
|
||||
const url = generateShareURL();
|
||||
navigator.clipboard.writeText(url)
|
||||
.then(() => showToast('シェア URL をコピーしました'))
|
||||
.catch(() => {
|
||||
// Fallback: prompt
|
||||
prompt('シェア URL をコピーしてください', url);
|
||||
});
|
||||
});
|
||||
document.getElementById('share-banner-own-btn').addEventListener('click', () => {
|
||||
history.replaceState(null, '', location.pathname);
|
||||
location.reload();
|
||||
});
|
||||
document.getElementById('btn-ai').addEventListener('click', () => {
|
||||
document.getElementById('ai-context-text').textContent = generateAIContext();
|
||||
openModal('ai-modal-overlay');
|
||||
|
|
@ -1591,6 +1672,52 @@ if ('serviceWorker' in navigator) {
|
|||
navigator.serviceWorker.register('/sw.js');
|
||||
}
|
||||
|
||||
// ── URL Sharing ────────────────────────────────────────────────
|
||||
function generateShareURL() {
|
||||
const json = JSON.stringify(atlasData);
|
||||
const b64 = btoa(unescape(encodeURIComponent(json)));
|
||||
return `${location.origin}${location.pathname}#atlas=${b64}`;
|
||||
}
|
||||
|
||||
function checkShareURL() {
|
||||
const hash = location.hash;
|
||||
if (!hash.startsWith('#atlas=')) return false;
|
||||
try {
|
||||
const b64 = hash.slice(7);
|
||||
const json = decodeURIComponent(escape(atob(b64)));
|
||||
const data = JSON.parse(json);
|
||||
if (!Array.isArray(data.nodes) || !Array.isArray(data.edges)) return false;
|
||||
atlasData = data;
|
||||
isReadOnly = true;
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function applyReadOnly() {
|
||||
const banner = document.getElementById('share-banner');
|
||||
banner.removeAttribute('hidden');
|
||||
document.body.classList.add('has-banner');
|
||||
if (window.lucide) lucide.createIcons({ nodes: [banner] });
|
||||
document.getElementById('btn-add-node').style.display = 'none';
|
||||
}
|
||||
|
||||
// ── Edge delete ────────────────────────────────────────────────
|
||||
function deleteEdge(from, to) {
|
||||
if (isReadOnly) return;
|
||||
atlasData.edges = atlasData.edges.filter(e => !(e.from === from && e.to === to));
|
||||
saveData();
|
||||
const keepId = selectedNodeId;
|
||||
initGraph();
|
||||
if (keepId) {
|
||||
selectedNodeId = keepId;
|
||||
updateHighlight(currentSimNodes, currentSimEdges);
|
||||
showDetail(keepId, currentSimNodes, currentSimEdges);
|
||||
}
|
||||
showToast('接続を削除しました');
|
||||
}
|
||||
|
||||
// ── Wizard ─────────────────────────────────────────────────────
|
||||
function showWizard() {
|
||||
const overlay = document.getElementById('wizard-overlay');
|
||||
|
|
@ -1640,6 +1767,15 @@ function handleImportFile(file, isWizard) {
|
|||
// ── Init ───────────────────────────────────────────────────────
|
||||
loadData().then(() => {
|
||||
bindEvents();
|
||||
// 1. Check for shared atlas in URL hash (read-only mode)
|
||||
if (checkShareURL()) {
|
||||
buildFilterBar();
|
||||
initGraph();
|
||||
applyReadOnly();
|
||||
setTimeout(fitGraph, 800);
|
||||
return;
|
||||
}
|
||||
// 2. Normal flow
|
||||
if (!atlasData) {
|
||||
showWizard();
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in New Issue