header layout (#455)

* fix: collapse excess tags with +N button and fix mobile menu overlap

- limits visible tags to 2 by default (never wraps to second line)
- shows "+N" button when more tags exist
- clicking "+N" expands to show all tags (allows wrapping when expanded)
- uses flex-wrap: nowrap + overflow: hidden to guarantee single-line constraint
- collapses back when track changes (component recycle)
- fix LinksMenu and ProfileMenu mobile positioning to avoid player overlap
- menus now position from bottom with player height offset on mobile

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: keep mobile menus centered but shift up for player

- menus stay full-size and centered
- shift center point up by half player height when player is open
- cap max-height to avoid overlap with player

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: add search trigger to header and mobile menu

desktop:
- SearchTrigger component shows magnifying glass + keyboard shortcut hint
- detects platform for correct shortcut (โŒ˜K on Mac, Ctrl+K on Windows/Linux)
- subtle styling with accent highlight on hover

mobile:
- search added as first item in ProfileMenu (three-dot menu)
- styled with accent tint background to stand out
- opens search modal and closes menu

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: hide portal link on portal page, move search to far right

- portal link (@handle) now hidden when already on /portal
- search trigger moved after logout button (far right)

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: reorder desktop header - search in nav, logout on far right

- search trigger now in nav: @handle โ†’ search โ†’ settings
- logout button moved to far right (mirroring stats on left)
- hide portal link when already on /portal page

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: swap search and portal link order in nav

nav order now: feed โ†’ liked โ†’ search โ†’ @handle โ†’ settings

๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* more stuff

* yuhhhhhhhh

---------

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 6570b21e 5b7012a6

Changed files
+113 -4
frontend
+47 -4
frontend/src/lib/components/Header.svelte
··· 5 5 import LinksMenu from './LinksMenu.svelte'; 6 6 import ProfileMenu from './ProfileMenu.svelte'; 7 7 import PlatformStats from './PlatformStats.svelte'; 8 + import SearchTrigger from './SearchTrigger.svelte'; 8 9 import { APP_NAME, APP_TAGLINE } from '$lib/branding'; 9 10 10 11 interface Props { ··· 17 18 </script> 18 19 19 20 <header> 20 - <!-- Stats positioned on far left, outside main header flow --> 21 + <!-- Stats and search positioned on far left, splitting margin into thirds --> 21 22 <div class="stats-left desktop-only"> 22 23 <PlatformStats variant="header" /> 24 + </div> 25 + <div class="search-left desktop-only"> 26 + <SearchTrigger /> 27 + </div> 28 + <!-- Logout positioned on far right, mirroring stats --> 29 + <div class="logout-right desktop-only"> 30 + <button onclick={onLogout} class="btn-logout-outer" title="log out">logout</button> 23 31 </div> 24 32 <div class="header-content"> 25 33 <div class="left-section"> ··· 108 116 <span>liked</span> 109 117 </a> 110 118 {/if} 111 - <a href="/portal" class="user-handle" title="go to portal">@{user?.handle}</a> 119 + {#if $page.url.pathname !== '/portal'} 120 + <a href="/portal" class="user-handle" title="go to portal">@{user?.handle}</a> 121 + {/if} 112 122 <SettingsMenu /> 113 - <button onclick={onLogout} class="btn-logout" title="log out">logout</button> 114 123 </div> 115 124 116 125 <!-- Mobile nav: just ProfileMenu --> ··· 208 217 209 218 .stats-left { 210 219 position: absolute; 211 - left: calc((100vw - var(--queue-width, 0px) - 800px) / 4); 220 + left: calc((100vw - var(--queue-width, 0px) - 800px) / 6); 221 + top: 50%; 222 + transform: translate(-50%, -50%); 223 + transition: left 0.3s ease; 224 + } 225 + 226 + .search-left { 227 + position: absolute; 228 + left: calc((100vw - var(--queue-width, 0px) - 800px) / 3); 212 229 top: 50%; 213 230 transform: translate(-50%, -50%); 214 231 transition: left 0.3s ease; 232 + } 233 + 234 + .logout-right { 235 + position: absolute; 236 + right: calc((100vw - var(--queue-width, 0px) - 800px) / 4); 237 + top: 50%; 238 + transform: translate(50%, -50%); 239 + transition: right 0.3s ease; 240 + } 241 + 242 + .btn-logout-outer { 243 + background: transparent; 244 + border: 1px solid var(--border-emphasis); 245 + color: var(--text-secondary); 246 + padding: 0.5rem 1rem; 247 + border-radius: 6px; 248 + font-size: 0.9rem; 249 + font-family: inherit; 250 + cursor: pointer; 251 + transition: all 0.2s; 252 + white-space: nowrap; 253 + } 254 + 255 + .btn-logout-outer:hover { 256 + border-color: var(--accent); 257 + color: var(--accent); 215 258 } 216 259 217 260 .bluesky-link,
+27
frontend/src/lib/components/ProfileMenu.svelte
··· 3 3 import { page } from '$app/stores'; 4 4 import { queue } from '$lib/queue.svelte'; 5 5 import { preferences, type Theme } from '$lib/preferences.svelte'; 6 + import { search } from '$lib/search.svelte'; 6 7 import type { User } from '$lib/types'; 7 8 8 9 interface Props { ··· 104 105 closeMenu(); 105 106 await onLogout(); 106 107 } 108 + 109 + function openSearch() { 110 + closeMenu(); 111 + search.open(); 112 + } 107 113 </script> 108 114 109 115 <div class="profile-menu"> ··· 132 138 133 139 {#if !showSettings} 134 140 <nav class="menu-items"> 141 + <button class="menu-item search-item" onclick={openSearch}> 142 + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 143 + <circle cx="11" cy="11" r="8"></circle> 144 + <line x1="21" y1="21" x2="16.65" y2="16.65"></line> 145 + </svg> 146 + <div class="item-content"> 147 + <span class="item-title">search</span> 148 + <span class="item-subtitle">tracks, artists, albums, tags</span> 149 + </div> 150 + </button> 151 + 135 152 {#if !isOnPortal} 136 153 <a href="/portal" class="menu-item" onclick={closeMenu}> 137 154 <svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"> ··· 396 413 397 414 .menu-item.logout:hover svg:first-child { 398 415 color: var(--error); 416 + } 417 + 418 + .menu-item.search-item { 419 + background: color-mix(in srgb, var(--accent) 8%, transparent); 420 + border: 1px solid color-mix(in srgb, var(--accent) 20%, transparent); 421 + } 422 + 423 + .menu-item.search-item:hover { 424 + background: color-mix(in srgb, var(--accent) 15%, transparent); 425 + border-color: color-mix(in srgb, var(--accent) 40%, transparent); 399 426 } 400 427 401 428 .item-content {
+39
frontend/src/lib/components/SearchTrigger.svelte
··· 1 + <script lang="ts"> 2 + import { search } from '$lib/search.svelte'; 3 + </script> 4 + 5 + <button class="search-trigger" onclick={() => search.open()} title="search (โŒ˜K)"> 6 + <svg 7 + width="16" 8 + height="16" 9 + viewBox="0 0 24 24" 10 + fill="none" 11 + stroke="currentColor" 12 + stroke-width="2" 13 + stroke-linecap="round" 14 + stroke-linejoin="round" 15 + > 16 + <circle cx="11" cy="11" r="8"></circle> 17 + <line x1="21" y1="21" x2="16.65" y2="16.65"></line> 18 + </svg> 19 + </button> 20 + 21 + <style> 22 + .search-trigger { 23 + background: transparent; 24 + border: 1px solid var(--border-default); 25 + color: var(--text-secondary); 26 + padding: 0.5rem; 27 + border-radius: 4px; 28 + cursor: pointer; 29 + transition: all 0.2s; 30 + display: flex; 31 + align-items: center; 32 + justify-content: center; 33 + } 34 + 35 + .search-trigger:hover { 36 + color: var(--accent); 37 + border-color: var(--accent); 38 + } 39 + </style>