1// The bottom tab bar, on phones. 2// 3// Every destination on this site lived behind a hamburger: zero of them were 4// visible without opening a menu, so reaching the leaderboard was two taps and 5// knowing it existed was a matter of curiosity. A phone app puts its rooms 6// along the bottom, always lit, always one tap â which is also the only way a 7// player discovers Duels and Crews without being told. 8// 9// The bar is built here for the same reason the header is: seventeen pages 10// would otherwise each carry a copy, and the last one would always be an edit 11// behind. Desktop keeps the inline nav row it already has; this is phones only. 12(() => { 13 // Matches the hamburger breakpoint in style.css. Above it the header already 14 // shows every link inline and a second copy along the bottom would be noise. 15 const NARROW = '(max-width: 860px)'; 16 if (!window.matchMedia || !window.matchMedia(NARROW).matches) return; 17 18 const here = location.pathname.replace(/\/$/, '') || '/'; 19 20 // The front door used to be excluded, and the reason was sound while it was 21 // true: / was a splash with one deliberate choice on it â ENTER â and a row 22 // of five alternatives along the bottom is an argument against the one thing 23 // that screen is asking for. 24 // 25 // It is not a splash any more. / is the homepage now: a hero, the live board, 26 // six playable games and four links out. Withholding the bar there would make 27 // the front door the ONE page on the site where a phone has no navigation, 28 // which is the exact problem this file was written to fix. So it gets the bar 29 // like everything else. 30 31 // Line icons rather than emoji: emoji render as somebody else's artwork at 32 // somebody else's weight, and five of them never look like one set. 33 const ICON = { 34 play: '<path d="M8 5.5v13l11-6.5z"/>', 35 ranks: '<rect x="3" y="13" width="4.5" height="8" rx="1"/>' 36 + '<rect x="9.75" y="8" width="4.5" height="13" rx="1"/>' 37 + '<rect x="16.5" y="4" width="4.5" height="17" rx="1"/>', 38 duels: '<path d="M4 4l9.5 9.5M20 4l-9.5 9.5" stroke-width="2" stroke-linecap="round" fill="none"/>' 39 + '<path d="M13.5 13.5l3.5 3.5-2 2-3.5-3.5M10.5 13.5L7 17l2 2 3.5-3.5" stroke-width="2" stroke-linejoin="round" fill="none"/>', 40 you: '<circle cx="12" cy="8" r="3.6" stroke-width="2" fill="none"/>' 41 + '<path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" stroke-width="2" stroke-linecap="round" fill="none"/>', 42 }; 43 44 // href may be a function, for a tab whose destination depends on who is 45 // asking. Order is by how often a player wants it, left to right, with the 46 // thing they came to do first. 47 // 48 // The labels are the words the desktop nav uses, not shorter synonyms for 49 // them. PLAY and RANKS read fine on their own but they named the same two 50 // destinations the bar at the top calls ARCADE and LEADERBOARD, so a player 51 // moving between a phone and a laptop had to learn the site twice. YOU has 52 // no counterpart in the top nav â it is the player's own page â so it keeps 53 // its name. 54 // 55 // `id` is an internal key, not a label: it selects the icon, and 'ranks' 56 // stays 'ranks' because renaming it would rename the icon it draws for no 57 // gain a player can see. 58 const TABS = [ 59 { id: 'play', label: 'Arcade', href: '/arcade', match: ['/arcade'] }, 60 { id: 'ranks', label: 'Leaderboard', href: '/leaderboard', match: ['/leaderboard'] }, 61 { id: 'duels', label: 'Duels', href: '/duel', match: ['/duel', '/crews'] }, 62 // CLAIMS WAS THE FOURTH TAB and is gone with the desk it pointed at, which 63 // closed at 00:00 UTC on 20 Sep 2026. Its icon went with it. The route 64 // still answers 410 for a holder arriving on an old link â see the claim 65 // desk block in server.js â but a tab is furniture, and furniture pointing 66 // at a withdrawn page is a dead end one tap from every screen on a phone. 67 // 68 // DO NOT PASTE THE OLD ENTRY BACK INTO THIS COMMENT to show what it was. 69 // homeuitest counts the tabs by grepping this file for the literal opening 70 // of an entry, and searches it for the desk's path; a commented-out copy 71 // reads as a fifth tab and fails both. That is the same trap legacyuitest 72 // records for <main>. Four tabs, and the grid in style.css no longer 73 // writes the number down a second time. 74 { id: 'you', label: 'You', href: null, match: ['/player'] }, 75 ]; 76
77 // "You" has no fixed address: it is the player's own page when we know who 78 // they are, and the way to become known when we do not. 79 function youHref() { 80 const w = window.ARCADE_WALLET; 81 const pk = (w && w.publicKey) || (() => { 82 try { return (JSON.parse(localStorage.getItem('webcade-wallet') || 'null') || {}).pk || null; } 83 catch (_) { return null; } 84 })(); 85 // Same for the tab bar: a Telegram player has no pk, and without this 86 // their profile tab simply is not there. 87 const tgId = (window.ARCADE_TG && window.ARCADE_TG.id) || null; 88 const who = pk || tgId; 89 return who ? '/player?w=' + encodeURIComponent(who) : null; 90 } 91 92 const bar = document.createElement('nav'); 93 bar.id = 'tabbar'; 94 bar.setAttribute('aria-label', 'Sections'); 95 96 bar.innerHTML = TABS.map(t => { 97 const on = t.match.some(m => here === m.replace(/\/$/, '')); 98 const href = t.id === 'you' ? youHref() : t.href; 99 // A tab with nowhere to go is still a tab â it connects a wallet instead, 100 // which is the one thing that gives it somewhere to go. 101 const tag = href ? 'a' : 'button'; 102 const attrs = href ? ` href="${href}"` : ' type="button"'; 103 return `<${tag} class="tb${on ? ' on' : ''}" data-tab="${t.id}"${attrs}${on ? ' aria-current="page"' : ''}> 104 <svg viewBox="0 0 24 24" aria-hidden="true">${ICON[t.id]}</svg> 105 <span>${t.label}</span> 106 </${tag}>`; 107 }).join(''); 108 109 // Appended to <body>, not into <footer>: the bar is fixed to the viewport and 110 // has to outlive whatever a page does to its own layout. 111 document.body.appendChild(bar); 112 document.body.classList.add('has-tabbar'); 113 114 const you = bar.querySelector('[data-tab="you"]'); 115 if (you && you.tagName === 'BUTTON') { 116 you.addEventListener('click', () => { 117 // Ask again before reaching for a wallet. This file is injected into 118 // <head> by server.js and tg.js sits eleven scripts down the body, so the 119 // href was decided at a moment when ARCADE_TG could not exist yet â and 120 // for a Telegram player, who has no browser wallet either, that meant YOU 121 // became a button offering to install Phantom. By the time anyone taps 122 // it, the answer is available. 123 const href = youHref(); 124 if (href) { location.href = href; return; } 125 const w = window.ARCADE_WALLET; 126 if (w && w.connect) w.connect(); 127 }); 128 } 129 130 // The wallet arrives after this runs â it reconnects asynchronously, and on 131 // the Seeker only once a CDN import resolves. So the tab is corrected when 132 // that lands rather than being left pointing nowhere for the whole visit. 133 function refreshYou() { 134 const href = youHref(); 135 const el = bar.querySelector('[data-tab="you"]'); 136 if (!href || !el || el.tagName === 'A') return; 137 const a = document.createElement('a'); 138 a.className = el.className; 139 a.dataset.tab = 'you'; 140 a.href = href; 141 a.innerHTML = el.innerHTML; 142 el.replaceWith(a); 143 } 144 if (window.ARCADE_WALLET && window.ARCADE_WALLET.onChange) { 145 window.ARCADE_WALLET.onChange(refreshYou); 146 } else { 147 // wallet.js may not have run yet; ask again once it has 148 document.addEventListener('DOMContentLoaded', () => { 149 // And ask outright, not only through the wallet. onChange was the ONLY 150 // thing that ever corrected this tab, and it fires when a WALLET 151 // arrives â which for a Telegram player never happens, because their 152 // identity is a signed id and not a wallet at all. So the one group of 153 // players whose tab bar was wrong was also the one group nothing was 154 // ever going to fix it for. tg.js has published ARCADE_TG by now. 155 refreshYou(); 156 if (window.ARCADE_WALLET && window.ARCADE_WALLET.onChange) { 157 window.ARCADE_WALLET.onChange(refreshYou); 158 } 159 }); 160 } 161})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.