Files
Claude-Code-Workflow/.claude/workflows/_template-compare-matrix.html
catlog22 b27708a7da feat: v4.1.0 - UI design workflow matrix mode refactoring
## Core Changes
- Pure matrix mode (style × layout combinations)
- Removed standard/creative mode selection
- Simplified parameters: --style-variants, --layout-variants
- Path corrections: standalone mode uses .workflow/.scratchpad/

## Modified Files
- auto.md: Simplified Phase 3, corrected paths
- generate.md: Complete rewrite for matrix-only mode
- consolidate.md: Path corrections for standalone mode
- extract.md: Path corrections for standalone mode

## New Files
- CHANGELOG-v4.1.0.md: Complete changelog
- _template-compare-matrix.html: Interactive visualization template

## Breaking Changes
- Deprecated: --variants, --creative-variants
- New: --style-variants, --layout-variants
- File naming: {page}-style-{s}-layout-{l}.html

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-09 10:52:34 +08:00

693 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UI Design Matrix Comparison - {{run_id}}</title>
<style>
:root {
--color-primary: #2563eb;
--color-bg: #f9fafb;
--color-surface: #ffffff;
--color-border: #e5e7eb;
--color-text: #1f2937;
--color-text-secondary: #6b7280;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--color-bg);
color: var(--color-text);
line-height: 1.6;
}
.container {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
header {
background: var(--color-surface);
padding: 1.5rem 2rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
h1 {
color: var(--color-primary);
font-size: 1.875rem;
margin-bottom: 0.5rem;
}
.meta {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
.controls {
background: var(--color-surface);
padding: 1.5rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
display: flex;
gap: 1.5rem;
align-items: center;
flex-wrap: wrap;
}
.control-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
label {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-secondary);
}
select, button {
padding: 0.5rem 1rem;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
font-size: 0.875rem;
background: white;
cursor: pointer;
}
select:focus, button:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
button {
background: var(--color-primary);
color: white;
border: none;
font-weight: 500;
transition: background 0.2s;
}
button:hover {
background: #1d4ed8;
}
.matrix-container {
background: var(--color-surface);
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.matrix-table {
width: 100%;
border-collapse: collapse;
}
.matrix-table th,
.matrix-table td {
border: 1px solid var(--color-border);
padding: 0.75rem;
text-align: center;
}
.matrix-table th {
background: #f3f4f6;
font-weight: 600;
color: var(--color-text);
}
.matrix-table thead th {
background: var(--color-primary);
color: white;
}
.matrix-table tbody th {
background: #f9fafb;
font-weight: 600;
}
.prototype-cell {
position: relative;
padding: 0;
height: 400px;
vertical-align: top;
}
.prototype-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
.prototype-header {
padding: 0.5rem;
background: #f9fafb;
border-bottom: 1px solid var(--color-border);
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.prototype-title {
font-size: 0.75rem;
font-weight: 500;
color: var(--color-text-secondary);
}
.prototype-actions {
display: flex;
gap: 0.25rem;
}
.icon-btn {
width: 24px;
height: 24px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
border-radius: 0.25rem;
}
.icon-btn:hover {
background: #e5e7eb;
color: var(--color-text);
}
.icon-btn.selected {
background: var(--color-primary);
color: white;
}
.prototype-iframe-container {
flex: 1;
position: relative;
overflow: hidden;
}
.prototype-iframe {
width: 100%;
height: 100%;
border: none;
transform-origin: top left;
}
.zoom-info {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
background: rgba(0,0,0,0.7);
color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
font-size: 0.75rem;
pointer-events: none;
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
border-bottom: 2px solid var(--color-border);
}
.tab {
padding: 0.75rem 1.5rem;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-weight: 500;
color: var(--color-text-secondary);
margin-bottom: -2px;
}
.tab.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.fullscreen-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.95);
z-index: 1000;
padding: 2rem;
}
.fullscreen-overlay.active {
display: flex;
flex-direction: column;
}
.fullscreen-header {
color: white;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.fullscreen-iframe {
flex: 1;
border: none;
background: white;
border-radius: 0.5rem;
}
.close-btn {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
cursor: pointer;
}
.close-btn:hover {
background: rgba(255,255,255,0.3);
}
.selection-summary {
background: #fef3c7;
border-left: 4px solid #f59e0b;
padding: 1rem;
margin-bottom: 2rem;
border-radius: 0.375rem;
}
.selection-summary h3 {
color: #92400e;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.selection-list {
list-style: none;
color: #78350f;
font-size: 0.875rem;
}
.selection-list li {
padding: 0.25rem 0;
}
@media (max-width: 1200px) {
.prototype-cell {
height: 300px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🎨 UI Design Matrix Comparison</h1>
<div class="meta">
<strong>Run ID:</strong> {{run_id}} |
<strong>Session:</strong> {{session_id}} |
<strong>Generated:</strong> {{timestamp}}
</div>
</header>
<div class="controls">
<div class="control-group">
<label for="page-select">Page:</label>
<select id="page-select">
<!-- Populated by JavaScript -->
</select>
</div>
<div class="control-group">
<label for="zoom-level">Zoom:</label>
<select id="zoom-level">
<option value="0.25">25%</option>
<option value="0.5">50%</option>
<option value="0.75">75%</option>
<option value="1" selected>100%</option>
</select>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button id="sync-scroll-toggle">🔗 Sync Scroll: ON</button>
</div>
<div class="control-group">
<label>&nbsp;</label>
<button id="export-selection">📥 Export Selection</button>
</div>
</div>
<div id="selection-summary" class="selection-summary" style="display:none">
<h3>Selected Prototypes (<span id="selection-count">0</span>)</h3>
<ul id="selection-list" class="selection-list"></ul>
</div>
<div class="tabs">
<button class="tab active" data-tab="matrix">Matrix View</button>
<button class="tab" data-tab="comparison">Side-by-Side</button>
<button class="tab" data-tab="runs">Compare Runs</button>
</div>
<div class="tab-content active" data-content="matrix">
<div class="matrix-container">
<table class="matrix-table">
<thead>
<tr>
<th>Style ↓ / Layout →</th>
<th>Layout 1</th>
<th>Layout 2</th>
<th>Layout 3</th>
</tr>
</thead>
<tbody id="matrix-body">
<!-- Populated by JavaScript -->
</tbody>
</table>
</div>
</div>
<div class="tab-content" data-content="comparison">
<p>Select two prototypes from the matrix to compare side-by-side.</p>
<div id="comparison-view"></div>
</div>
<div class="tab-content" data-content="runs">
<p>Compare the same prototype across different runs.</p>
<div id="runs-comparison"></div>
</div>
</div>
<div id="fullscreen-overlay" class="fullscreen-overlay">
<div class="fullscreen-header">
<h2 id="fullscreen-title"></h2>
<button class="close-btn" onclick="closeFullscreen()">✕ Close</button>
</div>
<iframe id="fullscreen-iframe" class="fullscreen-iframe"></iframe>
</div>
<script>
// Configuration - Replace with actual values during generation
const CONFIG = {
runId: "{{run_id}}",
sessionId: "{{session_id}}",
styleVariants: {{style_variants}}, // e.g., 3
layoutVariants: {{layout_variants}}, // e.g., 3
pages: {{pages_json}}, // e.g., ["dashboard", "auth"]
basePath: "." // Relative path to prototypes
};
// State
let state = {
currentPage: CONFIG.pages[0],
zoomLevel: 1,
syncScroll: true,
selected: new Set(),
fullscreenSrc: null
};
// Initialize
document.addEventListener('DOMContentLoaded', () => {
populatePageSelect();
renderMatrix();
setupEventListeners();
loadSelectionFromStorage();
});
function populatePageSelect() {
const select = document.getElementById('page-select');
CONFIG.pages.forEach(page => {
const option = document.createElement('option');
option.value = page;
option.textContent = capitalize(page);
select.appendChild(option);
});
}
function renderMatrix() {
const tbody = document.getElementById('matrix-body');
tbody.innerHTML = '';
for (let s = 1; s <= CONFIG.styleVariants; s++) {
const row = document.createElement('tr');
// Style header cell
const headerCell = document.createElement('th');
headerCell.textContent = `Style ${s}`;
row.appendChild(headerCell);
// Prototype cells for each layout
for (let l = 1; l <= CONFIG.layoutVariants; l++) {
const cell = document.createElement('td');
cell.className = 'prototype-cell';
const filename = `${state.currentPage}-style-${s}-layout-${l}.html`;
const id = `${state.currentPage}-s${s}-l${l}`;
cell.innerHTML = `
<div class="prototype-wrapper">
<div class="prototype-header">
<span class="prototype-title">S${s}L${l}</span>
<div class="prototype-actions">
<button class="icon-btn select-btn" data-id="${id}" title="Select">
${state.selected.has(id) ? '★' : '☆'}
</button>
<button class="icon-btn" onclick="openFullscreen('${filename}', 'Style ${s} Layout ${l}')" title="Fullscreen">
</button>
<button class="icon-btn" onclick="openInNewTab('${filename}')" title="Open in new tab">
</button>
</div>
</div>
<div class="prototype-iframe-container">
<iframe
class="prototype-iframe"
src="${filename}"
data-cell="s${s}-l${l}"
style="transform: scale(${state.zoomLevel});"
></iframe>
<div class="zoom-info">${Math.round(state.zoomLevel * 100)}%</div>
</div>
</div>
`;
row.appendChild(cell);
}
tbody.appendChild(row);
}
// Re-attach selection event listeners
document.querySelectorAll('.select-btn').forEach(btn => {
btn.addEventListener('click', (e) => toggleSelection(e.target.dataset.id, e.target));
});
// Setup scroll sync
if (state.syncScroll) {
setupScrollSync();
}
}
function setupScrollSync() {
const iframes = document.querySelectorAll('.prototype-iframe');
let isScrolling = false;
iframes.forEach(iframe => {
iframe.addEventListener('load', () => {
const iframeWindow = iframe.contentWindow;
iframe.contentDocument.addEventListener('scroll', (e) => {
if (!state.syncScroll || isScrolling) return;
isScrolling = true;
const scrollTop = iframe.contentDocument.documentElement.scrollTop;
const scrollLeft = iframe.contentDocument.documentElement.scrollLeft;
iframes.forEach(otherIframe => {
if (otherIframe !== iframe && otherIframe.contentDocument) {
otherIframe.contentDocument.documentElement.scrollTop = scrollTop;
otherIframe.contentDocument.documentElement.scrollLeft = scrollLeft;
}
});
setTimeout(() => { isScrolling = false; }, 50);
});
});
});
}
function setupEventListeners() {
// Page selector
document.getElementById('page-select').addEventListener('change', (e) => {
state.currentPage = e.target.value;
renderMatrix();
});
// Zoom level
document.getElementById('zoom-level').addEventListener('change', (e) => {
state.zoomLevel = parseFloat(e.target.value);
renderMatrix();
});
// Sync scroll toggle
document.getElementById('sync-scroll-toggle').addEventListener('click', (e) => {
state.syncScroll = !state.syncScroll;
e.target.textContent = `🔗 Sync Scroll: ${state.syncScroll ? 'ON' : 'OFF'}`;
if (state.syncScroll) setupScrollSync();
});
// Export selection
document.getElementById('export-selection').addEventListener('click', exportSelection);
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const tabName = e.target.dataset.tab;
switchTab(tabName);
});
});
}
function toggleSelection(id, btn) {
if (state.selected.has(id)) {
state.selected.delete(id);
btn.textContent = '☆';
btn.classList.remove('selected');
} else {
state.selected.add(id);
btn.textContent = '★';
btn.classList.add('selected');
}
updateSelectionSummary();
saveSelectionToStorage();
}
function updateSelectionSummary() {
const summary = document.getElementById('selection-summary');
const count = document.getElementById('selection-count');
const list = document.getElementById('selection-list');
count.textContent = state.selected.size;
if (state.selected.size > 0) {
summary.style.display = 'block';
list.innerHTML = Array.from(state.selected)
.map(id => `<li>${id}</li>`)
.join('');
} else {
summary.style.display = 'none';
}
}
function saveSelectionToStorage() {
localStorage.setItem(`selection-${CONFIG.runId}`, JSON.stringify(Array.from(state.selected)));
}
function loadSelectionFromStorage() {
const stored = localStorage.getItem(`selection-${CONFIG.runId}`);
if (stored) {
state.selected = new Set(JSON.parse(stored));
updateSelectionSummary();
}
}
function exportSelection() {
const report = {
runId: CONFIG.runId,
sessionId: CONFIG.sessionId,
timestamp: new Date().toISOString(),
selections: Array.from(state.selected).map(id => ({
id,
file: `${id.replace(/-s(\d+)-l(\d+)/, '-style-$1-layout-$2')}.html`
}))
};
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `selection-${CONFIG.runId}.json`;
a.click();
URL.revokeObjectURL(url);
alert(`Exported ${state.selected.size} selections to selection-${CONFIG.runId}.json`);
}
function openFullscreen(src, title) {
const overlay = document.getElementById('fullscreen-overlay');
const iframe = document.getElementById('fullscreen-iframe');
const titleEl = document.getElementById('fullscreen-title');
iframe.src = src;
titleEl.textContent = title;
overlay.classList.add('active');
state.fullscreenSrc = src;
}
function closeFullscreen() {
const overlay = document.getElementById('fullscreen-overlay');
const iframe = document.getElementById('fullscreen-iframe');
overlay.classList.remove('active');
iframe.src = '';
state.fullscreenSrc = null;
}
function openInNewTab(src) {
window.open(src, '_blank');
}
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === tabName);
});
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.toggle('active', content.dataset.content === tabName);
});
}
function capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
// Close fullscreen on ESC key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && state.fullscreenSrc) {
closeFullscreen();
}
});
</script>
</body>
</html>