base.html
·
338 строк
·
17.9 KB
| 1 | <!DOCTYPE html> |
| 2 | <html lang="{{.Lang}}" data-theme="{{if .Theme}}{{.Theme}}{{else}}system{{end}}" data-copied-text="{{t .Lang "common.copied"}}"> |
| 3 | <head> |
| 4 | <meta charset="utf-8"> |
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 6 | <link rel="icon" type="image/svg+xml" href="/static/favicon.svg"> |
| 7 | <title>{{if .Title}}{{.Title}} · Polar{{else}}Polar{{end}}</title> |
| 8 | <meta name="description" content="{{t .Lang "app.tagline"}}"> |
| 9 | <script> |
| 10 | (function(){ |
| 11 | try { |
| 12 | var saved = localStorage.getItem('polar_theme'); |
| 13 | if (saved && ['light','dark','system'].indexOf(saved) !== -1) { |
| 14 | document.documentElement.setAttribute('data-theme', saved); |
| 15 | } |
| 16 | var sb = localStorage.getItem('polar_sidebar'); |
| 17 | if (sb === 'collapsed') { |
| 18 | document.documentElement.setAttribute('data-sidebar', 'collapsed'); |
| 19 | } |
| 20 | } catch(e) {} |
| 21 | })(); |
| 22 | </script> |
| 23 | <style>{{inlineCSS}}</style> |
| 24 | <link rel="stylesheet" href="/static/css/app.css?v={{assetVer}}"> |
| 25 | </head> |
| 26 | <body> |
| 27 | {{sprite}} |
| 28 | |
| 29 | <!-- Floating button to expand sidebar when collapsed --> |
| 30 | <button type="button" class="sidebar-expand-btn" data-toggle-sidebar title="{{t .Lang "nav.toggle_sidebar"}} (Ctrl+B)" aria-label="{{t .Lang "nav.toggle_sidebar"}}"> |
| 31 | {{icon "sidebar" "size-4"}} |
| 32 | </button> |
| 33 | |
| 34 | <div class="app-shell"> |
| 35 | <!-- Left Sidebar --> |
| 36 | <aside class="app-sidebar" aria-label="Sidebar"> |
| 37 | <div class="sidebar-inner"> |
| 38 | <div class="flex flex-col gap-2 min-w-0"> |
| 39 | <!-- Brand, Language & Collapse Sidebar --> |
| 40 | <div class="flex items-center justify-between px-3 py-2" style="padding:0.25rem 0.35rem;"> |
| 41 | <a href="/" class="font-semibold text-base tracking-tight" style="letter-spacing:-0.02em;">Polar</a> |
| 42 | <div class="flex items-center gap-1"> |
| 43 | {{if eq .Lang "en"}} |
| 44 | <a href="?lang=ru" class="btn btn-ghost btn-sm font-mono" title="{{t .Lang "nav.lang"}}">RU</a> |
| 45 | {{else}} |
| 46 | <a href="?lang=en" class="btn btn-ghost btn-sm font-mono" title="{{t .Lang "nav.lang"}}">EN</a> |
| 47 | {{end}} |
| 48 | <button type="button" class="btn btn-ghost btn-icon" style="width:1.65rem;height:1.65rem;" data-toggle-sidebar title="{{t .Lang "nav.toggle_sidebar"}} (Ctrl+B)" aria-label="{{t .Lang "nav.toggle_sidebar"}}"> |
| 49 | {{icon "sidebar" "size-4"}} |
| 50 | </button> |
| 51 | </div> |
| 52 | </div> |
| 53 | |
| 54 | <!-- Search / Command Palette Trigger --> |
| 55 | <button type="button" class="btn btn-outline w-full justify-between text-muted" style="padding:0 0.55rem;height:1.95rem;" data-open-palette=""> |
| 56 | <span class="flex items-center gap-2 text-xs"> |
| 57 | {{icon "search" "size-3.5"}} |
| 58 | <span>{{t .Lang "nav.search_placeholder"}}</span> |
| 59 | </span> |
| 60 | <kbd class="kbd">⌘K</kbd> |
| 61 | </button> |
| 62 | |
| 63 | <!-- Primary Navigation --> |
| 64 | <nav class="flex flex-col gap-1 mt-1"> |
| 65 | <a href="/" class="nav-item"> |
| 66 | <span class="flex items-center gap-2">{{icon "home" "size-4"}} {{t .Lang "nav.home"}}</span> |
| 67 | </a> |
| 68 | {{if .ExploreEnabled}} |
| 69 | <a href="/explore" class="nav-item"> |
| 70 | <span class="flex items-center gap-2">{{icon "globe" "size-4"}} {{t .Lang "nav.explore"}}</span> |
| 71 | </a> |
| 72 | {{end}} |
| 73 | {{if .CurrentUser}} |
| 74 | <a href="/settings" class="nav-item"> |
| 75 | <span class="flex items-center gap-2">{{icon "settings" "size-4"}} {{t .Lang "nav.settings"}}</span> |
| 76 | </a> |
| 77 | {{if .CurrentUser.IsAdmin}} |
| 78 | <a href="/admin" class="nav-item"> |
| 79 | <span class="flex items-center gap-2">{{icon "shield" "size-4"}} {{t .Lang "nav.admin"}}</span> |
| 80 | </a> |
| 81 | {{end}} |
| 82 | {{end}} |
| 83 | </nav> |
| 84 | |
| 85 | </div> |
| 86 | |
| 87 | <!-- Sidebar Bottom: 3-Theme Switcher + Account --> |
| 88 | <div class="flex flex-col gap-2 pt-2" style="border-top:1px solid hsl(var(--border));"> |
| 89 | <div class="segmented" role="radiogroup" aria-label="{{t .Lang "nav.theme"}}"> |
| 90 | <button type="button" class="segmented-btn {{if eq .Theme "light"}}active{{end}}" data-set-theme="light" title="{{t .Lang "theme.light"}}"> |
| 91 | {{icon "sun" "size-3.5"}} |
| 92 | <span>{{t .Lang "theme.light"}}</span> |
| 93 | </button> |
| 94 | <button type="button" class="segmented-btn {{if eq .Theme "dark"}}active{{end}}" data-set-theme="dark" title="{{t .Lang "theme.dark"}}"> |
| 95 | {{icon "moon" "size-3.5"}} |
| 96 | <span>{{t .Lang "theme.dark"}}</span> |
| 97 | </button> |
| 98 | <button type="button" class="segmented-btn {{if eq .Theme "system"}}active{{end}}" data-set-theme="system" title="{{t .Lang "theme.system"}}"> |
| 99 | {{icon "laptop" "size-3.5"}} |
| 100 | <span>{{t .Lang "theme.system"}}</span> |
| 101 | </button> |
| 102 | </div> |
| 103 | |
| 104 | {{if .CurrentUser}} |
| 105 | <div class="flex items-center justify-between gap-1" style="padding:0.15rem 0.25rem;"> |
| 106 | <a href="/{{.CurrentUser.Username}}" class="flex items-center gap-2 min-w-0 truncate text-sm font-medium"> |
| 107 | {{avatar .CurrentUser.Username .CurrentUser.AvatarURL}} |
| 108 | <span class="truncate">{{.CurrentUser.Username}}</span> |
| 109 | </a> |
| 110 | <div class="flex items-center gap-1"> |
| 111 | <form method="post" action="/logout" style="margin:0;"> |
| 112 | <input type="hidden" name="_csrf" value="{{.CSRFToken}}"> |
| 113 | <button type="submit" class="btn btn-ghost btn-icon" style="width:1.65rem;height:1.65rem;" title="{{t .Lang "nav.logout"}}" aria-label="{{t .Lang "nav.logout"}}"> |
| 114 | {{icon "log-out" "size-3.5"}} |
| 115 | </button> |
| 116 | </form> |
| 117 | </div> |
| 118 | </div> |
| 119 | {{else}} |
| 120 | <div class="flex flex-col gap-1.5"> |
| 121 | <a href="/login" class="btn btn-outline btn-sm w-full">{{t .Lang "nav.login"}}</a> |
| 122 | {{if .RegistrationEnabled}} |
| 123 | <a href="/register" class="btn btn-primary btn-sm w-full">{{t .Lang "nav.register"}}</a> |
| 124 | {{end}} |
| 125 | </div> |
| 126 | {{end}} |
| 127 | </div> |
| 128 | </div> |
| 129 | </aside> |
| 130 | |
| 131 | <!-- Main Content Column --> |
| 132 | <div class="app-col"> |
| 133 | <!-- Mobile Topbar (only visible on small screens) --> |
| 134 | <div class="mobile-topbar"> |
| 135 | <div class="flex items-center gap-2"> |
| 136 | <button type="button" class="btn btn-ghost btn-icon" data-drawer-open aria-label="Menu"> |
| 137 | {{icon "menu" "size-4"}} |
| 138 | </button> |
| 139 | <a href="/" class="font-semibold text-base tracking-tight" style="letter-spacing:-0.02em;">Polar</a> |
| 140 | </div> |
| 141 | <div class="flex items-center gap-1.5"> |
| 142 | <button type="button" class="btn btn-ghost btn-icon" data-open-palette="" aria-label="{{t .Lang "nav.command_palette"}}"> |
| 143 | {{icon "search" "size-4"}} |
| 144 | </button> |
| 145 | {{if .CurrentUser}} |
| 146 | <a href="/new" class="btn btn-primary btn-sm">{{icon "plus" "size-3.5"}}</a> |
| 147 | {{else}} |
| 148 | <a href="/login" class="btn btn-outline btn-sm">{{t .Lang "nav.login"}}</a> |
| 149 | {{end}} |
| 150 | </div> |
| 151 | </div> |
| 152 | |
| 153 | <main class="app-main"> |
| 154 | {{if .Flash}} |
| 155 | <div class="container mb-4"> |
| 156 | <div class="card p-3 flex items-center justify-between gap-2" role="status" style="border-color:hsl(var(--primary) / 0.4);"> |
| 157 | <span class="flex items-center gap-2 text-sm">{{icon "check" "size-4"}} {{.Flash}}</span> |
| 158 | </div> |
| 159 | </div> |
| 160 | {{end}} |
| 161 | {{if .Error}} |
| 162 | <div class="container mb-4"> |
| 163 | <div class="card p-3 flex items-center justify-between gap-2" role="alert" style="border-color:hsl(var(--destructive) / 0.6);color:hsl(var(--destructive));"> |
| 164 | <span class="flex items-center gap-2 text-sm">{{icon "alert-circle" "size-4"}} {{.Error}}</span> |
| 165 | </div> |
| 166 | </div> |
| 167 | {{end}} |
| 168 | |
| 169 | {{template "content" .}} |
| 170 | </main> |
| 171 | </div> |
| 172 | </div> |
| 173 | |
| 174 | <!-- Mobile Drawer --> |
| 175 | <div id="mobile-drawer" class="mobile-drawer" aria-hidden="true"> |
| 176 | <div class="mobile-drawer-panel"> |
| 177 | <div class="flex flex-col gap-2"> |
| 178 | <div class="flex items-center justify-between" style="padding-bottom:0.5rem;border-bottom:1px solid hsl(var(--border));"> |
| 179 | <span class="font-semibold text-base tracking-tight" style="letter-spacing:-0.02em;">Polar</span> |
| 180 | <button type="button" class="btn btn-ghost btn-icon" data-drawer-close aria-label="{{t .Lang "common.close"}}"> |
| 181 | {{icon "close" "size-4"}} |
| 182 | </button> |
| 183 | </div> |
| 184 | <nav class="flex flex-col gap-1"> |
| 185 | <a href="/" class="nav-item">{{icon "home" "size-4"}} {{t .Lang "nav.home"}}</a> |
| 186 | {{if .CurrentUser}} |
| 187 | <a href="/new" class="nav-item">{{icon "plus" "size-4"}} {{t .Lang "repo.new.title"}}</a> |
| 188 | <a href="/settings" class="nav-item">{{icon "settings" "size-4"}} {{t .Lang "nav.settings"}}</a> |
| 189 | {{if .CurrentUser.IsAdmin}} |
| 190 | <a href="/admin" class="nav-item">{{icon "shield" "size-4"}} {{t .Lang "nav.admin"}}</a> |
| 191 | {{end}} |
| 192 | {{end}} |
| 193 | {{if .ExploreEnabled}} |
| 194 | <a href="/explore" class="nav-item">{{icon "globe" "size-4"}} {{t .Lang "nav.explore"}}</a> |
| 195 | {{end}} |
| 196 | </nav> |
| 197 | </div> |
| 198 | <div class="segmented"> |
| 199 | <button type="button" class="segmented-btn" data-set-theme="light">{{t .Lang "theme.light"}}</button> |
| 200 | <button type="button" class="segmented-btn" data-set-theme="dark">{{t .Lang "theme.dark"}}</button> |
| 201 | <button type="button" class="segmented-btn" data-set-theme="system">{{t .Lang "theme.system"}}</button> |
| 202 | </div> |
| 203 | </div> |
| 204 | </div> |
| 205 | |
| 206 | <!-- Command Palette (Ctrl/Cmd+K) --> |
| 207 | <div id="command-palette" class="backdrop" role="dialog" aria-modal="true" aria-label="{{t .Lang "nav.command_palette"}}" aria-hidden="true"> |
| 208 | <div class="palette-box"> |
| 209 | <div class="palette-input-wrap"> |
| 210 | {{icon "search" "size-4"}} |
| 211 | <input id="command-palette-input" type="text" class="palette-input" placeholder="{{t .Lang "palette.placeholder"}}" autocomplete="off"> |
| 212 | <kbd class="kbd">ESC</kbd> |
| 213 | </div> |
| 214 | <div class="palette-list" role="listbox"> |
| 215 | <div data-palette-group> |
| 216 | <div class="palette-group-title">{{t .Lang "palette.group.nav"}}</div> |
| 217 | <button type="button" class="palette-item" data-palette-item data-palette-href="/" data-palette-keywords="home главная домой"> |
| 218 | <span class="flex items-center gap-2">{{icon "home" "size-4"}} {{t .Lang "nav.home"}}</span> |
| 219 | <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">h</kbd></span> |
| 220 | </button> |
| 221 | {{if .ExploreEnabled}} |
| 222 | <button type="button" class="palette-item" data-palette-item data-palette-href="/explore" data-palette-keywords="explore обзор публичные"> |
| 223 | <span class="flex items-center gap-2">{{icon "globe" "size-4"}} {{t .Lang "nav.explore"}}</span> |
| 224 | <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">r</kbd></span> |
| 225 | </button> |
| 226 | {{end}} |
| 227 | {{if .CurrentUser}} |
| 228 | <button type="button" class="palette-item" data-palette-item data-palette-href="/new" data-palette-keywords="new create создать репозиторий"> |
| 229 | <span class="flex items-center gap-2">{{icon "plus" "size-4"}} {{t .Lang "repo.new.title"}}</span> |
| 230 | </button> |
| 231 | <button type="button" class="palette-item" data-palette-item data-palette-href="/settings" data-palette-keywords="settings tokens настройки токены"> |
| 232 | <span class="flex items-center gap-2">{{icon "settings" "size-4"}} {{t .Lang "nav.settings"}}</span> |
| 233 | </button> |
| 234 | {{if .CurrentUser.IsAdmin}} |
| 235 | <button type="button" class="palette-item" data-palette-item data-palette-href="/admin" data-palette-keywords="admin админка"> |
| 236 | <span class="flex items-center gap-2">{{icon "shield" "size-4"}} {{t .Lang "nav.admin"}}</span> |
| 237 | </button> |
| 238 | {{end}} |
| 239 | {{else}} |
| 240 | <button type="button" class="palette-item" data-palette-item data-palette-href="/login" data-palette-keywords="login войти"> |
| 241 | <span class="flex items-center gap-2">{{icon "user" "size-4"}} {{t .Lang "nav.login"}}</span> |
| 242 | </button> |
| 243 | <button type="button" class="palette-item" data-palette-item data-palette-href="/register" data-palette-keywords="register регистрация"> |
| 244 | <span class="flex items-center gap-2">{{icon "plus" "size-4"}} {{t .Lang "nav.register"}}</span> |
| 245 | </button> |
| 246 | {{end}} |
| 247 | <button type="button" class="palette-item" data-palette-item data-palette-action="toggle-sidebar" data-palette-keywords="sidebar toggle боковая панель скрыть"> |
| 248 | <span class="flex items-center gap-2">{{icon "sidebar" "size-4"}} {{t .Lang "nav.toggle_sidebar"}}</span> |
| 249 | <span class="flex items-center gap-1"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">B</kbd></span> |
| 250 | </button> |
| 251 | </div> |
| 252 | |
| 253 | {{if .PaletteFiles}} |
| 254 | <div data-palette-group> |
| 255 | <div class="palette-group-title">{{t .Lang "repo.files"}} ({{.Repo.OwnerUsername}}/{{.Repo.Name}})</div> |
| 256 | {{range .PaletteFiles}} |
| 257 | <button type="button" class="palette-item" data-palette-item data-palette-href="/{{$.Repo.OwnerUsername}}/{{$.Repo.Name}}/blob/{{$.Ref}}/{{.}}" data-palette-keywords="file: {{.}}"> |
| 258 | <span class="flex items-center gap-2 font-mono text-xs">{{icon "file-code" "size-4"}} {{.}}</span> |
| 259 | </button> |
| 260 | {{end}} |
| 261 | </div> |
| 262 | {{end}} |
| 263 | |
| 264 | {{if .PaletteRepos}} |
| 265 | <div data-palette-group> |
| 266 | <div class="palette-group-title">{{t .Lang "palette.group.repos"}}</div> |
| 267 | {{range .PaletteRepos}} |
| 268 | <button type="button" class="palette-item" data-palette-item data-palette-href="/{{.OwnerUsername}}/{{.Name}}" data-palette-keywords="repo {{.OwnerUsername}}/{{.Name}} {{.Description}}"> |
| 269 | <span class="flex items-center gap-2"> |
| 270 | {{if .IsPrivate}}{{icon "lock" "size-4"}}{{else}}{{icon "git-branch" "size-4"}}{{end}} |
| 271 | <span>{{.OwnerUsername}}/<strong>{{.Name}}</strong></span> |
| 272 | </span> |
| 273 | <span class="text-xs text-muted">{{if .IsPrivate}}{{t $.Lang "repo.private"}}{{else}}{{t $.Lang "repo.public"}}{{end}}</span> |
| 274 | </button> |
| 275 | {{end}} |
| 276 | </div> |
| 277 | {{end}} |
| 278 | |
| 279 | <div data-palette-group> |
| 280 | <div class="palette-group-title">{{t .Lang "palette.group.themes"}}</div> |
| 281 | <button type="button" class="palette-item" data-palette-item data-palette-theme="light" data-palette-keywords="theme light светлая"> |
| 282 | <span class="flex items-center gap-2">{{icon "sun" "size-4"}} {{t .Lang "theme.light"}}</span> |
| 283 | </button> |
| 284 | <button type="button" class="palette-item" data-palette-item data-palette-theme="dark" data-palette-keywords="theme dark темная тёмная"> |
| 285 | <span class="flex items-center gap-2">{{icon "moon" "size-4"}} {{t .Lang "theme.dark"}}</span> |
| 286 | </button> |
| 287 | <button type="button" class="palette-item" data-palette-item data-palette-theme="system" data-palette-keywords="theme system авто системная"> |
| 288 | <span class="flex items-center gap-2">{{icon "laptop" "size-4"}} {{t .Lang "theme.system"}}</span> |
| 289 | </button> |
| 290 | </div> |
| 291 | </div> |
| 292 | </div> |
| 293 | </div> |
| 294 | |
| 295 | <!-- Keyboard Shortcuts Dialog (?) --> |
| 296 | <div id="shortcuts-dialog" class="backdrop" role="dialog" aria-modal="true" aria-label="{{t .Lang "shortcuts.title"}}" aria-hidden="true"> |
| 297 | <div class="dialog-box"> |
| 298 | <div class="card-header"> |
| 299 | <span class="font-semibold flex items-center gap-2">{{icon "keyboard" "size-4"}} {{t .Lang "shortcuts.title"}}</span> |
| 300 | <button type="button" class="btn btn-ghost btn-icon" data-dialog-close aria-label="{{t .Lang "common.close"}}"> |
| 301 | {{icon "close" "size-4"}} |
| 302 | </button> |
| 303 | </div> |
| 304 | <div class="card-body flex flex-col gap-2 text-sm"> |
| 305 | <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> |
| 306 | <span>{{t .Lang "shortcuts.palette"}}</span> |
| 307 | <span class="flex items-center gap-1"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">K</kbd></span> |
| 308 | </div> |
| 309 | <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> |
| 310 | <span>{{t .Lang "shortcuts.sidebar"}}</span> |
| 311 | <span class="flex items-center gap-1"><kbd class="kbd">Ctrl</kbd><kbd class="kbd">B</kbd></span> |
| 312 | </div> |
| 313 | <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> |
| 314 | <span>{{t .Lang "shortcuts.search"}}</span> |
| 315 | <kbd class="kbd">/</kbd> |
| 316 | </div> |
| 317 | <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> |
| 318 | <span>{{t .Lang "shortcuts.home"}}</span> |
| 319 | <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">h</kbd></span> |
| 320 | </div> |
| 321 | <div class="flex items-center justify-between py-2" style="border-bottom:1px solid hsl(var(--border));"> |
| 322 | <span>{{t .Lang "nav.explore"}}</span> |
| 323 | <span class="flex items-center gap-1"><kbd class="kbd">g</kbd><kbd class="kbd">r</kbd></span> |
| 324 | </div> |
| 325 | <div class="flex items-center justify-between py-2"> |
| 326 | <span>{{t .Lang "shortcuts.files"}}</span> |
| 327 | <kbd class="kbd">t</kbd> |
| 328 | </div> |
| 329 | </div> |
| 330 | </div> |
| 331 | </div> |
| 332 | |
| 333 | <div id="toast-container" class="toast-container" aria-live="polite"></div> |
| 334 | |
| 335 | <script src="/static/js/htmx.min.js" defer></script> |
| 336 | <script src="/static/js/app.js?v={{assetVer}}" defer></script> |
| 337 | </body> |
| 338 | </html> |