source: Klonkt/src/views/partials/topnav.ejs@ 746d98a

main
Last change on this file since 746d98a was 746d98a, checked in by Robin <roboburr@…>, 2 hours ago

Wardmodus: een besloten Klonkt met alleen het profiel openbaar

Robin, 30-9: "Ward Profile mode gaat actief wanneer ward gekoppeld is, dus
wanneer de persoon guardian(s) heeft. Een besloten Klonkt met enkel het profiel
van de ward publiek. Wij denken dat dit het beste valt bij emancipation om de
functie weer naar normaal Klonkt te brengen."

AFGELEID, NIET OPGESLAGEN. Heeft de site geaccepteerde guardians, dan staat de
modus aan; is de set leeg, dan uit. listGuardians telt alleen geaccepteerde
guardianships, dus een voorstel zet niets dicht, en de set leegmaken is
emancipatie (FEP-633c 3.4) -- precies waar het ontwerp de weg terug legt. Een
schakelaar zou een tweede waarheid naast de guardianship zijn.

STANDAARD DICHT (middleware/ward-profile.js, in server.js direct na de
siteresolver en de schrijfpoort). Een bezoeker ziet op / het profiel en verder
niets: een pagina gaat terug naar het profiel, al het andere is 404, een POST
bereikt zijn handler niet, en een htmx-navigatie krijgt HX-Redirect in plaats
van een brokstuk. Wat niet op de lijst staat komt er niet langs, zodat een
nieuwe route niet vanzelf een kind openzet.

Open blijft: inloggen, account, beheer, het guardianpaneel, /lang, de bestanden
van de pagina zelf, en /audio/stream -- die bewaakt elk bestand zelf, en een
track die de ward op de fediverse opende moet daar speelbaar blijven. De
federatie (ActivityPub, WebFinger, OAuth voor Shaer, /media, /assets, /og) hangt
al VOOR de poort; de paden staan toch op de lijst, voor als de volgorde ooit
omgaat, want dan zou een ward stil van de fediverse verdwijnen.

Wie alles ziet: de ward zelf, en god, kijker en sitebeheer. Een andere
ingelogde gebruiker is gewoon een bezoeker.

DE PROFIELPAGINA (pages/ward-profile.ejs) mag verzorgd, want dit is alles wat
een bezoeker ziet: foto met een ring in de accentkleur, de naam groot, de
handle op een pil, de bio, de links als brede knoppen in hun merkkleur, een
volgknop die de bestaande fediverse-dialoog opent (een volgverzoek aan een ward
gaat eerst langs de guardians), en een neutrale regel dat berichten alleen
voor volgers zijn. Nergens "kind" of "ward": dat de account guardians heeft
staat al openbaar op de fediverse. Dezelfde bronnen als de profielkop, zodat
het profiel op geen twee plekken iets anders kan zeggen. De chrome biedt een
bezoeker niets aan wat dicht is: geen zoekknop, geen archief, geen
Lezen/Grid-schakelaar.

DE PROFIELLINKS verhuizen van Appearance naar het account (/account#links),
bij naam, bio en foto. Twee vallen onderweg, allebei getoetst:

  • Appearance schreef profile_links altijd, uit de formuliervelden. Met de velden weg zou elke keer opslaan daar de links wissen. Die pagina schrijft ze niet meer.
  • ownedSite haalde profile_links niet op, dus de accountpagina toonde nul links en opslaan wiste ze. Gevonden bij het bekijken, niet door een toets; nu wel.

buildProfileLinks staat in PlatformIcons.js, en de rijen-knoppen in een eigen
module (mod/profile-links.js).

Getoetst: gewone Klonkt zonder guardians, een voorstel zet niets dicht,
gekoppeld alleen het profiel, pagina's terug, 404 voor wat geen pagina is, geen
handler voor een POST, HX-Redirect, wat open moet blijft open, ward en beheer
zien alles, een andere ingelogde is een bezoeker, emancipatie maakt het weer
gewoon, de links bij het account en Appearance wist ze niet. Tegenproef per
zaak. Volledige suite 1325 groen.

Nagekeken in de browser op 375 en 1280 pixels: het profiel past op een
telefoonscherm, knoppen van 55 pixels, geen overloop, de volgknop opent de
dialoog; de accountpagina toont de bestaande links en voegt er een toe.

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

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