🌿 Collaborative wiki on ATProto
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update header layout

juprodh bb0aa75d 049315b4

+33 -17
+2
src/views/icons.ts
··· 3 3 settings: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>`, 4 4 globe: `<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z"/></svg>`, 5 5 user: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`, 6 + login: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>`, 7 + logout: `<svg class="w-4 h-4 shrink-0" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg>`, 6 8 } as const;
+31 -17
src/views/layout.ts
··· 102 102 const msg = t(locale); 103 103 104 104 const session = options?.session; 105 - const authHtml = session 106 - ? `<a href="${profileUrl(session.did)}" class="${THEME.textMuted} ${THEME.textSecondaryHover}" title="${msg.profile.myWikis}">${ICONS.user}</a> 107 - <form method="POST" action="/logout" class="inline"> 108 - <button type="submit" class="${outlineSmallButtonClass}">${msg.nav.logout}</button> 109 - </form>` 110 - : `<a href="/login" class="${outlineSmallButtonClass}">${msg.nav.login}</a>`; 105 + 106 + const navBtnClass = `flex items-center justify-center gap-2 h-9 px-2.5 border ${THEME.borderDefault} rounded-lg ${THEME.bgSurface} ${THEME.textSecondary} ${THEME.textSecondaryHover} ${THEME.accentLightHoverBg} cursor-pointer text-sm`; 107 + const dropdownClass = `hidden absolute right-0 mt-1 ${THEME.bgSurface} border ${THEME.borderDefault} rounded-lg shadow-lg py-1.5 z-20 w-max`; 108 + const dropdownItemClass = `flex items-center gap-2.5 px-4 py-2 text-sm ${THEME.textSecondary} ${THEME.accentLightHoverBg} rounded`; 111 109 112 110 const localeItems = SUPPORTED_LOCALES.map( 113 111 (l) => 114 - `<a href="#" class="block px-3 py-1.5 text-sm ${l === locale ? `font-semibold ${THEME.accentText}` : THEME.textSecondary} ${THEME.accentLightHoverBg} rounded" onclick="event.preventDefault();fetch('/set-locale',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'locale=${l}'}).then(()=>location.reload())">${LOCALE_LABELS[l] ?? l.toUpperCase()}</a>`, 112 + `<a href="#" class="block px-3 py-2 text-sm ${l === locale ? `font-semibold ${THEME.accentText}` : THEME.textSecondary} ${THEME.accentLightHoverBg} rounded" onclick="event.preventDefault();fetch('/set-locale',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'locale=${l}'}).then(()=>location.reload())">${LOCALE_LABELS[l] ?? l.toUpperCase()}</a>`, 115 113 ).join("\n"); 116 114 const localePicker = `<div class="relative" id="locale-picker"> 117 - <button type="button" onclick="document.getElementById('locale-menu').classList.toggle('hidden')" class="flex items-center gap-1 text-sm ${THEME.textMuted} ${THEME.textSecondaryHover} cursor-pointer" aria-label="Change language"> 115 + <button type="button" onclick="document.getElementById('locale-menu').classList.toggle('hidden')" class="${navBtnClass}" aria-label="Change language"> 118 116 ${ICONS.globe} 119 - <span class="text-xs uppercase">${locale}</span> 117 + <span class="uppercase">${locale}</span> 120 118 </button> 121 - <div id="locale-menu" class="hidden absolute right-0 mt-2 ${THEME.bgSurface} border ${THEME.borderDefault} rounded-lg shadow-lg py-1 min-w-[8rem] z-20"> 119 + <div id="locale-menu" class="${dropdownClass}"> 122 120 ${localeItems} 123 121 </div> 124 122 </div>`; 125 123 124 + const profileOrLogin = session 125 + ? `<div class="relative" id="profile-picker"> 126 + <button type="button" onclick="document.getElementById('profile-menu').classList.toggle('hidden')" class="${navBtnClass}" aria-label="${msg.profile.myWikis}"> 127 + ${ICONS.user} 128 + </button> 129 + <div id="profile-menu" class="${dropdownClass}"> 130 + <a href="${profileUrl(session.did)}" class="${dropdownItemClass}">${ICONS.user} <span>${msg.profile.myWikis}</span></a> 131 + <form method="POST" action="/logout"> 132 + <button type="submit" class="w-full ${dropdownItemClass} text-left cursor-pointer">${ICONS.logout} <span>${msg.nav.logout}</span></button> 133 + </form> 134 + </div> 135 + </div>` 136 + : `<a href="/login" class="${navBtnClass}" aria-label="${msg.nav.login}" title="${msg.nav.login}">${ICONS.login}</a>`; 137 + 126 138 return `<!DOCTYPE html> 127 139 <html lang="${locale}"> 128 140 <head> ··· 135 147 </head> 136 148 <body class="${THEME.bgPage} ${THEME.textPrimary} min-h-screen flex flex-col"> 137 149 <nav class="${THEME.bgSurface} border-b ${THEME.borderDefault}"> 138 - <div class="max-w-6xl mx-auto px-6 py-3 grid grid-cols-[1fr_auto_1fr] items-center gap-4"> 139 - <div class="flex items-center gap-2"> 150 + <div class="max-w-6xl mx-auto px-6 py-3 flex items-center justify-between gap-4"> 151 + <div class="flex items-center gap-2 min-w-0"> 140 152 <a href="/" class="flex items-center gap-2 shrink-0"><span class="inline-block w-3 h-3 rounded-full ${THEME.accentBg}"></span><span class="text-lg font-semibold ${THEME.accentText}">Lichen</span></a> 153 + ${options?.wikiName ? `<span class="${THEME.textMuted}">/</span><a href="/wiki/${options.wikiSlug}" class="text-lg font-medium ${THEME.textSecondary} ${THEME.accentHoverText} truncate">${escapeHtml(options.wikiName)}</a>` : ""} 141 154 </div> 142 - <div class="text-center truncate">${options?.wikiName ? `<a href="/wiki/${options.wikiSlug}" class="text-lg font-medium ${THEME.textSecondary} ${THEME.accentHoverText}">${escapeHtml(options.wikiName)}</a>` : ""}</div> 143 - <div class="flex items-center justify-end gap-3"> 155 + <div class="flex items-center gap-2 shrink-0"> 156 + ${profileOrLogin} 144 157 ${localePicker} 145 - ${authHtml} 146 158 </div> 147 159 </div> 148 160 </nav> ··· 161 173 </dialog> 162 174 <script> 163 175 document.addEventListener('click', (e) => { 164 - const lp = document.getElementById('locale-picker'); 165 - if (lp && !lp.contains(e.target)) document.getElementById('locale-menu')?.classList.add('hidden'); 176 + for (const [pickerId, menuId] of [['locale-picker', 'locale-menu'], ['profile-picker', 'profile-menu']]) { 177 + const el = document.getElementById(pickerId); 178 + if (el && !el.contains(e.target)) document.getElementById(menuId)?.classList.add('hidden'); 179 + } 166 180 }); 167 181 document.addEventListener('keydown', (e) => { 168 182 const tag = document.activeElement?.tagName;