extremely claude-assisted go game based on atproto! working on cleaning up and giving a more unique design, still has a bit of a slop vibe to it.

Add reaction emoji overlay on board stones

When "All Reactions" mode is toggled, reaction emojis are overlaid
directly on the stones they reference, making it easy to see which
moves received reactions at a glance.

- Add reactionOverlay prop to Board component
- Compute overlay data mapping move reactions to stone coordinates
- Position emojis with drop shadow for visibility on both stone colors
- Show up to 3 unique emojis per stone, with +N count for more

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+106 -1
+79 -1
src/lib/components/Board.svelte
··· 12 12 whiteTerritory: number; 13 13 } 14 14 15 + interface ReactionOverlayItem { 16 + x: number; 17 + y: number; 18 + emojis: string[]; 19 + } 20 + 15 21 interface Props { 16 22 boardSize?: number; 17 23 gameState?: any; ··· 24 30 deadStones?: string[]; 25 31 markingDeadStones?: boolean; 26 32 onToggleDeadStone?: (x: number, y: number, color: 'black' | 'white') => void; 33 + reactionOverlay?: ReactionOverlayItem[]; 27 34 } 28 35 29 36 let { ··· 37 44 libertyData = null, 38 45 deadStones = [], 39 46 markingDeadStones = false, 40 - onToggleDeadStone = () => {} 47 + onToggleDeadStone = () => {}, 48 + reactionOverlay = [] 41 49 }: Props = $props(); 42 50 43 51 let boardElement: HTMLDivElement; ··· 692 700 {/each} 693 701 </div> 694 702 {/if} 703 + {#if reactionOverlay && reactionOverlay.length > 0} 704 + {@const padding = gridSize * 0.8} 705 + {@const margin = gridSize * 0.8} 706 + {@const stoneRadius = gridSize * 0.38} 707 + {@const boardOffset = padding + margin + stoneRadius - 1} 708 + <div class="reaction-overlay"> 709 + {#each reactionOverlay as item} 710 + {@const emojiSize = Math.max(12, gridSize * 0.45)} 711 + <div 712 + class="reaction-emoji-badge" 713 + style=" 714 + left: {boardOffset + item.x * gridSize}px; 715 + top: {boardOffset + item.y * gridSize}px; 716 + font-size: {emojiSize}px; 717 + " 718 + title={item.emojis.join(' ')} 719 + > 720 + {#if item.emojis.length === 1} 721 + {item.emojis[0]} 722 + {:else if item.emojis.length === 2} 723 + <span class="emoji-stack"> 724 + {#each item.emojis as emoji} 725 + <span>{emoji}</span> 726 + {/each} 727 + </span> 728 + {:else} 729 + {item.emojis[0]}<span class="emoji-count">+{item.emojis.length - 1}</span> 730 + {/if} 731 + </div> 732 + {/each} 733 + </div> 734 + {/if} 695 735 </div> 696 736 </div> 697 737 ··· 903 943 0 0 8px rgba(255, 255, 255, 0.8), 904 944 2px 2px 4px rgba(0, 0, 0, 0.9); 905 945 opacity: 0.95; 946 + } 947 + 948 + .reaction-overlay { 949 + position: absolute; 950 + top: 0; 951 + left: 0; 952 + right: 0; 953 + bottom: 0; 954 + pointer-events: none; 955 + } 956 + 957 + .reaction-emoji-badge { 958 + position: absolute; 959 + transform: translate(10%, -90%); 960 + line-height: 1; 961 + pointer-events: none; 962 + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)); 963 + z-index: 10; 964 + } 965 + 966 + .emoji-stack { 967 + display: flex; 968 + gap: 0; 969 + } 970 + 971 + .emoji-stack span:not(:first-child) { 972 + margin-left: -0.3em; 973 + } 974 + 975 + .emoji-count { 976 + font-size: 0.6em; 977 + font-weight: 700; 978 + color: white; 979 + background: rgba(0, 0, 0, 0.6); 980 + border-radius: 999px; 981 + padding: 0 0.3em; 982 + margin-left: 0.1em; 983 + vertical-align: super; 906 984 } 907 985 908 986 .mobile-confirmation-overlay {
+27
src/routes/game/[id]/+page.svelte
··· 311 311 return emojis.slice(0, 3); // Max 3 emojis for badge display 312 312 } 313 313 314 + // Build reaction overlay data for the board (maps reaction emojis to stone coordinates) 315 + const reactionOverlayData = $derived(() => { 316 + if (!showAllReactions || reactions.size === 0 || moves.length === 0) return []; 317 + 318 + const coordMap = new Map<string, string[]>(); // "x,y" -> emojis 319 + 320 + for (const [moveUri, reacts] of reactions) { 321 + const move = moves.find(m => getMoveUri(m) === moveUri); 322 + if (!move) continue; 323 + 324 + const key = `${move.x},${move.y}`; 325 + const emojis = coordMap.get(key) || []; 326 + for (const r of reacts) { 327 + if (r.emoji && !emojis.includes(r.emoji)) { 328 + emojis.push(r.emoji); 329 + } 330 + } 331 + coordMap.set(key, emojis); 332 + } 333 + 334 + return Array.from(coordMap.entries()).map(([key, emojis]) => { 335 + const [x, y] = key.split(',').map(Number); 336 + return { x, y, emojis: emojis.slice(0, 3) }; 337 + }); 338 + }); 339 + 314 340 async function loadGameData() { 315 341 console.log('[loadGameData] Starting...', { creatorDid: data.creatorDid, gameRkey: data.gameRkey }); 316 342 let record: GameRecord | null = null; ··· 1302 1328 deadStones={deadStones} 1303 1329 markingDeadStones={markingDeadStones} 1304 1330 onToggleDeadStone={handleToggleDeadStone} 1331 + reactionOverlay={reactionOverlayData()} 1305 1332 /> 1306 1333 {/if} 1307 1334