| 1 | <%
|
|---|
| 2 | // Profile bottom sheet — mobile-only.
|
|---|
| 3 | //
|
|---|
| 4 | // Triggered by tapping any element with [data-profile-sheet-toggle]
|
|---|
| 5 | // (the Profiel tab in bottom-tab.ejs sets this).
|
|---|
| 6 | //
|
|---|
| 7 | // UX matches .audio-sheet for consistency: slide-up from bottom,
|
|---|
| 8 | // drag-to-dismiss, blur backdrop, safe-area-aware. Hidden ≥768px
|
|---|
| 9 | // (desktop uses the user dropdown in top-nav).
|
|---|
| 10 | //
|
|---|
| 11 | // Extension point: add more <li> blocks in any of the menu groups.
|
|---|
| 12 |
|
|---|
| 13 | if (!user) return;
|
|---|
| 14 |
|
|---|
| 15 | const _isAdmin = (user.role === 'god' || user.role === 'admin');
|
|---|
| 16 | const _canPost = !!((typeof canMutate === 'undefined' || canMutate)
|
|---|
| 17 | && typeof permissions !== 'undefined'
|
|---|
| 18 | && permissions
|
|---|
| 19 | && permissions.canCreatePost
|
|---|
| 20 | && permissions.canCreatePost(user, site));
|
|---|
| 21 | const _roleLabel = (user.role === 'god' || user.role === 'admin') ? 'beheerder'
|
|---|
| 22 | : (user.role === 'editor') ? 'redacteur'
|
|---|
| 23 | : '';
|
|---|
| 24 | // Site-scoped link (post-aanmaken) heeft in hub de /user/<slug>-prefix nodig.
|
|---|
| 25 | const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
|
|---|
| 26 | %>
|
|---|
| 27 |
|
|---|
| 28 | <div class="profile-sheet" id="profile-sheet" aria-hidden="true" role="dialog" aria-label="Profiel menu">
|
|---|
| 29 | <div class="profile-sheet-backdrop" id="profile-sheet-backdrop"></div>
|
|---|
| 30 | <div class="profile-sheet-panel">
|
|---|
| 31 |
|
|---|
| 32 | <div class="profile-sheet-drag-zone" id="profile-sheet-drag-zone">
|
|---|
| 33 | <button type="button" class="profile-sheet-handle" id="profile-sheet-close" aria-label="Sluiten"></button>
|
|---|
| 34 | </div>
|
|---|
| 35 |
|
|---|
| 36 | <!-- Identity header -->
|
|---|
| 37 | <div class="profile-sheet-header">
|
|---|
| 38 | <% if (user.avatar_url) { %>
|
|---|
| 39 | <img class="profile-sheet-avatar profile-sheet-avatar-img" src="<%= avatar(user.avatar_url, 128) %>" alt="" aria-hidden="true">
|
|---|
| 40 | <% } else { %>
|
|---|
| 41 | <div class="profile-sheet-avatar" aria-hidden="true"><%= user.username.charAt(0).toUpperCase() %></div>
|
|---|
| 42 | <% } %>
|
|---|
| 43 | <div class="profile-sheet-meta">
|
|---|
| 44 | <div class="profile-sheet-name"><%= user.username %></div>
|
|---|
| 45 | <% if (_roleLabel) { %><div class="profile-sheet-role"><%= _roleLabel %></div><% } %>
|
|---|
| 46 | </div>
|
|---|
| 47 | </div>
|
|---|
| 48 |
|
|---|
| 49 | <!-- Group 1: primary destinations -->
|
|---|
| 50 | <ul class="profile-sheet-menu" role="menu">
|
|---|
| 51 | <li role="none">
|
|---|
| 52 | <a href="/account" class="profile-sheet-item" role="menuitem" data-close-sheet>
|
|---|
| 53 | <svg class="psi-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>
|
|---|
| 54 | <span class="psi-label"><%= t('nav.account') %></span>
|
|---|
| 55 | <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
|---|
| 56 | </a>
|
|---|
| 57 | </li>
|
|---|
| 58 | <% if (typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
|
|---|
| 59 | <li role="none">
|
|---|
| 60 | <a href="/notifications" class="profile-sheet-item" role="menuitem" data-close-sheet>
|
|---|
| 61 | <svg class="psi-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>
|
|---|
| 62 | <span class="psi-label"><%= t('nav.notifications') %><% if (typeof notifUnread !== 'undefined' && notifUnread > 0) { %> (<%= notifUnread %>)<% } %></span>
|
|---|
| 63 | <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
|---|
| 64 | </a>
|
|---|
| 65 | </li>
|
|---|
| 66 | <% } %>
|
|---|
| 67 | <% if (typeof canManageFedi !== 'undefined' && canManageFedi && apEnabled) { %>
|
|---|
| 68 | <li role="none">
|
|---|
| 69 | <a href="/news" class="profile-sheet-item" role="menuitem" data-close-sheet
|
|---|
| 70 | hx-get="/news?partial=1" hx-target="#pcms-main" hx-swap="innerHTML" hx-push-url="/news" hx-indicator="#pcms-loading">
|
|---|
| 71 | <svg class="psi-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>
|
|---|
| 72 | <span class="psi-label"><%= t('nav.fediverse') %></span>
|
|---|
| 73 | <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
|---|
| 74 | </a>
|
|---|
| 75 | </li>
|
|---|
| 76 | <% } %>
|
|---|
| 77 | <% if (typeof canSeeBeheer !== 'undefined' && canSeeBeheer) { %>
|
|---|
| 78 | <li role="none">
|
|---|
| 79 | <a href="/admin" class="profile-sheet-item" role="menuitem" data-close-sheet>
|
|---|
| 80 | <svg class="psi-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 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z"/></svg>
|
|---|
| 81 | <span class="psi-label"><%= t('nav.admin') %></span>
|
|---|
| 82 | <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
|---|
| 83 | </a>
|
|---|
| 84 | </li>
|
|---|
| 85 | <% } %>
|
|---|
| 86 | <% if (_canPost) { %>
|
|---|
| 87 | <li role="none">
|
|---|
| 88 | <a href="<%= _base %>/posts/new" class="profile-sheet-item" role="menuitem" data-close-sheet
|
|---|
| 89 | hx-get="<%= _base %>/posts/new?partial=1" hx-target="#pcms-main" hx-swap="innerHTML"
|
|---|
| 90 | hx-push-url="<%= _base %>/posts/new" hx-indicator="#pcms-loading">
|
|---|
| 91 | <svg class="psi-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-4L16.5 3.5z"/></svg>
|
|---|
| 92 | <span class="psi-label"><%= t('nav.new_post') %></span>
|
|---|
| 93 | <svg class="psi-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
|
|---|
| 94 | </a>
|
|---|
| 95 | </li>
|
|---|
| 96 | <% } %>
|
|---|
| 97 | </ul>
|
|---|
| 98 |
|
|---|
| 99 | <div class="profile-sheet-divider" role="separator"></div>
|
|---|
| 100 |
|
|---|
| 101 | <!-- Group 2: preferences (inline toggles) -->
|
|---|
| 102 | <ul class="profile-sheet-menu" role="menu">
|
|---|
| 103 | <li role="none">
|
|---|
| 104 | <button type="button" class="profile-sheet-item" id="profile-sheet-theme" role="menuitemcheckbox">
|
|---|
| 105 | <svg class="psi-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>
|
|---|
| 106 | <span class="psi-label"><%= t('nav.theme_label') %></span>
|
|---|
| 107 | <span class="psi-meta" id="profile-sheet-theme-state"><%= t('nav.dark') %></span>
|
|---|
| 108 | </button>
|
|---|
| 109 | </li>
|
|---|
| 110 | </ul>
|
|---|
| 111 |
|
|---|
| 112 | <div class="profile-sheet-divider" role="separator"></div>
|
|---|
| 113 |
|
|---|
| 114 | <!-- Group 3: destructive actions -->
|
|---|
| 115 | <ul class="profile-sheet-menu" role="menu">
|
|---|
| 116 | <li role="none">
|
|---|
| 117 | <a href="/auth/logout" class="profile-sheet-item is-destructive" role="menuitem">
|
|---|
| 118 | <svg class="psi-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>
|
|---|
| 119 | <span class="psi-label"><%= t('nav.logout') %></span>
|
|---|
| 120 | </a>
|
|---|
| 121 | </li>
|
|---|
| 122 | </ul>
|
|---|
| 123 |
|
|---|
| 124 | </div>
|
|---|
| 125 | </div>
|
|---|
| 126 |
|
|---|
| 127 | <style>
|
|---|
| 128 | /* ============================================================
|
|---|
| 129 | PROFILE SHEET — mobile bottom-sheet menu
|
|---|
| 130 | Hidden ≥768px (desktop uses .user-menu dropdown in top-nav).
|
|---|
| 131 | ============================================================ */
|
|---|
| 132 |
|
|---|
| 133 | .profile-sheet {
|
|---|
| 134 | position: fixed;
|
|---|
| 135 | inset: 0;
|
|---|
| 136 | z-index: 1100; /* same as audio-sheet — modal layer */
|
|---|
| 137 | pointer-events: none;
|
|---|
| 138 | opacity: 0;
|
|---|
| 139 | transition: opacity .2s ease;
|
|---|
| 140 | }
|
|---|
| 141 | .profile-sheet.is-open {
|
|---|
| 142 | pointer-events: auto;
|
|---|
| 143 | opacity: 1;
|
|---|
| 144 | }
|
|---|
| 145 |
|
|---|
| 146 | @media (min-width: 768px) {
|
|---|
| 147 | /* Desktop has its own user-menu dropdown */
|
|---|
| 148 | .profile-sheet { display: none !important; }
|
|---|
| 149 | }
|
|---|
| 150 |
|
|---|
| 151 | body.profile-sheet-locked { overflow: hidden; }
|
|---|
| 152 |
|
|---|
| 153 | .profile-sheet-backdrop {
|
|---|
| 154 | position: absolute;
|
|---|
| 155 | inset: 0;
|
|---|
| 156 | background: color-mix(in srgb, var(--ink, #000) 35%, transparent);
|
|---|
| 157 | backdrop-filter: blur(6px);
|
|---|
| 158 | -webkit-backdrop-filter: blur(6px);
|
|---|
| 159 | }
|
|---|
| 160 |
|
|---|
| 161 | .profile-sheet-panel {
|
|---|
| 162 | position: absolute;
|
|---|
| 163 | left: 0; right: 0; bottom: 0;
|
|---|
| 164 | background: var(--paper);
|
|---|
| 165 | color: var(--ink);
|
|---|
| 166 | border-top-left-radius: 22px;
|
|---|
| 167 | border-top-right-radius: 22px;
|
|---|
| 168 | box-shadow: 0 -20px 60px rgba(0, 0, 0, .25);
|
|---|
| 169 | padding-bottom: max(.75rem, env(safe-area-inset-bottom, 0));
|
|---|
| 170 | transform: translateY(100%);
|
|---|
| 171 | transition: transform .3s cubic-bezier(.22, .9, .3, 1);
|
|---|
| 172 | display: flex;
|
|---|
| 173 | flex-direction: column;
|
|---|
| 174 | max-height: 85vh;
|
|---|
| 175 | overflow-y: auto;
|
|---|
| 176 | overscroll-behavior: contain;
|
|---|
| 177 | }
|
|---|
| 178 | .profile-sheet.is-open .profile-sheet-panel {
|
|---|
| 179 | transform: translateY(var(--pcms-drag-y, 0px));
|
|---|
| 180 | }
|
|---|
| 181 | .profile-sheet-panel.is-dragging { transition: none; will-change: transform; }
|
|---|
| 182 |
|
|---|
| 183 | [data-theme="dark"] .profile-sheet-panel {
|
|---|
| 184 | box-shadow: 0 -20px 60px rgba(0, 0, 0, .5);
|
|---|
| 185 | }
|
|---|
| 186 |
|
|---|
| 187 | /* ── Drag handle zone ───────────────────────────── */
|
|---|
| 188 | .profile-sheet-drag-zone {
|
|---|
| 189 | flex-shrink: 0;
|
|---|
| 190 | padding: 12px 16px 4px;
|
|---|
| 191 | /* MUST be 'none' so JS pointer events get the gesture (see audio-sheet) */
|
|---|
| 192 | touch-action: none;
|
|---|
| 193 | user-select: none;
|
|---|
| 194 | -webkit-user-select: none;
|
|---|
| 195 | cursor: grab;
|
|---|
| 196 | }
|
|---|
| 197 | .profile-sheet-drag-zone:active { cursor: grabbing; }
|
|---|
| 198 | @media (hover: hover) and (pointer: fine) {
|
|---|
| 199 | /* On desktop the sheet isn't shown anyway, but be safe */
|
|---|
| 200 | .profile-sheet-drag-zone { touch-action: auto; cursor: default; }
|
|---|
| 201 | }
|
|---|
| 202 |
|
|---|
| 203 | .profile-sheet-handle {
|
|---|
| 204 | display: block;
|
|---|
| 205 | margin: 0 auto;
|
|---|
| 206 | width: 44px;
|
|---|
| 207 | height: 4px;
|
|---|
| 208 | border-radius: 2px;
|
|---|
| 209 | background: color-mix(in srgb, var(--ink, #000) 25%, transparent);
|
|---|
| 210 | border: 0;
|
|---|
| 211 | padding: 0;
|
|---|
| 212 | position: relative;
|
|---|
| 213 | }
|
|---|
| 214 | .profile-sheet-handle::before {
|
|---|
| 215 | /* Larger invisible tap target on the handle itself */
|
|---|
| 216 | content: "";
|
|---|
| 217 | position: absolute;
|
|---|
| 218 | inset: -16px -32px;
|
|---|
| 219 | }
|
|---|
| 220 |
|
|---|
| 221 | /* ── Identity header ────────────────────────────── */
|
|---|
| 222 | .profile-sheet-header {
|
|---|
| 223 | display: flex;
|
|---|
| 224 | align-items: center;
|
|---|
| 225 | gap: 14px;
|
|---|
| 226 | padding: 8px 20px 18px;
|
|---|
| 227 | }
|
|---|
| 228 | .profile-sheet-avatar {
|
|---|
| 229 | width: 48px;
|
|---|
| 230 | height: 48px;
|
|---|
| 231 | display: flex;
|
|---|
| 232 | align-items: center;
|
|---|
| 233 | justify-content: center;
|
|---|
| 234 | border-radius: 50%;
|
|---|
| 235 | background: var(--paper-2); /* neutral so transparent avatars blend */
|
|---|
| 236 | color: var(--accent); /* fallback initial color */
|
|---|
| 237 | font-family: var(--font-ui);
|
|---|
| 238 | font-weight: 700;
|
|---|
| 239 | font-size: 20px;
|
|---|
| 240 | flex-shrink: 0;
|
|---|
| 241 | overflow: hidden;
|
|---|
| 242 | }
|
|---|
| 243 | .profile-sheet-avatar-img {
|
|---|
| 244 | /* When rendered as <img>, fill the round container edge-to-edge. */
|
|---|
| 245 | object-fit: cover;
|
|---|
| 246 | }
|
|---|
| 247 | .profile-sheet-meta { min-width: 0; }
|
|---|
| 248 | .profile-sheet-name {
|
|---|
| 249 | font-family: var(--font-display, var(--font-ui));
|
|---|
| 250 | font-size: 1.2rem;
|
|---|
| 251 | font-weight: 600;
|
|---|
| 252 | line-height: 1.2;
|
|---|
| 253 | color: var(--ink);
|
|---|
| 254 | }
|
|---|
| 255 | .profile-sheet-role {
|
|---|
| 256 | font-family: var(--font-ui);
|
|---|
| 257 | font-size: .82rem;
|
|---|
| 258 | color: var(--ink-soft);
|
|---|
| 259 | margin-top: 3px;
|
|---|
| 260 | text-transform: lowercase;
|
|---|
| 261 | letter-spacing: .02em;
|
|---|
| 262 | }
|
|---|
| 263 |
|
|---|
| 264 | /* ── Menu list ──────────────────────────────────── */
|
|---|
| 265 | .profile-sheet-menu {
|
|---|
| 266 | list-style: none;
|
|---|
| 267 | margin: 0;
|
|---|
| 268 | padding: 0;
|
|---|
| 269 | }
|
|---|
| 270 | .profile-sheet-divider {
|
|---|
| 271 | height: 1px;
|
|---|
| 272 | background: var(--rule);
|
|---|
| 273 | margin: 4px 20px;
|
|---|
| 274 | }
|
|---|
| 275 | .profile-sheet-item {
|
|---|
| 276 | display: flex;
|
|---|
| 277 | align-items: center;
|
|---|
| 278 | gap: 14px;
|
|---|
| 279 | width: 100%;
|
|---|
| 280 | min-height: 52px;
|
|---|
| 281 | padding: 12px 20px;
|
|---|
| 282 | background: transparent;
|
|---|
| 283 | border: 0;
|
|---|
| 284 | color: var(--ink);
|
|---|
| 285 | text-decoration: none;
|
|---|
| 286 | font-family: var(--font-ui);
|
|---|
| 287 | font-size: 1rem;
|
|---|
| 288 | text-align: left;
|
|---|
| 289 | cursor: pointer;
|
|---|
| 290 | -webkit-tap-highlight-color: transparent;
|
|---|
| 291 | transition: background .12s ease, transform .12s ease;
|
|---|
| 292 | }
|
|---|
| 293 | .profile-sheet-item:active {
|
|---|
| 294 | background: var(--paper-2);
|
|---|
| 295 | transform: scale(0.99);
|
|---|
| 296 | }
|
|---|
| 297 | @media (hover: hover) {
|
|---|
| 298 | .profile-sheet-item:hover { background: var(--paper-2); }
|
|---|
| 299 | }
|
|---|
| 300 |
|
|---|
| 301 | .psi-icon {
|
|---|
| 302 | width: 22px;
|
|---|
| 303 | height: 22px;
|
|---|
| 304 | flex-shrink: 0;
|
|---|
| 305 | color: var(--ink-soft);
|
|---|
| 306 | }
|
|---|
| 307 | .psi-label {
|
|---|
| 308 | flex: 1;
|
|---|
| 309 | min-width: 0;
|
|---|
| 310 | }
|
|---|
| 311 | .psi-chev {
|
|---|
| 312 | width: 16px;
|
|---|
| 313 | height: 16px;
|
|---|
| 314 | color: var(--ink-muted);
|
|---|
| 315 | flex-shrink: 0;
|
|---|
| 316 | }
|
|---|
| 317 | .psi-meta {
|
|---|
| 318 | font-size: .85rem;
|
|---|
| 319 | color: var(--ink-soft);
|
|---|
| 320 | font-family: var(--font-ui);
|
|---|
| 321 | }
|
|---|
| 322 |
|
|---|
| 323 | .profile-sheet-item.is-destructive { color: #dc2626; }
|
|---|
| 324 | .profile-sheet-item.is-destructive .psi-icon { color: currentColor; }
|
|---|
| 325 | [data-theme="dark"] .profile-sheet-item.is-destructive { color: #f87171; }
|
|---|
| 326 | </style>
|
|---|
| 327 |
|
|---|
| 328 | <script>
|
|---|
| 329 | (function() {
|
|---|
| 330 | const sheet = document.getElementById('profile-sheet');
|
|---|
| 331 | if (!sheet) return;
|
|---|
| 332 |
|
|---|
| 333 | const backdrop = document.getElementById('profile-sheet-backdrop');
|
|---|
| 334 | const panel = sheet.querySelector('.profile-sheet-panel');
|
|---|
| 335 | const closeBtn = document.getElementById('profile-sheet-close');
|
|---|
| 336 | const dragZone = document.getElementById('profile-sheet-drag-zone');
|
|---|
| 337 | const themeBtn = document.getElementById('profile-sheet-theme');
|
|---|
| 338 | const themeLbl = document.getElementById('profile-sheet-theme-state');
|
|---|
| 339 |
|
|---|
| 340 | function openSheet() {
|
|---|
| 341 | sheet.classList.add('is-open');
|
|---|
| 342 | sheet.setAttribute('aria-hidden', 'false');
|
|---|
| 343 | document.body.classList.add('profile-sheet-locked');
|
|---|
| 344 | syncTheme();
|
|---|
| 345 | }
|
|---|
| 346 | function closeSheet() {
|
|---|
| 347 | sheet.classList.remove('is-open');
|
|---|
| 348 | sheet.setAttribute('aria-hidden', 'true');
|
|---|
| 349 | document.body.classList.remove('profile-sheet-locked');
|
|---|
| 350 | panel.style.removeProperty('--pcms-drag-y');
|
|---|
| 351 | }
|
|---|
| 352 |
|
|---|
| 353 | // Open: any element with [data-profile-sheet-toggle]
|
|---|
| 354 | document.addEventListener('click', function(e) {
|
|---|
| 355 | const trigger = e.target.closest('[data-profile-sheet-toggle]');
|
|---|
| 356 | if (trigger) {
|
|---|
| 357 | e.preventDefault();
|
|---|
| 358 | openSheet();
|
|---|
| 359 | }
|
|---|
| 360 | });
|
|---|
| 361 |
|
|---|
| 362 | // Close: backdrop tap, handle tap, ESC, or any [data-close-sheet] item
|
|---|
| 363 | if (backdrop) backdrop.addEventListener('click', closeSheet);
|
|---|
| 364 | if (closeBtn) closeBtn.addEventListener('click', closeSheet);
|
|---|
| 365 | document.addEventListener('keydown', (e) => {
|
|---|
| 366 | if (e.key === 'Escape' && sheet.classList.contains('is-open')) closeSheet();
|
|---|
| 367 | });
|
|---|
| 368 |
|
|---|
| 369 | // Menu items that navigate: close synchronously before nav fires
|
|---|
| 370 | sheet.querySelectorAll('[data-close-sheet]').forEach((el) => {
|
|---|
| 371 | el.addEventListener('click', closeSheet);
|
|---|
| 372 | });
|
|---|
| 373 |
|
|---|
| 374 | // Drag-down-to-close on touch.
|
|---|
| 375 | // Uses --pcms-drag-y custom property rather than overwriting the panel's
|
|---|
| 376 | // transform string. This composes correctly with any horizontal centering
|
|---|
| 377 | // (currently none here, but the pattern matches audio-sheet for safety).
|
|---|
| 378 | let startY = 0, lastY = 0, dragging = false;
|
|---|
| 379 | function onDown(e) {
|
|---|
| 380 | if (e.pointerType !== 'touch') return;
|
|---|
| 381 | if (panel.scrollTop > 0) return;
|
|---|
| 382 | startY = lastY = e.clientY;
|
|---|
| 383 | dragging = true;
|
|---|
| 384 | panel.classList.add('is-dragging');
|
|---|
| 385 | }
|
|---|
| 386 | function onMove(e) {
|
|---|
| 387 | if (!dragging) return;
|
|---|
| 388 | lastY = e.clientY;
|
|---|
| 389 | const dy = Math.max(0, lastY - startY);
|
|---|
| 390 | panel.style.setProperty('--pcms-drag-y', dy + 'px');
|
|---|
| 391 | }
|
|---|
| 392 | function onUp() {
|
|---|
| 393 | if (!dragging) return;
|
|---|
| 394 | dragging = false;
|
|---|
| 395 | panel.classList.remove('is-dragging');
|
|---|
| 396 | const dy = lastY - startY;
|
|---|
| 397 | if (dy > 80) closeSheet();
|
|---|
| 398 | else panel.style.removeProperty('--pcms-drag-y');
|
|---|
| 399 | }
|
|---|
| 400 | if (window.PointerEvent && dragZone) {
|
|---|
| 401 | dragZone.addEventListener('pointerdown', onDown);
|
|---|
| 402 | document.addEventListener('pointermove', onMove);
|
|---|
| 403 | document.addEventListener('pointerup', onUp);
|
|---|
| 404 | document.addEventListener('pointercancel', onUp);
|
|---|
| 405 | }
|
|---|
| 406 |
|
|---|
| 407 | // Theme toggle inside sheet — syncs label
|
|---|
| 408 | function syncTheme() {
|
|---|
| 409 | if (!themeLbl) return;
|
|---|
| 410 | const t = document.documentElement.getAttribute('data-theme') || 'dark';
|
|---|
| 411 | themeLbl.textContent = t === 'dark' ? '<%= t('nav.dark') %>' : '<%= t('nav.light') %>';
|
|---|
| 412 | }
|
|---|
| 413 | if (themeBtn) {
|
|---|
| 414 | themeBtn.addEventListener('click', function() {
|
|---|
| 415 | const cur = document.documentElement.getAttribute('data-theme') || 'dark';
|
|---|
| 416 | const next = cur === 'dark' ? 'light' : 'dark';
|
|---|
| 417 | document.documentElement.setAttribute('data-theme', next);
|
|---|
| 418 | try { localStorage.setItem('pcms-theme', next); } catch (e) {}
|
|---|
| 419 | syncTheme();
|
|---|
| 420 | });
|
|---|
| 421 | }
|
|---|
| 422 | })();
|
|---|
| 423 | </script>
|
|---|