source: Klonkt/src/views/partials/topnav.ejs@ ca260d1

main
Last change on this file since ca260d1 was fd8faf5, checked in by Robin <roboburr@…>, 5 hours ago

Zoeken is één vlak: de balk bovenaan, de resultaten eronder, en @naam@server

Robins ontwerp van 30-9: de aparte resultatenpagina is niet meer nodig als het
vak onder de zoekbalk blijft staan. De balk en het vak eronder zijn samen het
hele zoeken, en het vak krijgt alle ruimte die er onder de balk is.

EEN VORM, TWEE INGANGEN. partials/search-surface.ejs is de balk plus het vak;
de overlay op elke pagina gebruikt hem, en /search?q=... ook. De resultaten
zelf staan in partials/search-results.ejs, en het live vak haalt precies die
render op (/search/results). De pagina bevat dus teken voor teken wat het vak
toont; een toets bewaakt dat, want twee renders lopen gegarandeerd uit de pas.
/search blijft bestaan zodat een gedeelde link en Enter zonder JavaScript op
hetzelfde beeld uitkomen.

/search/suggest (JSON, dat de browser tot een eigen lijstje bouwde, anders dan
de pagina) en /search/remote (alleen het adres) zijn weg; niets anders
gebruikte ze. remote-preview.ejs ging mee.

HET VLAK. Vaste positie over het hele scherm, de balk bovenaan, het vak vult
de rest en scrolt zelf. Op z-index 1060: de vorige overlay lag op 200, onder de
tabbalk (1050), zodat op een telefoon het onderste stuk resultaten wegviel. Enter
blijft staan en zoekt meteen, want het vak ís de resultaten. Op de pagina beweegt
het adres mee (/search?q=...), zodat wat je ziet te delen blijft; sluiten is daar
teruggaan. Balk en resultaten staan op één lijn: een scrollbalk alleen rechts
schoof de kolom anders zo'n 9 pixels naar links.

WEBFINGER. @naam@server (met of zonder @ vooraan, zoals Mastodon) haalt het
profiel op, met de volgknop. Zonder eerst een post te proberen: een handle is
nooit een post. De server moet een echte domeinnaam zijn; "@robin" blijft een
naam om naar te zoeken. WebFinger zelf is openbaar en loopt via safeFetch;
het profiel erachter gaat ondertekend, en alleen namens een ingelogde
beheerder, dezelfde grens als een geplakte link.

Onderweg gevonden: search-results.ejs verwees kaal naar queryError, en een
aanroeper die hem vergat liet de hele render omvallen. Alles gaat nu via
locals. In de app gaf gather hem altijd mee, dus live ging het goed; het was
één vergeten veld van een kapotte zoekpagina.

Nagelopen in de browser op 375 en 1280 pixels: de balk bovenaan, het vak tot
de onderrand, tabbalk bedekt, geen horizontale overloop, tikdoelen 44, balk en
resultaten op één lijn. Het gedrag met de echte chrome.js: openen vers en met
focus, typen, Enter zonder weg te gaan, de wachtregel bij een link en een
handle, een traag antwoord dat niets overschrijft, een resultaat aantikken en
Escape sluiten, en het adres dat op de pagina meebeweegt.

Getoetst: pagina en vak teken voor teken gelijk, de handle-herkenning, een
handle ondertekend en zonder post-poging, en niet ingelogd geen enkel verzoek.
Tegenproef: pagina laten afwijken, handle niet herkennen, bij een handle toch
een post proberen en de inlogcontrole eruit laten elk hun toetsen omvallen.
Volledige suite 1292 groen.

shaer-utpi

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

  • Property mode set to 100644
