({ preset, onClick })
| 142 | }; |
| 143 | |
| 144 | const PresetTile = ({ preset, onClick }) => { |
| 145 | // Render an accurate, merged preview of the preset (shapes + bridges + merged corners), |
| 146 | // so the thumbnail matches exactly what gets placed on the canvas. |
| 147 | const shapes = preset.build(); |
| 148 | const bridges = computeBridges(shapes, preset.radius, 1); |
| 149 | const corners = computeCornerRadii(shapes, preset.radius, 1); |
| 150 | |
| 151 | let minX = Infinity, |
| 152 | minY = Infinity, |
| 153 | maxX = -Infinity, |
| 154 | maxY = -Infinity; |
| 155 | shapes.forEach(s => { |
| 156 | minX = Math.min(minX, s.x); |
| 157 | minY = Math.min(minY, s.y); |
| 158 | maxX = Math.max(maxX, s.x + s.w); |
| 159 | maxY = Math.max(maxY, s.y + s.h); |
| 160 | }); |
| 161 | const pad = 12; |
| 162 | const vbW = maxX - minX + pad * 2; |
| 163 | const vbH = maxY - minY + pad * 2; |
| 164 | |
| 165 | return ( |
| 166 | <Flex |
| 167 | as="button" |
| 168 | type="button" |
| 169 | direction="column" |
| 170 | align="center" |
| 171 | justify="center" |
| 172 | gap={1} |
| 173 | p={2} |
| 174 | bg="var(--bg-elevated)" |
| 175 | border="1px solid var(--border-primary)" |
| 176 | borderRadius="var(--radius-sm)" |
| 177 | cursor="pointer" |
| 178 | transition="all 0.15s" |
| 179 | _hover={{ borderColor: 'var(--color-primary)', bg: 'rgba(168, 85, 247, 0.08)' }} |
| 180 | onClick={onClick} |
| 181 | title={preset.name} |
| 182 | > |
| 183 | <Box w="100%" h="42px"> |
| 184 | <svg |
| 185 | width="100%" |
| 186 | height="100%" |
| 187 | viewBox={`${minX - pad} ${minY - pad} ${vbW} ${vbH}`} |
| 188 | preserveAspectRatio="xMidYMid meet" |
| 189 | > |
| 190 | <g fill="var(--text-muted)"> |
| 191 | {shapes.map(s => ( |
| 192 | <path |
| 193 | key={s.id} |
| 194 | d={getRoundedRectPath( |
| 195 | s.x, |
| 196 | s.y, |
| 197 | s.w, |
| 198 | s.h, |
| 199 | corners[s.id] || { |
| 200 | tl: preset.radius, |
| 201 | tr: preset.radius, |
nothing calls this directly
no test coverage detected