source: Klonkt/src/views/partials/topnav.ejs@ 9eb6e1b

main
Last change on this file since 9eb6e1b was 9eb6e1b, checked in by Robin <roboburr@…>, 3 hours ago

Een fediverse-post ziet eruit als een post, ook in de zoekbalk

Plak je een link in de zoekbalk, dan laadt de uitklaplijst nu de post zelf
in plaats van te zoeken naar de tekst van de link. Dezelfde kaart staat op de
zoekpagina en op /authorize_interaction, dus één render op drie plekken.

DE KAART. Wat eromheen stond is teruggebracht tot een kop met gezicht, de
inhoud, en één rij knoppen: Reageer, Like, Boost en het origineel. Weg zijn de
zin "je reageert op", de aparte knop naar het origineel, de twee grote pillen,
"of reageer:" en het antwoordvenster dat altijd openstond. Antwoorden klapt
open als je erom vraagt; melden staat onderaan en niet in de weg.

Foto's staan in een raster met vaste rijhoogtes. Met verhoudingen groeide het
mee met de breedte, en werd op een desktop de eerste van drie foto's zo hoog
dat de knoppen onder de vouw van de uitklaplijst verdwenen.

EEN INHOUDSWAARSCHUWING WORDT GERESPECTEERD. De oude render toonde alles open,
ook wat de schrijver achter een waarschuwing had gezet.

MOBIEL. Elke knop minstens 44 punten. Op een smal scherm vallen de woorden weg
en blijven de tekens staan, gelijk verdeeld; de woorden blijven bestaan voor
wie het scherm voorgelezen krijgt. Nagemeten op 375 pixels: geen horizontale
overloop, kleinste tikdoel 44, de knoppen in beeld zonder te scrollen.

DE PREVIEW IS COMPACT. Waarderen en boosten kan ter plekke; antwoorden en
melden gaan naar de volle pagina, met ?reply=1 of ?report=1 zodat dat deel
daar al openstaat. Een editor in een uitklaplijst is op een telefoon geen plek
om te schrijven. Zonder JS doet dezelfde link hetzelfde.

/search/remote levert het fragment, achter dezelfde grens als de volle
pagina: alleen wie de site beheert laat ophalen. Die grens weegt hier zwaarder,
want deze route wordt bij elke toetsaanslag geraakt; zonder hem is hij een
haalservice die iedereen deze server op adressen naar keuze laat afsturen.
Voor een bezoeker 204, en dan valt de zoekbalk terug op de gewone suggesties.
Elke toetsaanslag krijgt een volgnummer, zodat een traag antwoord op een oude
vraag niet overschrijft wat je inmiddels hebt getypt.

Waarderen en boosten ter plekke verhuisde van authorize-interaction.js naar
chrome.js: de kaart staat nu op elke pagina, via de zoekbalk. De zoekpagina
stuurde je bij een like eerder weg naar /authorize_interaction.

Onderweg: /authorize_interaction haalt nu ook ondertekend op, net als de
zoekbalk. Anders zegt die pagina "niet gevonden" over een post voor volgers die
de preview wel liet zien.

Getoetst: de preview met de post, ondertekend; wat er in de compacte kaart wel
en niet staat; dat een bezoeker via de preview niets laat ophalen; dat een
zoekterm niets ophaalt; de waarschuwing; het venster op verzoek. Tegenproef:
rechtencheck eruit, waarschuwing genegeerd, compact genegeerd en anoniem
ophalen laten elk hun eigen toets omvallen. Volledige suite 1283 groen.

Het gedrag in chrome.js is in de browser nagelopen met een nagebootste
server: herkennen, laden, invoegen, de volgorde bij een traag antwoord, de
terugval bij 204, waarderen en boosten heen en terug, en het antwoordvenster
open en dicht met de focus op de goede plek.

shaer-utpi

Co-Authored-By: Claude Opus 5.5 <noreply@…>

  • Property mode set to 100644
