issue #1202: add design canvas prototype (HTML artboard)
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-08-19 00:41:56 +00:00
parent 39f91e1e23
commit ec43813c91
+218
View File
@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Design Canvas — プロトタイプ</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #1a1a2e; color: #e0e0e0; font-family: 'Segoe UI', system-ui, sans-serif; }
#toolbar { position: fixed; top: 0; left: 0; right: 0; height: 48px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; gap: 12px; padding: 0 16px; z-index: 100; }
#toolbar h1 { font-size: 15px; font-weight: 600; color: #a0d2ff; white-space: nowrap; }
.toolbar-btn { background: #0f3460; color: #e0e0e0; border: 1px solid #1a5276; padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; transition: background 0.15s; }
.toolbar-btn:hover { background: #1a5276; }
.toolbar-btn.primary { background: #e94560; border-color: #e94560; color: #fff; }
.toolbar-btn.primary:hover { background: #c73e54; }
#viewport { position: fixed; top: 48px; left: 0; right: 0; bottom: 0; overflow: hidden; cursor: grab; }
#viewport.grabbing { cursor: grabbing; }
#canvas { position: absolute; top: 0; left: 0; transform-origin: 0 0; width: 2000px; height: 1400px; background: #0f0f1a; }
#artboard { width: 960px; height: 600px; margin: 60px; background: #ffffff; border-radius: 4px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); position: relative; overflow: hidden; }
#properties-panel { position: fixed; top: 48px; right: 0; width: 280px; bottom: 0; background: #16213e; border-left: 1px solid #0f3460; padding: 16px; overflow-y: auto; transform: translateX(100%); transition: transform 0.2s ease; z-index: 90; }
#properties-panel.open { transform: translateX(0); }
#properties-panel h2 { font-size: 14px; color: #a0d2ff; margin-bottom: 12px; }
.prop-row { margin-bottom: 10px; }
.prop-row label { display: block; font-size: 11px; color: #888; margin-bottom: 3px; }
.prop-row input[type="text"], .prop-row input[type="number"], .prop-row textarea { width: 100%; background: #0f3460; border: 1px solid #1a5276; color: #e0e0e0; padding: 6px 8px; border-radius: 4px; font-size: 13px; }
.prop-row input[type="color"] { width: 100%; height: 32px; border: none; background: none; cursor: pointer; }
#zoom-indicator { position: fixed; bottom: 16px; left: 16px; background: rgba(22,33,62,0.9); padding: 6px 12px; border-radius: 4px; font-size: 12px; color: #a0d2ff; z-index: 100; }
[data-editable] { cursor: pointer; outline: 1px dashed transparent; transition: outline 0.1s; }
[data-editable]:hover { outline: 1px dashed #e94560; }
[data-editable].selected { outline: 2px solid #e94560; }
.slide-header { position: absolute; top: 0; left: 0; right: 0; height: 80px; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); display: flex; align-items: center; padding: 0 32px; }
.slide-header h2 { color: #ffffff; font-size: 28px; font-weight: 700; }
.slide-header .page-num { margin-left: auto; color: #e94560; font-size: 14px; font-weight: 600; }
.kpi-grid { position: absolute; top: 100px; left: 32px; right: 32px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
.kpi-card { background: #f8f9fa; border-radius: 8px; padding: 16px; border-left: 4px solid #3498db; }
.kpi-card.warning { border-left-color: #f39c12; }
.kpi-card.success { border-left-color: #27ae60; }
.kpi-card.danger { border-left-color: #e74c3c; }
.kpi-label { font-size: 11px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
.kpi-value { font-size: 28px; font-weight: 700; color: #2c3e50; }
.kpi-delta { font-size: 12px; margin-top: 4px; color: #27ae60; }
.kpi-delta.negative { color: #e74c3c; }
.chart-area { position: absolute; top: 260px; left: 32px; right: 32px; bottom: 32px; background: #f8f9fa; border-radius: 8px; padding: 20px; }
.chart-area h3 { font-size: 14px; color: #2c3e50; margin-bottom: 12px; }
.bar-chart { display: flex; align-items: flex-end; gap: 8px; height: 200px; padding-top: 20px; }
.bar { flex: 1; background: #3498db; border-radius: 3px 3px 0 0; position: relative; transition: background 0.2s; }
.bar:hover { background: #2980b9; }
.bar-label { position: absolute; bottom: -20px; left: 0; right: 0; text-align: center; font-size: 10px; color: #888; }
.bar-value { position: absolute; top: -20px; left: 0; right: 0; text-align: center; font-size: 11px; font-weight: 600; color: #2c3e50; }
.legend { display: flex; gap: 16px; margin-top: 24px; }
.legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: #555; }
.legend-dot { width: 10px; height: 10px; border-radius: 2px; }
#toast { position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%) translateY(20px); background: #27ae60; color: #fff; padding: 10px 24px; border-radius: 6px; font-size: 13px; opacity: 0; transition: opacity 0.2s, transform 0.2s; z-index: 200; pointer-events: none; }
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
</style>
</head>
<body>
<div id="toolbar">
<h1>Design Canvas</h1>
<span style="font-size:12px;color:#666;">スライド1/10 — デザイン確認</span>
<div style="flex:1"></div>
<button class="toolbar-btn" onclick="resetView()">リセット</button>
<button class="toolbar-btn" onclick="exportJSON()">JSON エクスポート</button>
<button class="toolbar-btn primary" onclick="confirmSlide()">これでOK →</button>
</div>
<div id="viewport">
<div id="canvas">
<div id="artboard">
<div class="slide-header">
<h2 data-editable data-label="タイトル" data-property="title">月次経営ダッシュボード</h2>
<span class="page-num" data-editable data-label="ページ番号" data-property="page">01</span>
</div>
<div class="kpi-grid">
<div class="kpi-card" data-editable data-label="KPI1" data-property="kpi1">
<div class="kpi-label">売上高</div>
<div class="kpi-value">12.4億円</div>
<div class="kpi-delta">+8.2% 前月比</div>
</div>
<div class="kpi-card success" data-editable data-label="KPI2" data-property="kpi2">
<div class="kpi-label">粗利率</div>
<div class="kpi-value">42.3%</div>
<div class="kpi-delta">+1.2pt 前月比</div>
</div>
<div class="kpi-card warning" data-editable data-label="KPI3" data-property="kpi3">
<div class="kpi-label">顧客数</div>
<div class="kpi-value">1,847</div>
<div class="kpi-delta">+3.1% 前月比</div>
</div>
<div class="kpi-card danger" data-editable data-label="KPI4" data-property="kpi4">
<div class="kpi-label">離脱率</div>
<div class="kpi-value">2.8%</div>
<div class="kpi-delta negative">+0.4pt 前月比</div>
</div>
</div>
<div class="chart-area">
<h3 data-editable data-label="チャートタイトル" data-property="chartTitle">月次売上推移(直近6ヶ月)</h3>
<div class="bar-chart" data-editable data-label="チャート" data-property="chart">
<div class="bar" style="height:45%"><span class="bar-value">8.2</span><span class="bar-label">3月</span></div>
<div class="bar" style="height:52%"><span class="bar-value">9.1</span><span class="bar-label">4月</span></div>
<div class="bar" style="height:58%"><span class="bar-value">10.3</span><span class="bar-label">5月</span></div>
<div class="bar" style="height:65%"><span class="bar-value">11.5</span><span class="bar-label">6月</span></div>
<div class="bar" style="height:72%"><span class="bar-value">11.9</span><span class="bar-label">7月</span></div>
<div class="bar" style="height:100%;background:#27ae60"><span class="bar-value">12.4</span><span class="bar-label">8月</span></div>
</div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#3498db"></div>通常月</div>
<div class="legend-item"><div class="legend-dot" style="background:#27ae60"></div>当月</div>
</div>
</div>
</div>
</div>
</div>
<div id="properties-panel">
<h2>プロパティ編集</h2>
<div id="prop-contents">
<p style="font-size:12px;color:#666;">要素をクリックして選択してください。</p>
</div>
</div>
<div id="zoom-indicator">100%</div>
<div id="toast"></div>
<script>
(function(){
let panX = 0, panY = 0, scale = 1, isPanning = false, startX = 0, startY = 0;
const viewport = document.getElementById('viewport');
const canvas = document.getElementById('canvas');
const zoomInd = document.getElementById('zoom-indicator');
const panel = document.getElementById('properties-panel');
const propContents = document.getElementById('prop-contents');
let selectedEl = null;
function updateTransform() {
canvas.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
zoomInd.textContent = Math.round(scale * 100) + '%';
}
viewport.addEventListener('mousedown', e => {
if (e.target.closest('[data-editable]') || e.target.closest('#properties-panel')) return;
isPanning = true;
startX = e.clientX - panX;
startY = e.clientY - panY;
viewport.classList.add('grabbing');
if (selectedEl) { selectedEl.classList.remove('selected'); selectedEl = null; panel.classList.remove('open'); }
});
window.addEventListener('mousemove', e => {
if (!isPanning) return;
panX = e.clientX - startX;
panY = e.clientY - startY;
updateTransform();
});
window.addEventListener('mouseup', () => { isPanning = false; viewport.classList.remove('grabbing'); });
viewport.addEventListener('wheel', e => {
e.preventDefault();
const delta = e.deltaY > 0 ? 0.9 : 1.1;
scale = Math.max(0.25, Math.min(4, scale * delta));
updateTransform();
}, { passive: false });
document.querySelectorAll('[data-editable]').forEach(el => {
el.addEventListener('click', e => {
e.stopPropagation();
if (selectedEl) selectedEl.classList.remove('selected');
selectedEl = el;
el.classList.add('selected');
const label = el.dataset.label || el.dataset.property;
const prop = el.dataset.property;
propContents.innerHTML = `
<div class="prop-row"><label>要素名</label><input type="text" value="${label}" disabled></div>
<div class="prop-row"><label>テキスト内容</label><textarea rows="3">${el.textContent.trim()}</textarea></div>
<div class="prop-row"><label>背景色</label><input type="color" value="#ffffff"></div>
<div class="prop-row"><label>文字色</label><input type="color" value="#2c3e50"></div>
<div class="prop-row"><label>フォントサイズ</label><input type="number" value="16"></div>
<button class="toolbar-btn primary" style="width:100%;margin-top:12px" onclick="showToast('適用されました')">適用</button>
`;
panel.classList.add('open');
});
});
window.resetView = function() {
scale = 1; panX = 0; panY = 0;
updateTransform();
};
window.exportJSON = function() {
const data = {};
document.querySelectorAll('[data-property]').forEach(el => {
data[el.dataset.property] = el.textContent.trim();
});
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'slide-1-design.json';
a.click();
showToast('JSON をエクスポートしました');
};
window.confirmSlide = function() {
showToast('スライド1 確定 → PPTX 生成に進みます');
};
window.showToast = function(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
setTimeout(() => t.classList.remove('show'), 2000);
};
// Center artboard on load
const vw = viewport.clientWidth, vh = viewport.clientHeight;
panX = (vw - 960) / 2 - 60;
panY = (vh - 600) / 2 - 60;
updateTransform();
})();
</script>
</body>
</html>