1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
import { LANGUAGE_ICON, FRIENDS_ICON } from './screens/icons.js';
import { LANGUAGES, languageLabel } from './languages.js';
import { getSettings, updateSettings } from './settings.js';
import { renderThemeButton } from './themeButton.js';
import { getUser, onAuthChange } from './auth.js';
import { onCountsChange, getCounts } from './counts.js';
import { attachTooltips } from './tooltip.js';
import { escapeHtml } from './util.js';
// The top-right cluster: the typing language, the theme, and who you are --
// Friends sits directly beside the account control because the two are the
// same thing, your identity and who it connects you to. These are the app's own chrome --
// they belong together and away from the bottom rail's per-screen
// navigation. Screens with their own top-right controls (the typing screen's
// Restart and Zen Finish) pass them in as `extras` so everything shares one
// row instead of each fixed-positioning itself into a collision.
export function renderTopRail(root, { onShowAccount, onShowFriends, extras = [] } = {}) {
const wrap = document.createElement('div');
wrap.className = 'top-rail';
root.appendChild(wrap);
for (const el of extras) wrap.appendChild(el);
const langGroup = document.createElement('div');
langGroup.className = 'lang-group';
langGroup.innerHTML = `
<button class="corner-icon-button" data-action="lang" aria-label="Typing language" data-tooltip="Typing language">${LANGUAGE_ICON}</button>
<div class="lang-popover" hidden>
<div class="lang-heading">Typing language</div>
${LANGUAGES.map(l => `
<button class="lang-item" data-lang="${escapeHtml(l.id)}">${escapeHtml(l.label)}</button>
`).join('')}
<div class="lang-note">Applies to Words, Timed, Zen and Practice.</div>
</div>
`;
wrap.appendChild(langGroup);
const cleanupTheme = renderThemeButton(wrap);
const friends = document.createElement('div');
friends.className = 'friends-control';
if (onShowFriends) {
friends.innerHTML = `<button class="corner-icon-button" data-action="rail-friends" aria-label="Friends" data-tooltip="Friends">${FRIENDS_ICON}</button>
<span class="online-count" data-badge="friends-online" hidden></span>`;
}
wrap.appendChild(friends);
const auth = document.createElement('div');
auth.className = 'auth-control';
wrap.appendChild(auth);
attachTooltips(wrap);
// Language ------------------------------------------------------------
const langPopover = langGroup.querySelector('.lang-popover');
function paintLang() {
const current = getSettings().language || 'en';
langGroup.querySelectorAll('.lang-item').forEach(item => {
item.classList.toggle('active', item.dataset.lang === current);
});
langGroup.querySelector('[data-action="lang"]').dataset.tooltip = `Typing language: ${languageLabel(current)}`;
}
paintLang();
langGroup.querySelector('[data-action="lang"]').addEventListener('click', e => {
e.stopPropagation();
langPopover.hidden = !langPopover.hidden;
});
langGroup.querySelectorAll('.lang-item').forEach(item => {
item.addEventListener('click', () => {
updateSettings({ language: item.dataset.lang });
paintLang();
langPopover.hidden = true;
});
});
// Identity ------------------------------------------------------------
function paintAuth() {
const user = getUser();
auth.innerHTML = user
? `<button class="top-rail-link signed-in" data-action="account" data-tooltip="Your account">${escapeHtml(user.username)}</button>`
: `<button class="top-rail-link" data-action="signin" data-tooltip="Sign in to save stats and join the leaderboard">Sign In</button>
<button class="top-rail-link accent" data-action="signup" data-tooltip="Create an account">Sign Up</button>`;
attachTooltips(auth);
auth.querySelectorAll('button').forEach(btn => {
btn.addEventListener('click', () => onShowAccount?.());
});
}
paintAuth();
const unsubscribeAuth = onAuthChange(paintAuth);
// Friends ------------------------------------------------------------
const friendsBtn = friends.querySelector('[data-action="rail-friends"]');
if (friendsBtn) friendsBtn.addEventListener('click', onShowFriends);
// How many friends are online, not how many you have - a total is a fact
// about your account, whereas "2 online" is a reason to open the screen.
const friendsOnlineEl = friends.querySelector('[data-badge="friends-online"]');
function paintCounts({ friends: friendCount, friendsOnline }) {
if (friendsBtn) {
friendsBtn.dataset.tooltip = !friendCount
? 'Friends'
: (friendsOnline ? `Friends - ${friendsOnline} of ${friendCount} online` : `Friends - ${friendCount}, none online`);
}
if (!friendsOnlineEl) return;
friendsOnlineEl.hidden = !friendsOnline;
friendsOnlineEl.textContent = `${friendsOnline} online`;
}
paintCounts(getCounts());
const unsubscribeCounts = onCountsChange(paintCounts);
const handleOutsideClick = e => {
if (!langPopover.hidden && !langGroup.contains(e.target)) langPopover.hidden = true;
};
document.addEventListener('click', handleOutsideClick);
return () => {
unsubscribeCounts();
document.removeEventListener('click', handleOutsideClick);
unsubscribeAuth();
cleanupTheme();
wrap.remove();
};
}
|