File size: 20.5 KB
Line 
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
11// (Archief / Search / Install / Theme / User-menu).
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;
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;
18const _hasSearch = !site || site.show_search === undefined || site.show_search;
19const _canPost = user && (typeof canMutate === 'undefined' || canMutate)
20 && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site);
21%>
22
23<header class="masthead">
24 <div class="container masthead-inner">
25
26 <!-- Brand block (always visible) -->
27 <div class="masthead-brand">
28 <% if (_isAdmin || _isSpecial) { %>
29 <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin"><%= t('nav.back_to_site') %></a>
30 <% } else { %>
31 <a class="site-title" href="/">Klonkt</a>
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
49 <% if (!_isAdmin && !_isSpecial) { %>
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>
55 <span class="nav-label"><%= t('nav.archive') %></span>
56 </a>
57 <% } %>
58
59 <% if (_hasSearch) { %>
60 <button type="button" class="nav-btn" id="search-toggle" aria-label="<%= t('nav.search') %>" title="<%= t('nav.search') %>">
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
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>
89 <% } %>
90
91 <% if (user && typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
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">
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>
95 <% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %><span class="notif-badge"><%= notifUnread > 9 ? '9+' : notifUnread %></span><% } %>
96 </a>
97 <% } %>
98
99 <% if (user) { %>
100 <details class="user-menu">
101 <summary class="nav-btn user-toggle" aria-label="Gebruikersmenu">
102 <% if (user.avatar_url) { %>
103 <img class="user-avatar user-avatar-img" src="<%= avatar(user.avatar_url, 128) %>" alt="">
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) { %>
111 <img class="udh-avatar" src="<%= avatar(user.avatar_url, 128) %>" alt="">
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') { %>
118 <span class="udh-role">God</span>
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">
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">
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>
129 <span><%= t('nav.account') %></span>
130 </a>
131 <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
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">
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>
136 <span><%= t('nav.admin') %></span>
137 </a>
138 <% } %>
139 <% if (_canPost) { %>
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">
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>
144 <span><%= t('nav.new_post') %></span>
145 </a>
146 <% } %>
147 <% if (typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
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">
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 <% } %>
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>
160 <span><%= t('nav.logout') %></span>
161 </a>
162 </div>
163 </div>
164 </details>
165 <% } else { %>
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">
169 <span class="nav-label"><%= t('nav.login') %></span>
170 </a>
171 <% } %>
172
173 </nav>
174 </div>
175</header>
176
177<!-- Het zoeken (gedeeld door de tabbalk op mobiel en de topnav op desktop) -->
178<%# Balk + vak, dezelfde partial als de pagina /search: zie search-surface.ejs. %>
179<%- include('./search-surface', { mode: 'overlay', t: t, siteUrlBase: _siteUrlBase }) %>
180<%- include('./search-styles') %>
181
182<%# Het script van de topnav staat in assets/js/mod/chrome.js (shaer-bqr). %>
183
184<style>
185/* ============================================================
186 TOP-NAV — responsive
187 Mobile: brand centered, small theme btn right, NOTHING else.
188 Desktop (≥768px): full v9 layout — brand left, action row right.
189 ============================================================ */
190
191.masthead-inner {
192 /* Override v9's flex-end → space-between so brand is on the left */
193 justify-content: space-between !important;
194 gap: 1rem;
195}
196.masthead-brand .site-title,
197.masthead-brand .nav-link-admin {
198 display: inline-block;
199 vertical-align: middle;
200}
201
202/* Site title is suppressed everywhere — the profile-header carries the brand
203 on content pages, and admin pages have their own "← Terug naar site" link
204 (a different selector, .nav-link-admin, which stays visible). The masthead
205 becomes a thin utility strip with just theme-toggle / search / etc. */
206.masthead-brand .site-title {
207 display: none;
208}
209
210/* Hub-home brand IS visible — the home button on artist/hub pages. */
211.masthead-brand .nav-hub-home {
212 display: inline-flex; align-items: center; justify-content: center;
213 width: 40px; height: 40px;
214 color: var(--ink); text-decoration: none;
215 border-radius: 8px;
216 transition: color 120ms, background 120ms;
217}
218.masthead-brand .nav-hub-home:hover { color: var(--accent); background: var(--paper-2); }
219.masthead-brand .nav-hub-home svg { width: 22px; height: 22px; }
220
221/* On mobile, hide the entire masthead except for admin pages. The brand
222 appears in the profile-header on content pages, and the bottom-tab
223 provides all navigation. Admin keeps it because the "← Terug naar site"
224 link is the only way back to the public site from /admin/*. */
225@media (max-width: 767px) {
226 body:not(.on-admin) .masthead { display: none; }
227 /* The masthead is gone on mobile, so it can't carry the iOS safe-area top inset
228 (env() resolves to 0 in the home-screen PWA; --ios-safe-top is set by JS in shell).
229 Pad the body instead so the profile-header clears the Dynamic Island / notch. */
230 body:not(.on-admin) { padding-top: var(--ios-safe-top, 0px); }
231}
232
233/* On mobile, center the brand in the masthead */
234@media (max-width: 767px) {
235 .masthead-inner {
236 justify-content: center !important;
237 position: relative;
238 }
239 .masthead-brand {
240 text-align: center;
241 }
242 /* Lone theme button for logged-out users — minimal, icon-only,
243 no border/background to avoid the iOS sticky-hover look. */
244 .top-nav-mobile {
245 position: absolute;
246 right: 0;
247 top: 50%;
248 transform: translateY(-50%);
249 }
250 .top-nav-mobile .nav-btn {
251 width: 44px;
252 height: 44px;
253 padding: 0;
254 background: transparent !important;
255 border-color: transparent !important;
256 color: var(--ink-soft);
257 display: inline-flex;
258 align-items: center;
259 justify-content: center;
260 }
261 .top-nav-mobile .nav-btn:active {
262 transform: scale(0.92);
263 color: var(--ink);
264 }
265 /* Suppress sticky hover on touch — only show feedback for real hover devices */
266 @media (hover: none) {
267 .top-nav-mobile .nav-btn:hover {
268 background: transparent !important;
269 border-color: transparent !important;
270 color: var(--ink-soft);
271 }
272 }
273 .top-nav-desktop {
274 display: none !important;
275 }
276}
277
278/* On desktop, hide the mobile-only theme button */
279@media (min-width: 768px) {
280 .top-nav-mobile { display: none; }
281}
282
283/* ── Admin "back" link styling (inherited from v9 patterns) ── */
284.nav-link-admin {
285 background: var(--accent);
286 color: #fff !important;
287 padding: .5rem .85rem;
288 border-radius: 6px;
289 text-decoration: none;
290 font-family: var(--font-ui);
291 font-size: .875rem;
292 font-weight: 500;
293}
294.nav-link-admin:hover { opacity: .9; color: #fff !important; }
295
296/* ── Tap-target enforcement for all nav buttons on touch ── */
297@media (hover: none) {
298 .nav-btn, .nav-link {
299 min-height: 44px;
300 min-width: 44px;
301 }
302 .nav-btn:active {
303 transform: scale(0.95);
304 background: var(--paper-2);
305 }
306}
307
308/* ── User dropdown (desktop only — mobile uses bottom-tab → /account) ── */
309.user-menu { position: relative; list-style: none; }
310.user-menu > summary {
311 list-style: none; cursor: pointer;
312 padding: .25rem; border-radius: 6px;
313 display: inline-flex; align-items: center;
314 border: 1px solid transparent;
315 transition: all var(--transition);
316}
317.user-menu > summary::-webkit-details-marker { display: none; }
318.user-menu > summary::marker { content: ''; }
319.user-menu > summary:hover { background: var(--paper-2); border-color: var(--rule); }
320.user-avatar {
321 display: inline-flex; align-items: center; justify-content: center;
322 width: 28px; height: 28px; border-radius: 50%;
323 background: var(--paper-2); /* neutral so transparent avatars blend */
324 color: var(--accent); /* fallback initial color */
325 font-family: var(--font-ui); font-weight: 600; font-size: .8rem;
326 pointer-events: none;
327 overflow: hidden;
328}
329.user-avatar-img {
330 /* When the pill renders as <img> instead of <span>, fill it edge-to-edge. */
331 object-fit: cover;
332}
333.user-dropdown {
334 position: absolute; top: calc(100% + .55rem); right: 0;
335 min-width: 220px;
336 padding: .35rem;
337 /* Frosted glass: transparent + heavy blur so the frosted effect is clearly visible. */
338 background: color-mix(in srgb, var(--paper) 60%, transparent);
339 backdrop-filter: blur(34px) saturate(190%);
340 -webkit-backdrop-filter: blur(34px) saturate(190%);
341 border: 1px solid var(--rule);
342 border-radius: 12px;
343 box-shadow:
344 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent),
345 0 8px 24px color-mix(in srgb, var(--ink) 14%, transparent),
346 0 16px 48px color-mix(in srgb, var(--ink) 10%, transparent);
347 z-index: 200;
348 font-family: var(--font-ui);
349 transform-origin: top right;
350}
351/* Open animation — only fires when <details open> matches, so closing the
352 dropdown doesn't replay the entry animation. */
353.user-menu[open] > .user-dropdown {
354 animation: ud-fade-in .14s cubic-bezier(.2, .8, .25, 1);
355}
356@keyframes ud-fade-in {
357 from { opacity: 0; transform: translateY(-6px) scale(.98); }
358 to { opacity: 1; transform: translateY(0) scale(1); }
359}
360
361/* Identity header — small avatar + username/role badge */
362.user-dropdown-header {
363 display: flex; align-items: center; gap: .65rem;
364 padding: .55rem .65rem .65rem;
365 border-bottom: 1px solid color-mix(in srgb, var(--rule) 65%, transparent);
366 margin-bottom: .35rem;
367}
368.udh-avatar {
369 width: 36px; height: 36px; border-radius: 50%;
370 object-fit: cover;
371 background: var(--paper-2);
372 display: inline-flex; align-items: center; justify-content: center;
373 flex-shrink: 0;
374}
375.udh-avatar-fallback {
376 color: var(--accent); font-weight: 700; font-size: .95rem;
377}
378.udh-meta { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
379.udh-name {
380 font-weight: 600; font-size: .92rem; color: var(--ink);
381 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px;
382}
383.udh-role {
384 font-size: .68rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
385 color: var(--accent); opacity: .85; margin-top: .12rem;
386}
387
388/* Item list group */
389.user-dropdown-list { display: flex; flex-direction: column; gap: 1px; }
390.user-dropdown-divider {
391 height: 1px;
392 margin: .35rem .25rem;
393 background: color-mix(in srgb, var(--rule) 60%, transparent);
394}
395
396/* Items — icon + label, rounded inner highlight on hover (macOS feel) */
397.user-dropdown .udi {
398 display: flex; align-items: center; gap: .65rem;
399 padding: .5rem .65rem;
400 color: var(--ink); text-decoration: none;
401 font-size: .88rem; font-weight: 500;
402 border-radius: 7px;
403 transition: background var(--transition), color var(--transition), transform var(--transition);
404}
405.user-dropdown .udi:hover,
406.user-dropdown .udi:focus-visible {
407 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
408 color: var(--accent);
409 outline: none;
410}
411.user-dropdown .udi:active { transform: scale(.98); }
412.user-dropdown .udi-icon {
413 width: 16px; height: 16px;
414 opacity: .7;
415 flex-shrink: 0;
416 transition: opacity var(--transition);
417}
418.user-dropdown .udi:hover .udi-icon,
419.user-dropdown .udi:focus-visible .udi-icon { opacity: 1; }
420
421/* Logout: subtle danger tint on hover, never on rest. Uses a fixed red so
422 it always reads as a destructive action even if the user's accent is red. */
423.user-dropdown .udi-danger:hover,
424.user-dropdown .udi-danger:focus-visible {
425 background: color-mix(in srgb, #dc2626 14%, var(--paper-2));
426 color: #dc2626;
427}
428
429/* Het zoeken heeft zijn opmaak in partials/search-styles.ejs. */
430/* Notification bell + unread badge */
431.nav-notif { position: relative; }
432.notif-badge {
433 position: absolute; top: 1px; right: 1px;
434 min-width: 16px; height: 16px; padding: 0 4px; box-sizing: border-box;
435 display: inline-flex; align-items: center; justify-content: center;
436 background: #e0245e; color: #fff; font-size: 10px; font-weight: 700;
437 border-radius: 999px; line-height: 1; pointer-events: none;
438}
439</style>
Note: See TracBrowser for help on using the repository browser.