File size: 23.9 KB
RevLine 
[7bc636b]1<%
2// Top nav — RESPONSIVE.
3//
4// Mobile (<768px):
5// - Brand centered, small theme-toggle right.
6// - All other actions live in the bottom-tab nav.
7// - Sticky top, blur background, safe-area-top respected.
8//
9// Desktop (≥768px):
10// - Full v9 masthead: brand left, action row right
[d2b7247]11// (Archief / Search / Install / Theme / User-menu).
[7bc636b]12// - bottom-tab is hidden via CSS, so all nav must live here.
13
14const _siteUrlBase = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
15const _isAdmin = typeof bodyClass !== 'undefined' && bodyClass.indexOf('on-admin') >= 0;
[297c77d]16// Fediverse pages (on-special) get the same top-nav "back to site" treatment as admin.
17const _isSpecial = typeof bodyClass !== 'undefined' && bodyClass.indexOf('on-special') >= 0;
[7bc636b]18const _hasSearch = !site || site.show_search === undefined || site.show_search;
[8afbdd6]19const _canPost = user && (typeof canMutate === 'undefined' || canMutate)
20 && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site);
[7bc636b]21%>
22
23<header class="masthead">
24 <div class="container masthead-inner">
25
26 <!-- Brand block (always visible) -->
27 <div class="masthead-brand">
[297c77d]28 <% if (_isAdmin || _isSpecial) { %>
[03fa548]29 <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin"><%= t('nav.back_to_site') %></a>
[7bc636b]30 <% } else { %>
[8afbdd6]31 <a class="site-title" href="/">Klonkt</a>
[7bc636b]32 <% } %>
33 </div>
34
35 <!-- Mobile: theme toggle ONLY for logged-out users.
36 Logged-in users get the toggle inside the profile sheet
37 (bottom-tab → Profiel → Thema). All other actions in bottom-tab. -->
38 <% if (!user) { %>
39 <div class="top-nav-mobile">
40 <button type="button" class="nav-btn" id="theme-toggle-mobile" aria-label="Thema wisselen" title="Thema wisselen">
41 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
42 </button>
43 </div>
44 <% } %>
45
46 <!-- Desktop-only: full action row -->
47 <nav class="top-nav top-nav-desktop" aria-label="Hoofdnavigatie">
48
[297c77d]49 <% if (!_isAdmin && !_isSpecial) { %>
[7bc636b]50 <a class="nav-link" href="<%= _siteUrlBase %>/archive"
51 hx-get="<%= _siteUrlBase %>/archive?partial=1" hx-target="#pcms-main"
52 hx-swap="innerHTML" hx-push-url="<%= _siteUrlBase %>/archive"
53 hx-indicator="#pcms-loading">
54 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8v13H3V8"/><rect x="1" y="3" width="22" height="5" rx="1"/><line x1="10" y1="12" x2="14" y2="12"/></svg>
[03fa548]55 <span class="nav-label"><%= t('nav.archive') %></span>
[7bc636b]56 </a>
57 <% } %>
58
59 <% if (_hasSearch) { %>
[3af8fa3]60 <button type="button" class="nav-btn" id="search-toggle" aria-label="<%= t('nav.search') %>" title="<%= t('nav.search') %>">
[7bc636b]61 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
62 </button>
63 <% } %>
64
65 <button type="button" class="nav-btn" id="pwa-install-btn" hidden aria-label="App installeren" title="App installeren">
66 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
67 </button>
68
69 <button type="button" class="nav-btn" id="theme-toggle" aria-label="Thema wisselen" title="Thema wisselen">
70 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>
71 </button>
72
[83b8b8f]73 <% if (typeof langs !== 'undefined' && langs && langs.length > 1) {
74 var _curLang = (langs.find(function(l){ return l.active; }) || { code: 'nl' }).code;
75 var _langR = (typeof currentPath !== 'undefined' && currentPath) ? currentPath : '/';
76 %>
77 <details class="lang-menu">
78 <summary class="nav-btn lang-toggle" aria-label="<%= t('nav.language') %>" title="<%= t('nav.language') %>">
79 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 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-10 15.3 15.3 0 0 1 4-10z"/></svg>
80 <span class="lang-code"><%= _curLang.toUpperCase() %></span>
81 <svg class="lang-caret" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>
82 </summary>
83 <div class="lang-dropdown" role="menu">
84 <% langs.forEach(function(l){ %>
85 <a class="lang-item<%= l.active ? ' is-active' : '' %>" role="menuitem" href="/lang/<%= l.code %>?r=<%= encodeURIComponent(_langR) %>"><span class="lang-item-code"><%= l.code.toUpperCase() %></span><%= l.name %></a>
86 <% }); %>
87 </div>
88 </details>
[2149f5a]89 <% } %>
90
[a995698]91 <% if (user && typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
[297c77d]92 <a class="nav-btn nav-notif" href="/notifications" aria-label="<%= t('notif.title') %>" title="<%= t('notif.title') %>"
93 hx-get="/notifications?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/notifications" hx-indicator="#pcms-loading">
[c9c6a2d]94 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
[e951b7c]95 <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %><span class="notif-badge"><%= notifUnread > 9 ? '9+' : notifUnread %></span><% } %>
[c9c6a2d]96 </a>
97 <% } %>
98
[7bc636b]99 <% if (user) { %>
100 <details class="user-menu">
101 <summary class="nav-btn user-toggle" aria-label="Gebruikersmenu">
102 <% if (user.avatar_url) { %>
[99989f9]103 <img class="user-avatar user-avatar-img" src="<%= avatar(user.avatar_url, 128) %>" alt="">
[7bc636b]104 <% } else { %>
105 <span class="user-avatar"><%= user.username.charAt(0).toUpperCase() %></span>
106 <% } %>
107 </summary>
108 <div class="user-dropdown" role="menu">
109 <div class="user-dropdown-header">
110 <% if (user.avatar_url) { %>
[99989f9]111 <img class="udh-avatar" src="<%= avatar(user.avatar_url, 128) %>" alt="">
[7bc636b]112 <% } else { %>
113 <span class="udh-avatar udh-avatar-fallback"><%= user.username.charAt(0).toUpperCase() %></span>
114 <% } %>
115 <div class="udh-meta">
116 <span class="udh-name"><%= user.username %></span>
117 <% if (user.role === 'god') { %>
[a742e9f]118 <span class="udh-role">God</span>
[7bc636b]119 <% } else if (user.role === 'admin' || user.role === 'moderator') { %>
120 <span class="udh-role"><%= user.role.charAt(0).toUpperCase() + user.role.slice(1) %></span>
121 <% } %>
122 </div>
123 </div>
124 <div class="user-dropdown-list">
[44cf44d]125 <a href="/account" role="menuitem" class="udi"
126 hx-get="/account?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
127 hx-push-url="/account" hx-indicator="#pcms-loading">
[7bc636b]128 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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>
[03fa548]129 <span><%= t('nav.account') %></span>
[7bc636b]130 </a>
[a3169f5]131 <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
[44cf44d]132 <a href="/admin" role="menuitem" class="udi"
133 hx-get="/admin?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
134 hx-push-url="/admin" hx-indicator="#pcms-loading">
[7bc636b]135 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h7v7H3z"/><path d="M14 3h7v7h-7z"/><path d="M14 14h7v7h-7z"/><path d="M3 14h7v7H3z"/></svg>
[03fa548]136 <span><%= t('nav.admin') %></span>
[7bc636b]137 </a>
138 <% } %>
139 <% if (_canPost) { %>
[8cb1dc7]140 <a href="<%= _siteUrlBase %>/posts/new" role="menuitem" class="udi"
141 hx-get="<%= _siteUrlBase %>/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
142 hx-push-url="<%= _siteUrlBase %>/posts/new" hx-indicator="#pcms-loading">
[7bc636b]143 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
[03fa548]144 <span><%= t('nav.new_post') %></span>
[7bc636b]145 </a>
146 <% } %>
[a995698]147 <% if (typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
[eefd302]148 <a href="/news" role="menuitem" class="udi"
149 hx-get="/news?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
150 hx-push-url="/news" hx-indicator="#pcms-loading">
[9e5438e]151 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>
152 <span><%= t('nav.fediverse') %></span>
153 </a>
154 <% } %>
[7bc636b]155 </div>
156 <div class="user-dropdown-divider" aria-hidden="true"></div>
157 <div class="user-dropdown-list">
158 <a href="/auth/logout" role="menuitem" class="udi udi-danger">
159 <svg class="udi-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><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>
[03fa548]160 <span><%= t('nav.logout') %></span>
[7bc636b]161 </a>
162 </div>
163 </div>
164 </details>
165 <% } else { %>
[441f5d6]166 <a class="nav-link" href="/auth/login"
167 hx-get="/auth/login?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
168 hx-push-url="/auth/login" hx-indicator="#pcms-loading">
[03fa548]169 <span class="nav-label"><%= t('nav.login') %></span>
[7bc636b]170 </a>
171 <% } %>
172
173 </nav>
174 </div>
175</header>
176
177<!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
[156baa3]178<%# De teksten staan op het element dat ze gebruikt, niet in het script: een
179 module is een statisch bestand en kan geen vertaling interpoleren. %>
[9eb6e1b]180<div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-remote="<%= _siteUrlBase %>/search/remote" data-action="<%= _siteUrlBase %>/search"
181 data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty'), looking: t('search.fedi_looking') }) %>">
[7bc636b]182 <div class="container">
183 <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
[097da22]184 <input type="search" name="q" placeholder="<%= t('search.placeholder') %>" autocomplete="off">
185 <button type="submit" class="nav-btn" aria-label="<%= t('search.button') %>">
[7bc636b]186 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
187 </button>
[097da22]188 <button type="button" class="nav-btn" id="search-close" aria-label="<%= t('imed.cancel') %>">
[7bc636b]189 <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
190 </button>
191 </form>
[097da22]192 <div id="search-suggest" class="search-suggest" data-lenis-prevent></div>
[7bc636b]193 </div>
194</div>
195
[156baa3]196<%# Het script van de topnav staat in assets/js/mod/chrome.js (shaer-bqr). %>
[7bc636b]197
198<style>
199/* ============================================================
200 TOP-NAV — responsive
201 Mobile: brand centered, small theme btn right, NOTHING else.
202 Desktop (≥768px): full v9 layout — brand left, action row right.
203 ============================================================ */
204
205.masthead-inner {
206 /* Override v9's flex-end → space-between so brand is on the left */
207 justify-content: space-between !important;
208 gap: 1rem;
209}
210.masthead-brand .site-title,
211.masthead-brand .nav-link-admin {
212 display: inline-block;
213 vertical-align: middle;
214}
215
216/* Site title is suppressed everywhere — the profile-header carries the brand
217 on content pages, and admin pages have their own "← Terug naar site" link
218 (a different selector, .nav-link-admin, which stays visible). The masthead
219 becomes a thin utility strip with just theme-toggle / search / etc. */
220.masthead-brand .site-title {
221 display: none;
222}
223
[834bcc3]224/* Hub-home brand IS visible — the home button on artist/hub pages. */
[0c2228a]225.masthead-brand .nav-hub-home {
[851239c]226 display: inline-flex; align-items: center; justify-content: center;
227 width: 40px; height: 40px;
[0c2228a]228 color: var(--ink); text-decoration: none;
[851239c]229 border-radius: 8px;
230 transition: color 120ms, background 120ms;
[0c2228a]231}
[851239c]232.masthead-brand .nav-hub-home:hover { color: var(--accent); background: var(--paper-2); }
233.masthead-brand .nav-hub-home svg { width: 22px; height: 22px; }
[0c2228a]234
[7bc636b]235/* On mobile, hide the entire masthead except for admin pages. The brand
236 appears in the profile-header on content pages, and the bottom-tab
237 provides all navigation. Admin keeps it because the "← Terug naar site"
238 link is the only way back to the public site from /admin/*. */
239@media (max-width: 767px) {
240 body:not(.on-admin) .masthead { display: none; }
[f6b2c68]241 /* The masthead is gone on mobile, so it can't carry the iOS safe-area top inset
242 (env() resolves to 0 in the home-screen PWA; --ios-safe-top is set by JS in shell).
243 Pad the body instead so the profile-header clears the Dynamic Island / notch. */
244 body:not(.on-admin) { padding-top: var(--ios-safe-top, 0px); }
[7bc636b]245}
246
247/* On mobile, center the brand in the masthead */
248@media (max-width: 767px) {
249 .masthead-inner {
250 justify-content: center !important;
251 position: relative;
252 }
253 .masthead-brand {
254 text-align: center;
255 }
256 /* Lone theme button for logged-out users — minimal, icon-only,
257 no border/background to avoid the iOS sticky-hover look. */
258 .top-nav-mobile {
259 position: absolute;
260 right: 0;
261 top: 50%;
262 transform: translateY(-50%);
263 }
264 .top-nav-mobile .nav-btn {
265 width: 44px;
266 height: 44px;
267 padding: 0;
268 background: transparent !important;
269 border-color: transparent !important;
270 color: var(--ink-soft);
271 display: inline-flex;
272 align-items: center;
273 justify-content: center;
274 }
275 .top-nav-mobile .nav-btn:active {
276 transform: scale(0.92);
277 color: var(--ink);
278 }
279 /* Suppress sticky hover on touch — only show feedback for real hover devices */
280 @media (hover: none) {
281 .top-nav-mobile .nav-btn:hover {
282 background: transparent !important;
283 border-color: transparent !important;
284 color: var(--ink-soft);
285 }
286 }
287 .top-nav-desktop {
288 display: none !important;
289 }
290}
291
292/* On desktop, hide the mobile-only theme button */
293@media (min-width: 768px) {
294 .top-nav-mobile { display: none; }
295}
296
297/* ── Admin "back" link styling (inherited from v9 patterns) ── */
298.nav-link-admin {
299 background: var(--accent);
300 color: #fff !important;
301 padding: .5rem .85rem;
302 border-radius: 6px;
303 text-decoration: none;
304 font-family: var(--font-ui);
305 font-size: .875rem;
306 font-weight: 500;
307}
308.nav-link-admin:hover { opacity: .9; color: #fff !important; }
309
310/* ── Tap-target enforcement for all nav buttons on touch ── */
311@media (hover: none) {
312 .nav-btn, .nav-link {
313 min-height: 44px;
314 min-width: 44px;
315 }
316 .nav-btn:active {
317 transform: scale(0.95);
318 background: var(--paper-2);
319 }
320}
321
322/* ── User dropdown (desktop only — mobile uses bottom-tab → /account) ── */
323.user-menu { position: relative; list-style: none; }
324.user-menu > summary {
325 list-style: none; cursor: pointer;
326 padding: .25rem; border-radius: 6px;
327 display: inline-flex; align-items: center;
328 border: 1px solid transparent;
329 transition: all var(--transition);
330}
331.user-menu > summary::-webkit-details-marker { display: none; }
332.user-menu > summary::marker { content: ''; }
333.user-menu > summary:hover { background: var(--paper-2); border-color: var(--rule); }
334.user-avatar {
335 display: inline-flex; align-items: center; justify-content: center;
336 width: 28px; height: 28px; border-radius: 50%;
337 background: var(--paper-2); /* neutral so transparent avatars blend */
338 color: var(--accent); /* fallback initial color */
339 font-family: var(--font-ui); font-weight: 600; font-size: .8rem;
340 pointer-events: none;
341 overflow: hidden;
342}
343.user-avatar-img {
344 /* When the pill renders as <img> instead of <span>, fill it edge-to-edge. */
345 object-fit: cover;
346}
347.user-dropdown {
348 position: absolute; top: calc(100% + .55rem); right: 0;
349 min-width: 220px;
350 padding: .35rem;
[834bcc3]351 /* Frosted glass: transparent + heavy blur so the frosted effect is clearly visible. */
[c2a9522]352 background: color-mix(in srgb, var(--paper) 60%, transparent);
353 backdrop-filter: blur(34px) saturate(190%);
354 -webkit-backdrop-filter: blur(34px) saturate(190%);
[7bc636b]355 border: 1px solid var(--rule);
356 border-radius: 12px;
357 box-shadow:
358 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent),
359 0 8px 24px color-mix(in srgb, var(--ink) 14%, transparent),
360 0 16px 48px color-mix(in srgb, var(--ink) 10%, transparent);
361 z-index: 200;
362 font-family: var(--font-ui);
363 transform-origin: top right;
364}
365/* Open animation — only fires when <details open> matches, so closing the
366 dropdown doesn't replay the entry animation. */
367.user-menu[open] > .user-dropdown {
368 animation: ud-fade-in .14s cubic-bezier(.2, .8, .25, 1);
369}
370@keyframes ud-fade-in {
371 from { opacity: 0; transform: translateY(-6px) scale(.98); }
372 to { opacity: 1; transform: translateY(0) scale(1); }
373}
374
375/* Identity header — small avatar + username/role badge */
376.user-dropdown-header {
377 display: flex; align-items: center; gap: .65rem;
378 padding: .55rem .65rem .65rem;
379 border-bottom: 1px solid color-mix(in srgb, var(--rule) 65%, transparent);
380 margin-bottom: .35rem;
381}
382.udh-avatar {
383 width: 36px; height: 36px; border-radius: 50%;
384 object-fit: cover;
385 background: var(--paper-2);
386 display: inline-flex; align-items: center; justify-content: center;
387 flex-shrink: 0;
388}
389.udh-avatar-fallback {
390 color: var(--accent); font-weight: 700; font-size: .95rem;
391}
392.udh-meta { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
393.udh-name {
394 font-weight: 600; font-size: .92rem; color: var(--ink);
395 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px;
396}
397.udh-role {
398 font-size: .68rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
399 color: var(--accent); opacity: .85; margin-top: .12rem;
400}
401
402/* Item list group */
403.user-dropdown-list { display: flex; flex-direction: column; gap: 1px; }
404.user-dropdown-divider {
405 height: 1px;
406 margin: .35rem .25rem;
407 background: color-mix(in srgb, var(--rule) 60%, transparent);
408}
409
410/* Items — icon + label, rounded inner highlight on hover (macOS feel) */
411.user-dropdown .udi {
412 display: flex; align-items: center; gap: .65rem;
413 padding: .5rem .65rem;
414 color: var(--ink); text-decoration: none;
415 font-size: .88rem; font-weight: 500;
416 border-radius: 7px;
417 transition: background var(--transition), color var(--transition), transform var(--transition);
418}
419.user-dropdown .udi:hover,
420.user-dropdown .udi:focus-visible {
421 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
422 color: var(--accent);
423 outline: none;
424}
425.user-dropdown .udi:active { transform: scale(.98); }
426.user-dropdown .udi-icon {
427 width: 16px; height: 16px;
428 opacity: .7;
429 flex-shrink: 0;
430 transition: opacity var(--transition);
431}
432.user-dropdown .udi:hover .udi-icon,
433.user-dropdown .udi:focus-visible .udi-icon { opacity: 1; }
434
435/* Logout: subtle danger tint on hover, never on rest. Uses a fixed red so
436 it always reads as a destructive action even if the user's accent is red. */
437.user-dropdown .udi-danger:hover,
438.user-dropdown .udi-danger:focus-visible {
439 background: color-mix(in srgb, #dc2626 14%, var(--paper-2));
440 color: #dc2626;
441}
442
443/* ── Search overlay ── */
444.search-overlay {
445 position: fixed; top: 0; left: 0; right: 0;
446 background: color-mix(in srgb, var(--paper) 96%, transparent);
447 backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
448 border-bottom: 1px solid var(--rule);
449 padding: 1rem 0; z-index: 200;
450 padding-top: calc(1rem + env(safe-area-inset-top, 0));
451 box-shadow: 0 8px 24px color-mix(in srgb, var(--ink) 8%, transparent);
452}
453.search-overlay-form { display: flex; gap: .5rem; align-items: center; }
454.search-overlay-form input {
455 flex: 1; padding: .85rem 1rem;
456 min-height: 44px;
457 border: 1px solid var(--rule); border-radius: 8px;
458 font-family: var(--font-ui); font-size: 1rem;
459 background: var(--paper); color: var(--ink);
460}
461.search-overlay-form input:focus {
462 outline: 2px solid var(--accent); outline-offset: 0;
463}
464.search-overlay-form .nav-btn {
465 width: 44px; height: 44px;
466 display: inline-flex; align-items: center; justify-content: center;
467}
[097da22]468/* Live-resultaten dropdown */
[9eb6e1b]469.search-suggest { margin-top: .6rem; max-height: 60vh; max-height: 70dvh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; gap: .25rem; }
[097da22]470.search-suggest:empty { display: none; }
471.ss-group { padding: .25rem 0; }
472.ss-title { font-size: .72rem; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; color: var(--ink-soft, var(--ink-muted)); padding: .35rem .25rem .2rem; }
473.ss-item { display: block; padding: .55rem .6rem; border-radius: 8px; color: var(--ink); text-decoration: none; font-size: .95rem; }
474.ss-item:hover { background: var(--paper-2); color: var(--accent); }
475.ss-item.ss-noclick { opacity: .7; cursor: default; }
476.ss-sub { color: var(--ink-soft, var(--ink-muted)); font-size: .85em; }
477.ss-empty { padding: .8rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; }
478.ss-all { display: block; margin-top: .35rem; padding: .55rem .6rem; border-top: 1px solid var(--rule); color: var(--accent); font-weight: 600; text-decoration: none; font-size: .9rem; }
479.ss-all:hover { text-decoration: underline; }
[834bcc3]480/* Notification bell + unread badge */
[c9c6a2d]481.nav-notif { position: relative; }
482.notif-badge {
483 position: absolute; top: 1px; right: 1px;
484 min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box;
485 display: inline-flex; align-items: center; justify-content: center;
486 background: #e0245e; color: #fff; font-size: 10px; font-weight: 700;
487 border-radius: 999px; line-height: 1; pointer-events: none;
488}
[7bc636b]489</style>
Note: See TracBrowser for help on using the repository browser.