design_canvas.jsx
1 /** 2 * DesignCanvas — Variation side-by-side grid layout 3 * 4 * Displays 2+ static design variations for comparison and selection. 5 * Each variation has a label and zoom-on-hover. 6 * 7 * Usage: 8 * <DesignCanvas 9 * title="Hero Section Design Exploration" 10 * subtitle="Comparing 3 directions" 11 * columns={3} 12 * > 13 * <Variation label="Minimal" description="Minimalist restraint"> 14 * <div>...your design 1...</div> 15 * </Variation> 16 * <Variation label="Editorial" description="Magazine editorial style"> 17 * <div>...your design 2...</div> 18 * </Variation> 19 * <Variation label="Brutalist" description="Raw brutalist"> 20 * <div>...your design 3...</div> 21 * </Variation> 22 * </DesignCanvas> 23 * 24 * For use with React+Babel inline. Include in a script tag, then 25 * window.DesignCanvas / window.Variation are available. 26 */ 27 28 const canvasStyles = { 29 container: { 30 minHeight: '100vh', 31 background: '#F5F5F0', 32 padding: '40px 60px', 33 fontFamily: '-apple-system, "SF Pro Text", "PingFang SC", sans-serif', 34 }, 35 header: { 36 marginBottom: 48, 37 maxWidth: 900, 38 }, 39 title: { 40 fontSize: 36, 41 fontWeight: 600, 42 marginBottom: 12, 43 color: '#1A1A1A', 44 letterSpacing: '-0.02em', 45 }, 46 subtitle: { 47 fontSize: 16, 48 color: '#666', 49 lineHeight: 1.5, 50 }, 51 grid: { 52 display: 'grid', 53 gap: 32, 54 }, 55 cell: { 56 display: 'flex', 57 flexDirection: 'column', 58 gap: 12, 59 }, 60 cellHeader: { 61 display: 'flex', 62 alignItems: 'baseline', 63 gap: 12, 64 paddingBottom: 8, 65 borderBottom: '1px solid #E0E0DA', 66 }, 67 label: { 68 fontSize: 14, 69 fontWeight: 600, 70 color: '#1A1A1A', 71 letterSpacing: '-0.01em', 72 }, 73 description: { 74 fontSize: 13, 75 color: '#888', 76 }, 77 frame: { 78 background: '#fff', 79 borderRadius: 4, 80 border: '1px solid #E0E0DA', 81 overflow: 'hidden', 82 position: 'relative', 83 transition: 'transform 0.2s ease, box-shadow 0.2s ease', 84 cursor: 'pointer', 85 }, 86 frameInner: { 87 position: 'relative', 88 width: '100%', 89 }, 90 badge: { 91 position: 'absolute', 92 top: 12, 93 left: 12, 94 background: 'rgba(0, 0, 0, 0.7)', 95 color: '#fff', 96 padding: '3px 8px', 97 borderRadius: 4, 98 fontSize: 11, 99 fontWeight: 500, 100 letterSpacing: '0.5px', 101 textTransform: 'uppercase', 102 zIndex: 10, 103 pointerEvents: 'none', 104 }, 105 }; 106 107 function DesignCanvas({ title, subtitle, columns = 3, children }) { 108 const [expanded, setExpanded] = React.useState(null); 109 110 const gridStyle = { 111 ...canvasStyles.grid, 112 gridTemplateColumns: `repeat(${columns}, 1fr)`, 113 }; 114 115 return ( 116 <div style={canvasStyles.container}> 117 {(title || subtitle) && ( 118 <div style={canvasStyles.header}> 119 {title && <h1 style={canvasStyles.title}>{title}</h1>} 120 {subtitle && <p style={canvasStyles.subtitle}>{subtitle}</p>} 121 </div> 122 )} 123 124 <div style={gridStyle}> 125 {React.Children.map(children, (child, idx) => 126 React.isValidElement(child) 127 ? React.cloneElement(child, { 128 _index: idx, 129 _expanded: expanded === idx, 130 _onToggle: () => setExpanded(expanded === idx ? null : idx), 131 }) 132 : child 133 )} 134 </div> 135 136 {expanded !== null && ( 137 <div 138 onClick={() => setExpanded(null)} 139 style={{ 140 position: 'fixed', 141 inset: 0, 142 background: 'rgba(0, 0, 0, 0.75)', 143 zIndex: 1000, 144 display: 'flex', 145 alignItems: 'center', 146 justifyContent: 'center', 147 padding: 40, 148 cursor: 'zoom-out', 149 }} 150 > 151 <div 152 onClick={e => e.stopPropagation()} 153 style={{ 154 background: '#fff', 155 borderRadius: 8, 156 overflow: 'hidden', 157 maxWidth: '90vw', 158 maxHeight: '90vh', 159 position: 'relative', 160 }} 161 > 162 {React.Children.toArray(children)[expanded]} 163 </div> 164 </div> 165 )} 166 </div> 167 ); 168 } 169 170 function Variation({ label, description, number, children, _index, _expanded, _onToggle, aspectRatio = '4 / 3' }) { 171 const displayNumber = number || String(_index + 1).padStart(2, '0'); 172 173 return ( 174 <div style={canvasStyles.cell}> 175 <div style={canvasStyles.cellHeader}> 176 <span style={{ ...canvasStyles.label, color: '#999', fontFamily: 'ui-monospace, monospace', fontSize: 12 }}> 177 {displayNumber} 178 </span> 179 <span style={canvasStyles.label}>{label}</span> 180 {description && <span style={canvasStyles.description}>— {description}</span>} 181 </div> 182 183 <div 184 onClick={_onToggle} 185 style={{ 186 ...canvasStyles.frame, 187 aspectRatio, 188 }} 189 onMouseEnter={e => { 190 e.currentTarget.style.boxShadow = '0 8px 24px rgba(0,0,0,0.08)'; 191 }} 192 onMouseLeave={e => { 193 e.currentTarget.style.boxShadow = 'none'; 194 }} 195 > 196 <div style={canvasStyles.frameInner}> 197 {children} 198 </div> 199 </div> 200 </div> 201 ); 202 } 203 204 if (typeof window !== 'undefined') { 205 Object.assign(window, { DesignCanvas, Variation }); 206 }