source: Klonkt/src/views/partials/topnav.ejs@ 5e95aac

main
Last change on this file since 5e95aac was 8afbdd6, checked in by roboburr <roboburr@…>, 3 months ago

feat: viewer role + artists directory + Klonkt Hub Beta rebrand

Viewer role (replaces the separate view-mode/readonly toggle):

  • 'kijker' is now a real role (VALID_ROLES) selectable in Admin. May view EVERYTHING including Admin, but cannot modify anything.
  • isViewer(user) (= role kijker or legacy readonly flag) is the source; requireGod/requireSiteManager(BySlug) let a viewer through (viewing), the global guard 403s every write. Existing readonly accounts are migrated on each role change (readonly=0).
  • Write leaks via GET patched: /prutter/new (INSERT) blocks for viewers, /prutter/:id skips markAsRead (UPDATE); WS upgrade rejects viewers (the HTTP guard doesn't cover WebSockets).
  • Clean "Viewer mode" page (viewer-blocked) instead of raw 403 text; styled sticky banner; account page shows read-only UI instead of an upload button that silently 403s. canMutate hides write buttons.

Scalability (>50 artists):

  • Hub home shows max 24 (most active first) + "All N artists ->".
  • New searchable, paginated /artiesten directory (hub only).
  • 'user' + 'artiesten' reserved as slugs.

Rebrand PrutFolio v1 -> Klonkt Hub Beta (footer, PWA manifest, account/
admin texts, default page title, startup, README; internal package +
PWA id 'prutfolio' remain for stability).

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

  • Property mode set to 100644
File size: 20.2 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 / Prutter / 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;
16const _hasSearch = !site || site.show_search === undefined || site.show_search;
17const _hasPrutter = user && site && site.enable_prutter && (typeof tenancy !== 'undefined' && tenancy === 'hub');
18const _canPost = user && (typeof canMutate === 'undefined' || canMutate)
19 && permissions && permissions.canCreatePost && permissions.canCreatePost(user, site);
20%>
21
22<header class="masthead">
23 <div class="container masthead-inner">
24
25 <!-- Brand block (always visible) -->
26 <div class="masthead-brand">
27 <% if (_isAdmin) { %>
28 <a href="<%= _siteUrlBase %>/" class="nav-link nav-link-admin">← Terug naar site</a>
29 <% } else if (typeof tenancy !== 'undefined' && tenancy === 'hub') { %>
30 <%# Hub: de brand is de hub-naam en gaat naar de hub-home (volledige nav, geen
31 HTMX, zodat de headerloze overview correct rendert). Eigen class want
32 .site-title is overal display:none. %>
33 <a class="nav-hub-home" href="/" aria-label="Naar de hub-home"><%= (typeof hubTitle !== 'undefined' && hubTitle) ? hubTitle : 'Home' %></a>
34 <% } else if (site) { %>
35 <a class="site-title" href="<%= _siteUrlBase %>/"
36 hx-get="<%= _siteUrlBase %>/?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
37 hx-push-url="<%= _siteUrlBase %>/" hx-indicator="#pcms-loading"><%= site.title %></a>
38 <% } else { %>
39 <a class="site-title" href="/">Klonkt</a>
40 <% } %>
41 </div>
42
43 <!-- Mobile: theme toggle ONLY for logged-out users.
44 Logged-in users get the toggle inside the profile sheet
45 (bottom-tab → Profiel → Thema). All other actions in bottom-tab. -->
46 <% if (!user) { %>
47 <div class="top-nav-mobile">
48 <button type="button" class="nav-btn" id="theme-toggle-mobile" aria-label="Thema wisselen" title="Thema wisselen">
49 <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>
50 </button>
51 </div>
52 <% } %>
53
54 <!-- Desktop-only: full action row -->
55 <nav class="top-nav top-nav-desktop" aria-label="Hoofdnavigatie">
56
57 <% if (!_isAdmin) { %>
58 <a class="nav-link" href="<%= _siteUrlBase %>/archive"
59 hx-get="<%= _siteUrlBase %>/archive?partial=1" hx-target="#pcms-main"
60 hx-swap="innerHTML" hx-push-url="<%= _siteUrlBase %>/archive"
61 hx-indicator="#pcms-loading">
62 <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>
63 <span class="nav-label">Archief</span>
64 </a>
65 <% } %>
66
67 <% if (_hasSearch) { %>
68 <button type="button" class="nav-btn" id="search-toggle" aria-label="Zoeken" title="Zoeken">
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"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
70 </button>
71 <% } %>
72
73 <button type="button" class="nav-btn" id="pwa-install-btn" hidden aria-label="App installeren" title="App installeren">
74 <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>
75 </button>
76
77 <button type="button" class="nav-btn" id="theme-toggle" aria-label="Thema wisselen" title="Thema wisselen">
78 <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>
79 </button>
80
81 <% if (_hasPrutter) { %>
82 <a class="nav-btn" href="<%= _siteUrlBase %>/prutter" aria-label="Prutter (DMs)" title="Prutter">
83 <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 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
84 </a>
85 <% } %>
86
87 <% if (user) { %>
88 <details class="user-menu">
89 <summary class="nav-btn user-toggle" aria-label="Gebruikersmenu">
90 <% if (user.avatar_url) { %>
91 <img class="user-avatar user-avatar-img" src="<%= user.avatar_url %>" alt="">
92 <% } else { %>
93 <span class="user-avatar"><%= user.username.charAt(0).toUpperCase() %></span>
94 <% } %>
95 </summary>
96 <div class="user-dropdown" role="menu">
97 <div class="user-dropdown-header">
98 <% if (user.avatar_url) { %>
99 <img class="udh-avatar" src="<%= user.avatar_url %>" alt="">
100 <% } else { %>
101 <span class="udh-avatar udh-avatar-fallback"><%= user.username.charAt(0).toUpperCase() %></span>
102 <% } %>
103 <div class="udh-meta">
104 <span class="udh-name"><%= user.username %></span>
105 <% if (user.role === 'god') { %>
106 <span class="udh-role">Admin</span>
107 <% } else if (user.role === 'admin' || user.role === 'moderator') { %>
108 <span class="udh-role"><%= user.role.charAt(0).toUpperCase() + user.role.slice(1) %></span>
109 <% } %>
110 </div>
111 </div>
112 <div class="user-dropdown-list">
113 <a href="/account" role="menuitem" class="udi">
114 <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>
115 <span>Account</span>
116 </a>
117 <% if (user.role === 'god' || user.role === 'admin' || (typeof userOwnsSite !== 'undefined' && userOwnsSite)) { %>
118 <a href="/admin" role="menuitem" class="udi">
119 <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>
120 <span>Beheer</span>
121 </a>
122 <% } %>
123 <% if (_canPost) { %>
124 <a href="<%= _siteUrlBase %>/posts/new" role="menuitem" class="udi"
125 hx-get="<%= _siteUrlBase %>/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
126 hx-push-url="<%= _siteUrlBase %>/posts/new" hx-indicator="#pcms-loading">
127 <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>
128 <span>Nieuwe post</span>
129 </a>
130 <% } %>
131 </div>
132 <div class="user-dropdown-divider" aria-hidden="true"></div>
133 <div class="user-dropdown-list">
134 <a href="/auth/logout" role="menuitem" class="udi udi-danger">
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="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>
136 <span>Uitloggen</span>
137 </a>
138 </div>
139 </div>
140 </details>
141 <% } else { %>
142 <a class="nav-link" href="/auth/login">
143 <span class="nav-label">Inloggen</span>
144 </a>
145 <% } %>
146
147 </nav>
148 </div>
149</header>
150
151<!-- Search overlay (shared by mobile bottom-tab + desktop top-nav) -->
152<div id="search-overlay" class="search-overlay" hidden>
153 <div class="container">
154 <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search">
155 <input type="search" name="q" placeholder="Zoeken in posts…" autocomplete="off">
156 <button type="submit" class="nav-btn" aria-label="Zoeken">
157 <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>
158 </button>
159 <button type="button" class="nav-btn" id="search-close" aria-label="Sluiten">
160 <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>
161 </button>
162 </form>
163 </div>
164</div>
165
166<script>
167(function() {
168 // Theme toggle (works for both mobile + desktop buttons)
169 function toggleTheme() {
170 const cur = document.documentElement.getAttribute('data-theme') || 'dark';
171 const next = cur === 'dark' ? 'light' : 'dark';
172 document.documentElement.setAttribute('data-theme', next);
173 try { localStorage.setItem('pcms-theme', next); } catch (e) {}
174 }
175 // Theme toggle — event delegation so it works for ANY button with the
176 // matching ids, even ones rendered later in the page (footer is below
177 // topnav in source order, so getElementById would have returned null
178 // when this IIFE ran).
179 document.body.addEventListener('click', function(e) {
180 const btn = e.target.closest(
181 '#theme-toggle, #theme-toggle-mobile, #theme-toggle-footer'
182 );
183 if (btn) toggleTheme();
184 });
185
186 // Search overlay
187 const overlay = document.getElementById('search-overlay');
188 document.getElementById('search-toggle')?.addEventListener('click', function() {
189 if (overlay) {
190 overlay.hidden = false;
191 const inp = overlay.querySelector('input');
192 if (inp) inp.focus();
193 }
194 });
195 document.getElementById('search-close')?.addEventListener('click', function() {
196 if (overlay) overlay.hidden = true;
197 });
198 document.addEventListener('keydown', function(e) {
199 if (e.key === 'Escape' && overlay && !overlay.hidden) overlay.hidden = true;
200 });
201
202 // Close user dropdown on outside click
203 document.addEventListener('click', function(e) {
204 const open = document.querySelector('.user-menu[open]');
205 if (open && !open.contains(e.target)) open.removeAttribute('open');
206 });
207})();
208</script>
209
210<style>
211/* ============================================================
212 TOP-NAV — responsive
213 Mobile: brand centered, small theme btn right, NOTHING else.
214 Desktop (≥768px): full v9 layout — brand left, action row right.
215 ============================================================ */
216
217.masthead-inner {
218 /* Override v9's flex-end → space-between so brand is on the left */
219 justify-content: space-between !important;
220 gap: 1rem;
221}
222.masthead-brand .site-title,
223.masthead-brand .nav-link-admin {
224 display: inline-block;
225 vertical-align: middle;
226}
227
228/* Site title is suppressed everywhere — the profile-header carries the brand
229 on content pages, and admin pages have their own "← Terug naar site" link
230 (a different selector, .nav-link-admin, which stays visible). The masthead
231 becomes a thin utility strip with just theme-toggle / search / etc. */
232.masthead-brand .site-title {
233 display: none;
234}
235
236/* Hub-home brand is WÉL zichtbaar — de thuisknop op artiest-/hub-pagina's. */
237.masthead-brand .nav-hub-home {
238 display: inline-block; vertical-align: middle;
239 color: var(--ink); text-decoration: none;
240 font-family: var(--font-display, serif); font-weight: 700; font-size: 1.05rem;
241 transition: color 120ms;
242}
243.masthead-brand .nav-hub-home:hover { color: var(--accent); }
244
245/* On mobile, hide the entire masthead except for admin pages. The brand
246 appears in the profile-header on content pages, and the bottom-tab
247 provides all navigation. Admin keeps it because the "← Terug naar site"
248 link is the only way back to the public site from /admin/*. */
249@media (max-width: 767px) {
250 body:not(.on-admin) .masthead { display: none; }
251}
252
253/* On mobile, center the brand in the masthead */
254@media (max-width: 767px) {
255 .masthead-inner {
256 justify-content: center !important;
257 position: relative;
258 }
259 .masthead-brand {
260 text-align: center;
261 }
262 /* Lone theme button for logged-out users — minimal, icon-only,
263 no border/background to avoid the iOS sticky-hover look. */
264 .top-nav-mobile {
265 position: absolute;
266 right: 0;
267 top: 50%;
268 transform: translateY(-50%);
269 }
270 .top-nav-mobile .nav-btn {
271 width: 44px;
272 height: 44px;
273 padding: 0;
274 background: transparent !important;
275 border-color: transparent !important;
276 color: var(--ink-soft);
277 display: inline-flex;
278 align-items: center;
279 justify-content: center;
280 }
281 .top-nav-mobile .nav-btn:active {
282 transform: scale(0.92);
283 color: var(--ink);
284 }
285 /* Suppress sticky hover on touch — only show feedback for real hover devices */
286 @media (hover: none) {
287 .top-nav-mobile .nav-btn:hover {
288 background: transparent !important;
289 border-color: transparent !important;
290 color: var(--ink-soft);
291 }
292 }
293 .top-nav-desktop {
294 display: none !important;
295 }
296}
297
298/* On desktop, hide the mobile-only theme button */
299@media (min-width: 768px) {
300 .top-nav-mobile { display: none; }
301}
302
303/* ── Admin "back" link styling (inherited from v9 patterns) ── */
304.nav-link-admin {
305 background: var(--accent);
306 color: #fff !important;
307 padding: .5rem .85rem;
308 border-radius: 6px;
309 text-decoration: none;
310 font-family: var(--font-ui);
311 font-size: .875rem;
312 font-weight: 500;
313}
314.nav-link-admin:hover { opacity: .9; color: #fff !important; }
315
316/* ── Tap-target enforcement for all nav buttons on touch ── */
317@media (hover: none) {
318 .nav-btn, .nav-link {
319 min-height: 44px;
320 min-width: 44px;
321 }
322 .nav-btn:active {
323 transform: scale(0.95);
324 background: var(--paper-2);
325 }
326}
327
328/* ── User dropdown (desktop only — mobile uses bottom-tab → /account) ── */
329.user-menu { position: relative; list-style: none; }
330.user-menu > summary {
331 list-style: none; cursor: pointer;
332 padding: .25rem; border-radius: 6px;
333 display: inline-flex; align-items: center;
334 border: 1px solid transparent;
335 transition: all var(--transition);
336}
337.user-menu > summary::-webkit-details-marker { display: none; }
338.user-menu > summary::marker { content: ''; }
339.user-menu > summary:hover { background: var(--paper-2); border-color: var(--rule); }
340.user-avatar {
341 display: inline-flex; align-items: center; justify-content: center;
342 width: 28px; height: 28px; border-radius: 50%;
343 background: var(--paper-2); /* neutral so transparent avatars blend */
344 color: var(--accent); /* fallback initial color */
345 font-family: var(--font-ui); font-weight: 600; font-size: .8rem;
346 pointer-events: none;
347 overflow: hidden;
348}
349.user-avatar-img {
350 /* When the pill renders as <img> instead of <span>, fill it edge-to-edge. */
351 object-fit: cover;
352}
353.user-dropdown {
354 position: absolute; top: calc(100% + .55rem); right: 0;
355 min-width: 220px;
356 padding: .35rem;
357 background: color-mix(in srgb, var(--paper) 92%, transparent);
358 backdrop-filter: blur(14px) saturate(180%);
359 -webkit-backdrop-filter: blur(14px) saturate(180%);
360 border: 1px solid var(--rule);
361 border-radius: 12px;
362 box-shadow:
363 0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent),
364 0 8px 24px color-mix(in srgb, var(--ink) 14%, transparent),
365 0 16px 48px color-mix(in srgb, var(--ink) 10%, transparent);
366 z-index: 200;
367 font-family: var(--font-ui);
368 transform-origin: top right;
369}
370/* Open animation — only fires when <details open> matches, so closing the
371 dropdown doesn't replay the entry animation. */
372.user-menu[open] > .user-dropdown {
373 animation: ud-fade-in .14s cubic-bezier(.2, .8, .25, 1);
374}
375@keyframes ud-fade-in {
376 from { opacity: 0; transform: translateY(-6px) scale(.98); }
377 to { opacity: 1; transform: translateY(0) scale(1); }
378}
379
380/* Identity header — small avatar + username/role badge */
381.user-dropdown-header {
382 display: flex; align-items: center; gap: .65rem;
383 padding: .55rem .65rem .65rem;
384 border-bottom: 1px solid color-mix(in srgb, var(--rule) 65%, transparent);
385 margin-bottom: .35rem;
386}
387.udh-avatar {
388 width: 36px; height: 36px; border-radius: 50%;
389 object-fit: cover;
390 background: var(--paper-2);
391 display: inline-flex; align-items: center; justify-content: center;
392 flex-shrink: 0;
393}
394.udh-avatar-fallback {
395 color: var(--accent); font-weight: 700; font-size: .95rem;
396}
397.udh-meta { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
398.udh-name {
399 font-weight: 600; font-size: .92rem; color: var(--ink);
400 white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 140px;
401}
402.udh-role {
403 font-size: .68rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
404 color: var(--accent); opacity: .85; margin-top: .12rem;
405}
406
407/* Item list group */
408.user-dropdown-list { display: flex; flex-direction: column; gap: 1px; }
409.user-dropdown-divider {
410 height: 1px;
411 margin: .35rem .25rem;
412 background: color-mix(in srgb, var(--rule) 60%, transparent);
413}
414
415/* Items — icon + label, rounded inner highlight on hover (macOS feel) */
416.user-dropdown .udi {
417 display: flex; align-items: center; gap: .65rem;
418 padding: .5rem .65rem;
419 color: var(--ink); text-decoration: none;
420 font-size: .88rem; font-weight: 500;
421 border-radius: 7px;
422 transition: background var(--transition), color var(--transition), transform var(--transition);
423}
424.user-dropdown .udi:hover,
425.user-dropdown .udi:focus-visible {
426 background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
427 color: var(--accent);
428 outline: none;
429}
430.user-dropdown .udi:active { transform: scale(.98); }
431.user-dropdown .udi-icon {
432 width: 16px; height: 16px;
433 opacity: .7;
434 flex-shrink: 0;
435 transition: opacity var(--transition);
436}
437.user-dropdown .udi:hover .udi-icon,
438.user-dropdown .udi:focus-visible .udi-icon { opacity: 1; }
439
440/* Logout: subtle danger tint on hover, never on rest. Uses a fixed red so
441 it always reads as a destructive action even if the user's accent is red. */
442.user-dropdown .udi-danger:hover,
443.user-dropdown .udi-danger:focus-visible {
444 background: color-mix(in srgb, #dc2626 14%, var(--paper-2));
445 color: #dc2626;
446}
447
448/* ── Search overlay ── */
449.search-overlay {
450 position: fixed; top: 0; left: 0; right: 0;
451 background: color-mix(in srgb, var(--paper) 96%, transparent);
452 backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
453 border-bottom: 1px solid var(--rule);
454 padding: 1rem 0; z-index: 200;
455 padding-top: calc(1rem + env(safe-area-inset-top, 0));
456 box-shadow: 0 8px 24px color-mix(in srgb, var(--ink) 8%, transparent);
457}
458.search-overlay-form { display: flex; gap: .5rem; align-items: center; }
459.search-overlay-form input {
460 flex: 1; padding: .85rem 1rem;
461 min-height: 44px;
462 border: 1px solid var(--rule); border-radius: 8px;
463 font-family: var(--font-ui); font-size: 1rem;
464 background: var(--paper); color: var(--ink);
465}
466.search-overlay-form input:focus {
467 outline: 2px solid var(--accent); outline-offset: 0;
468}
469.search-overlay-form .nav-btn {
470 width: 44px; height: 44px;
471 display: inline-flex; align-items: center; justify-content: center;
472}
473</style>
Note: See TracBrowser for help on using the repository browser.