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;
|
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 label tooltip ────────────────────────────── */
|
||||||
#edge-tooltip {
|
#edge-tooltip {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|
@ -740,6 +785,13 @@
|
||||||
</aside>
|
</aside>
|
||||||
<div class="overlay" id="overlay" aria-hidden="true"></div>
|
<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 -->
|
<!-- Aurora -->
|
||||||
<div class="aurora-bg" aria-hidden="true">
|
<div class="aurora-bg" aria-hidden="true">
|
||||||
<div class="aurora-blob aurora-blob-1"></div>
|
<div class="aurora-blob aurora-blob-1"></div>
|
||||||
|
|
@ -909,6 +961,9 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
<pre id="ai-context-text"></pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -918,6 +973,9 @@
|
||||||
<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">
|
||||||
<i data-lucide="brain-circuit" style="width:17px;height:17px;stroke-width:1.5"></i>
|
<i data-lucide="brain-circuit" style="width:17px;height:17px;stroke-width:1.5"></i>
|
||||||
</button>
|
</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>
|
<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>
|
||||||
|
|
@ -1017,6 +1075,9 @@ let hiddenTypes = new Set();
|
||||||
let simulation = null;
|
let simulation = null;
|
||||||
let svgZoom = null;
|
let svgZoom = null;
|
||||||
let svgG = null;
|
let svgG = null;
|
||||||
|
let isReadOnly = false;
|
||||||
|
let currentSimNodes = [];
|
||||||
|
let currentSimEdges = [];
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY);
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
|
@ -1073,12 +1134,14 @@ function initGraph() {
|
||||||
|
|
||||||
// Clone objects for simulation (D3 mutates them)
|
// Clone objects for simulation (D3 mutates them)
|
||||||
const simNodes = nodes.map(n => ({ ...n }));
|
const simNodes = nodes.map(n => ({ ...n }));
|
||||||
|
currentSimNodes = simNodes;
|
||||||
const nodeMap = new Map(simNodes.map(n => [n.id, n]));
|
const nodeMap = new Map(simNodes.map(n => [n.id, n]));
|
||||||
const simEdges = edges.map(e => ({
|
const simEdges = edges.map(e => ({
|
||||||
...e,
|
...e,
|
||||||
source: nodeMap.get(e.from) || e.from,
|
source: nodeMap.get(e.from) || e.from,
|
||||||
target: nodeMap.get(e.to) || e.to,
|
target: nodeMap.get(e.to) || e.to,
|
||||||
}));
|
}));
|
||||||
|
currentSimEdges = simEdges;
|
||||||
|
|
||||||
// Simulation
|
// Simulation
|
||||||
simulation = d3.forceSimulation(simNodes)
|
simulation = d3.forceSimulation(simNodes)
|
||||||
|
|
@ -1272,11 +1335,16 @@ function showDetail(id, simNodes, simEdges) {
|
||||||
const otherId = isOut ? e.to : e.from;
|
const otherId = isOut ? e.to : e.from;
|
||||||
const other = atlasData.nodes.find(n => n.id === otherId);
|
const other = atlasData.nodes.find(n => n.id === otherId);
|
||||||
const color = TYPE_COLORS[other?.type] || '#9CA3AF';
|
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">
|
return `<div class="conn-item">
|
||||||
<span class="conn-dot" style="background:${color}"></span>
|
<span class="conn-dot" style="background:${color}"></span>
|
||||||
<span>${other?.label || otherId}</span>
|
<span>${other?.label || otherId}</span>
|
||||||
${e.label ? `<span style="font-size:10px;color:var(--text3)">${e.label}</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>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
@ -1505,6 +1573,19 @@ function exportJson() {
|
||||||
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-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('btn-ai').addEventListener('click', () => {
|
||||||
document.getElementById('ai-context-text').textContent = generateAIContext();
|
document.getElementById('ai-context-text').textContent = generateAIContext();
|
||||||
openModal('ai-modal-overlay');
|
openModal('ai-modal-overlay');
|
||||||
|
|
@ -1591,6 +1672,52 @@ if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.register('/sw.js');
|
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 ─────────────────────────────────────────────────────
|
// ── Wizard ─────────────────────────────────────────────────────
|
||||||
function showWizard() {
|
function showWizard() {
|
||||||
const overlay = document.getElementById('wizard-overlay');
|
const overlay = document.getElementById('wizard-overlay');
|
||||||
|
|
@ -1640,6 +1767,15 @@ function handleImportFile(file, isWizard) {
|
||||||
// ── Init ───────────────────────────────────────────────────────
|
// ── Init ───────────────────────────────────────────────────────
|
||||||
loadData().then(() => {
|
loadData().then(() => {
|
||||||
bindEvents();
|
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) {
|
if (!atlasData) {
|
||||||
showWizard();
|
showWizard();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue