This commit is contained in:
2026-09-28 23:24:50 +02:00
parent 2cc768f1fd
commit 73c0659d4a
29 changed files with 820 additions and 117 deletions
+24
View File
@@ -0,0 +1,24 @@
{{-- Server copy of the main comment input: must match comments.js renderInput() + setupEmojiPicker() (users) or renderGuestInput() (guests) exactly, so the client re-render swaps in an identical box without any layout shift --}}
@if(session)
<div class="comment-input main-input">
<textarea placeholder="{{ t('comments.write_comment') }}"@if(comment_max_length !== null && comment_max_length !== undefined) maxlength="{{ comment_max_length }}"@endif required></textarea>
<div class="comment-file-preview"></div>
<div class="input-actions">
@if(comment_max_length !== null && comment_max_length !== undefined)<span class="char-counter" data-max="{{ comment_max_length }}">0 / {{ comment_max_length }}</span>@endif
@if(allow_fileupload_comments && !session.is_anon || allow_fileupload_comments && anon_permissions.comment_attachments !== false)<button class="comment-attach-btn" title="{{ t('comments.attach_file') }}" type="button"><i class="fa-solid fa-paperclip"></i></button>@endif
@if(enable_comment_polls)<button class="comment-poll-btn" title="{{ t('comments.poll_btn_title') || 'Create poll' }}" type="button"><i class="fa-solid fa-chart-bar"></i></button>@endif
@if(is_mod_or_admin)<button id="lock-thread-btn" class="admin-lock-btn" title="{{ item.is_comments_locked ? 'Unlock Thread' : 'Lock Thread' }}">@if(item.is_comments_locked)<i class="fa-solid fa-lock"></i>@else<i class="fa-solid fa-lock-open"></i>@endif</button>@endif
<button class="spoiler-trigger" title="Insert spoiler tag">[S]</button>
<button class="emoji-trigger"><i class="fa-regular fa-face-smile"></i></button>
<button class="submit-comment"><span class="submit-label">{{ t('comments.post') }}</span><i class="fa-solid fa-spinner fa-spin submit-spinner"></i></button>
</div>
</div>
@else
<div class="comment-input main-input is-guest" aria-disabled="true" title="Log in to comment" onclick="const b=document.getElementById('nav-login-btn'); if (b) b.click();">
<textarea placeholder="Log in to comment" readonly tabindex="-1" aria-disabled="true"></textarea>
<div class="comment-file-preview"></div>
<div class="input-actions">
<button class="submit-comment" type="button" tabindex="-1" aria-disabled="true"><span class="submit-label">{{ t('comments.post') }}</span><i class="fa-solid fa-spinner fa-spin submit-spinner"></i></button>
</div>
</div>
@endif
+10 -7
View File
@@ -2,6 +2,7 @@
<div id="mod-action-modal" class="modal-overlay" style="display:none;">
<div class="modal-content ma-modal-content">
<div class="ma-header">
<button type="button" class="modal-x" aria-label="Close" title="Close" onclick="document.getElementById('mod-action-cancel').click()">&times;</button>
<h3 id="mod-action-title">{{ t('mod.confirm_action') }}</h3>
</div>
<div class="ma-body">
@@ -36,7 +37,7 @@
color: var(--ma-text);
}
#mod-action-modal .ma-modal-content * { border-radius: 0 !important; }
#mod-action-modal .ma-header { flex-shrink: 0; padding: 18px 24px 14px; border-bottom: 1px solid var(--ma-border); }
#mod-action-modal .ma-header { position: relative; flex-shrink: 0; padding: 18px 64px 14px 24px; border-bottom: 1px solid var(--ma-border); }
#mod-action-modal .ma-header h3 { margin: 0; font-size: 1.2em; font-weight: 700; color: var(--ma-text); overflow-wrap: anywhere; }
#mod-action-modal .ma-body {
flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
@@ -143,6 +144,7 @@
<div id="report-modal" class="modal-overlay" style="display:none;">
<div class="modal-content report-modal-content">
<div class="rm-header">
<button type="button" class="modal-x" aria-label="Close" title="Close" onclick="document.getElementById('report-cancel').click()">&times;</button>
<h3>{{ t('report.title') }}</h3>
<p>Select one or more reasons and optionally add details below.</p>
</div>
@@ -217,7 +219,7 @@
color: var(--rm-text);
}
#report-modal .report-modal-content * { border-radius: 0 !important; }
#report-modal .rm-header { flex-shrink: 0; padding: 20px 24px 14px; border-bottom: 1px solid var(--rm-border); }
#report-modal .rm-header { position: relative; flex-shrink: 0; padding: 20px 64px 14px 24px; border-bottom: 1px solid var(--rm-border); }
#report-modal .rm-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 16px 24px; }
#report-modal .rm-body > :last-child { margin-bottom: 0; }
#report-modal .rm-header h3 { margin: 0; font-size: 1.25em; font-weight: 700; color: var(--rm-text); }
@@ -415,14 +417,15 @@
</div>
</div>
<div class="global-sidebar-right-footer">
<a href="/ranking">{{ t('footer.ranking') }}</a>
<a href="/rules">{{ t('footer.rules') }}</a>
<a href="/about">{{ t('footer.about') }}</a>
<a href="/privacy">Privacy</a>
<div id="help-button" style="color: var(--accent); font-weight: bold; opacity: 0.7;" title="Keyboard Shortcuts">?</div>
<a href="/ranking" data-page-modal title="{{ t('footer.ranking') }}" aria-label="{{ t('footer.ranking') }}"><i class="fa-solid fa-users-between-lines"></i></a>
<a href="/rules" data-page-modal title="{{ t('footer.rules') }}" aria-label="{{ t('footer.rules') }}"><i class="fa-solid fa-scale-balanced"></i></a>
<a href="/about" data-page-modal title="{{ t('footer.about') }}" aria-label="{{ t('footer.about') }}"><i class="fa-solid fa-info"></i></a>
<a href="/privacy" data-page-modal title="Privacy" aria-label="Privacy"><i class="fa-solid fa-shield-halved"></i></a>
<div id="help-button" style="color: var(--accent); font-weight: bold; opacity: 0.7;" title="Keyboard Shortcuts" aria-label="Keyboard Shortcuts"><i class="fa-solid fa-question"></i></div>
</div>
</div>
@endif
@include(snippets/page-modal)
@if(!private_society || session)
<script src="/s/js/sanitizer.js?v={{ ts }}"></script>
+1 -1
View File
@@ -67,7 +67,7 @@
@endif
<link rel="stylesheet" href="/s/css/upload.css?v={{ ts }}">
@endif
<script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDefaultLayout = "{{ default_layout }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }}; window.f0ckCustomBrandText = "{{ custom_navbar_brand_text || '' }}"; window.f0ckMediaBase = "{{ paths_images }}"; window.f0ckShitpostMode = {{ shitpost_mode ? 'true' : 'false' }}; window.f0ckShitpostRequireRating = {{ shitpost_require_rating ? 'true' : 'false' }}; window.f0ckShitpostMinTags = {{ shitpost_min_tags || 0 }}; window.f0ckEnableItemTitle = {{ enable_item_title ? 'true' : 'false' }}; window.f0ckCommentBannerEnabled = @if(comment_banner_enabled) true @else false @endif; window.f0ckServerAudioTuner = {{ audio_tuner_json || 'null' }}; window.f0ckEnableComments = {{ enable_comments ? 'true' : 'false' }};</script>
<script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDefaultLayout = "{{ default_layout }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }}; window.f0ckCustomBrandText = "{{ custom_navbar_brand_text || '' }}"; window.f0ckMediaBase = "{{ paths_images }}"; window.f0ckShitpostMode = {{ shitpost_mode ? 'true' : 'false' }}; window.f0ckShitpostRequireRating = {{ shitpost_require_rating ? 'true' : 'false' }}; window.f0ckShitpostMinTags = {{ shitpost_min_tags || 0 }}; window.f0ckEnableItemTitle = {{ enable_item_title ? 'true' : 'false' }}; window.f0ckCommentBannerEnabled = @if(comment_banner_enabled) true @else false @endif; window.f0ckServerAudioTuner = {{ audio_tuner_json || 'null' }}; window.f0ckEnableComments = {{ enable_comments ? 'true' : 'false' }}; window.f0ckHwFingerprint = {{ hw_fingerprint_enabled ? 'true' : 'false' }};</script>
@if(!private_society || session)
@if(enable_comments)
<script src="/s/js/marked.min.js" defer></script>
+224 -31
View File
@@ -256,7 +256,7 @@
@endif
</a>
<div class="nav-collapse" id="navbarContent">
<div class="nav-collapse nav-collapse-guest" id="navbarContent">
<div class="nav-links">
<a href="/tags" title="Tags"><i class="fa-solid fa-tags"></i></a>
@if(halls_enabled)
@@ -294,7 +294,7 @@
<span id="nav-anon-label">@if(enable_anonymous_access && session && session.is_anon)anonymous@else guest @endif</span>
<span class="nav-avatar-caret">▾</span>
</button>
<div class="nav-user-menu" id="nav-visitor-menu" style="min-width: max-content; width: max-content; white-space: nowrap;">
<div class="nav-user-menu" id="nav-visitor-menu" style="min-width: 100%; width: max-content; white-space: nowrap;">
@if(enable_anonymous_access && session && session.is_anon)
<a href="/favs" id="nav-guest-favs-link" class="mobile-only">{{ t('nav.favs') }}</a>
@endif
@@ -304,7 +304,9 @@
<a href="/settings" id="nav-anon-settings-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-gear"></i> Settings</a>
@endif
<a href="#" id="nav-login-btn">{{ t('nav.login') }}</a>
@if(registration_open)
<a href="#" id="nav-register-btn">{{ t('nav.register') }}</a>
@endif
@if(enable_anonymous_access)
<div class="nav-user-divider" id="nav-anon-divider" @if(!session || !session.is_anon) style="display:none;" @endif></div>
<a href="#" id="nav-anon-logout-btn" @if(!session || !session.is_anon) style="display:none;" @endif><i class="fa-solid fa-right-from-bracket"></i> Logout (Guest)</a>
@@ -446,10 +448,6 @@ async function loginWithPasskey() {
<!-- Tab: Passkey -->
<div id="login-panel-passkey" class="lm-panel lm-panel-passkey" style="display: none;">
<div class="lm-passkey-icon"><i class="fa-solid fa-fingerprint"></i></div>
<p class="lm-passkey-text">
Use a saved passkey from Bitwarden, iCloud Keychain, or your OS to sign in without a password.
</p>
<button type="button" id="modal-login-passkey-btn" class="lm-btn lm-btn-primary" onclick="loginWithPasskey()">
<i class="fa-solid fa-fingerprint"></i> Use my passkey
</button>
@@ -463,9 +461,11 @@ async function loginWithPasskey() {
onclick="event.preventDefault(); if(window.f0ckAnonPasskey) window.f0ckAnonPasskey.openSetupModal(); else { const m=document.getElementById('anon-setup-modal'); if(m) m.style.display='flex'; }">
<i class="fa-solid fa-user-secret"></i> Login as Anonymous
</button>
@if(registration_open)
<button type="button" id="modal-login-register-btn" class="lm-btn lm-btn-ghost" style="margin-top: 8px;">
<i class="fa-solid fa-user-plus"></i> Register
</button>
@endif
</div>
@endif
@@ -653,6 +653,10 @@ async function loginWithPasskey() {
background: var(--lm-surface); border: 1px solid var(--lm-border); border-left: 2px solid var(--lm-accent);
}
#anon-setup-modal #anon-setup-view .lm-note i { color: var(--lm-accent); margin-right: 4px; }
#anon-setup-modal #anon-setup-view .lm-title { margin-bottom: 18px; }
#anon-setup-modal #anon-setup-view .anon-consent { margin: 0 0 16px; text-align: left; }
/* Disabled until the privacy consent is ticked (spinner state keeps the progress cursor) */
#anon-setup-modal #anon-setup-view .lm-btn:disabled:not(:has(.fa-spinner)) { cursor: not-allowed; opacity: 0.45; }
#anon-setup-modal #anon-setup-view .lm-textbtn {
margin: 0; padding: 0; background: none; border: 0;
font: inherit; font-weight: 600; color: var(--lm-accent); cursor: pointer;
@@ -816,20 +820,16 @@ async function loginWithPasskey() {
<div id="anon-setup-view">
<!-- New user view (no passkey yet) -->
<div id="anon-setup-new" class="lm-panel lm-panel-passkey">
<div class="lm-passkey-icon"><i class="fa-solid fa-fingerprint"></i></div>
<h2 class="lm-title">Create an anonymous passkey</h2>
<p class="lm-passkey-text" style="margin-top: 12px;">
Your browser will ask you to save a <strong>passkey</strong> — a secure credential stored in Bitwarden, iCloud Keychain, or your OS.
No account, no email, no password. Your comments and favorites are tied to this passkey.
</p>
<p class="lm-note">
<i class="fa-solid fa-rotate"></i> You can use it across devices if your passkey manager syncs (e.g. Bitwarden).
</p>
<p class="lm-note">
<i class="fa-solid fa-shield-halved"></i> To stop ban evasion we store a hashed device fingerprint@if(privacy_ip_mode === 'hashed') and a hashed IP address@elseif(privacy_ip_mode === 'raw') and your IP address@endif. <a href="/privacy" target="_blank" class="lm-link">What exactly is stored?</a>
</p>
<h2 class="lm-title">Create passkey</h2>
<div class="lm-row anon-consent">
<label class="lm-check lm-check-multiline" for="anon-consent-new">
<input type="checkbox" id="anon-consent-new" class="anon-consent-input" />
<span class="lm-check-box"><i class="fa-solid fa-check"></i></span>
<span>I have read the <a href="/privacy" class="lm-link anon-consent-open">privacy page</a> and accept how my data is processed.</span>
</label>
</div>
<button type="button" id="anon-setup-create-btn" class="lm-btn lm-btn-primary">
<i class="fa-solid fa-fingerprint"></i> Create my passkey
<i class="fa-solid fa-fingerprint"></i> Create
</button>
<div id="anon-setup-new-error" class="lm-error" style="display: none;"></div>
<p class="lm-footer">
@@ -839,13 +839,9 @@ async function loginWithPasskey() {
<!-- Returning user view (has passkey) -->
<div id="anon-setup-returning" class="lm-panel lm-panel-passkey" style="display: none;">
<div class="lm-passkey-icon"><i class="fa-solid fa-user-secret"></i></div>
<h2 class="lm-title">Use your anonymous passkey</h2>
<p class="lm-passkey-text" style="margin-top: 12px;">
Pick your saved passkey from Bitwarden, iCloud Keychain, or your OS to continue as the same anonymous user.
</p>
<h2 class="lm-title">Authenticate with passkey</h2>
<button type="button" id="anon-setup-auth-btn" class="lm-btn lm-btn-primary">
<i class="fa-solid fa-fingerprint"></i> Use my passkey
<i class="fa-solid fa-fingerprint"></i> Authenticate
</button>
<div id="anon-setup-ret-error" class="lm-error" style="display: none;"></div>
<p class="lm-footer">
@@ -855,6 +851,64 @@ async function loginWithPasskey() {
</div>
</div>
</div>
<!-- Privacy consent modal: opened from the "Create passkey" consent checkbox; must be read to the end -->
<div id="privacy-consent-modal" style="display: none;">
<div class="pc-content">
<div class="pc-header">
<h3><i class="fa-solid fa-shield-halved"></i> Privacy</h3>
<button type="button" id="privacy-consent-close" class="pc-close" aria-label="Close">&times;</button>
</div>
<div class="pc-body" id="privacy-consent-body"></div>
<div class="pc-footer">
<span class="pc-hint" id="privacy-consent-hint">Scroll to the end to accept.</span>
<button type="button" id="privacy-consent-accept" class="pc-accept" disabled><i class="fa-solid fa-check"></i> Accept</button>
</div>
</div>
</div>
<style>
#privacy-consent-modal {
position: fixed; inset: 0; z-index: 100000;
display: flex; align-items: center; justify-content: center;
background: rgba(0, 0, 0, 0.85); backdrop-filter: blur(4px);
}
#privacy-consent-modal .pc-content {
width: 100vw; height: 100vh; height: 100dvh;
display: flex; flex-direction: column; overflow: hidden;
background: var(--bg); border: 0; border-radius: 0;
}
#privacy-consent-modal .pc-header {
flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 14px 20px; border-bottom: 1px solid var(--accent);
}
#privacy-consent-modal .pc-header h3 { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 1.1em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.06em; }
#privacy-consent-modal .pc-header h3 i { color: var(--accent); }
#privacy-consent-modal .pc-close {
width: 36px; height: 36px; margin: 0; padding: 0; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
background: none; border: 1px solid rgba(255, 255, 255, 0.25); border-radius: 0;
color: var(--text-color, #fff); font-size: 1.6em; line-height: 1; cursor: pointer; transition: color 0.15s, border-color 0.15s;
}
#privacy-consent-modal .pc-close:hover { color: var(--accent); border-color: var(--accent); }
#privacy-consent-modal .pc-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
#privacy-consent-modal .pc-body .pv { padding-top: 20px; }
#privacy-consent-modal .pc-loading { padding: 40px 20px; text-align: center; color: var(--text-muted, #aaa); }
#privacy-consent-modal .pc-footer {
flex-shrink: 0; display: flex; align-items: center; justify-content: flex-end; gap: 16px;
padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px)); border-top: 1px solid rgba(255, 255, 255, 0.1); background: rgba(0, 0, 0, 0.2);
}
#privacy-consent-modal .pc-hint { font-size: 0.85em; color: var(--text-muted, #aaa); }
#privacy-consent-modal .pc-accept {
height: 42px; padding: 0 22px; margin: 0; font: inherit; font-size: 0.85em; font-weight: 800;
text-transform: uppercase; letter-spacing: 0.08em; cursor: pointer; border-radius: 0;
display: inline-flex; align-items: center; gap: 8px;
background: var(--accent); color: #000; border: 1px solid var(--accent); transition: background 0.18s, color 0.18s;
}
#privacy-consent-modal .pc-accept:hover:not(:disabled) { background: transparent; color: var(--accent); }
#privacy-consent-modal .pc-accept:disabled { opacity: 0.4; cursor: not-allowed; }
@media (max-width: 600px) {
#privacy-consent-modal .pc-footer { flex-direction: column; align-items: stretch; gap: 8px; text-align: center; }
#privacy-consent-modal .pc-accept { justify-content: center; }
}
</style>
<script>
(function() {
function closeSetupModal() {
@@ -874,10 +928,95 @@ async function loginWithPasskey() {
document.getElementById('anon-setup-returning').style.display = 'none';
document.getElementById('anon-setup-new').style.display = '';
});
// Privacy consent (new passkeys only): the checkbox can only be ticked by reading the privacy page in
// the consent modal to the end. Accepting is remembered in localStorage and the checkbox never shows again.
var CONSENT_KEY = 'f0ck_privacy_accepted';
var consentInput = document.getElementById('anon-consent-new');
var hasConsent = function() {
try { return !!localStorage.getItem(CONSENT_KEY); } catch (e) { return false; }
};
var syncConsent = function() {
var accepted = hasConsent();
document.querySelectorAll('#anon-setup-modal .anon-consent').forEach(function(el) { el.style.display = accepted ? 'none' : ''; });
var btn = document.getElementById('anon-setup-create-btn');
if (btn && !btn.querySelector('.fa-spinner')) btn.disabled = !(accepted || (consentInput && consentInput.checked));
};
var recordConsent = function() {
if (hasConsent()) return true;
if (!consentInput || !consentInput.checked) return false;
try { localStorage.setItem(CONSENT_KEY, JSON.stringify({ accepted_at: new Date().toISOString() })); } catch (e) {}
return true;
};
var pcModal = document.getElementById('privacy-consent-modal');
var pcBody = document.getElementById('privacy-consent-body');
var pcAccept = document.getElementById('privacy-consent-accept');
var pcHint = document.getElementById('privacy-consent-hint');
var pcLoaded = false;
var pcCheckEnd = function() {
if (!pcLoaded) return;
var atEnd = pcBody.scrollTop + pcBody.clientHeight >= pcBody.scrollHeight - 8;
if (atEnd) {
pcAccept.disabled = false;
if (pcHint) pcHint.textContent = 'You reached the end.';
}
};
var loadPrivacy = async function() {
if (pcLoaded) return;
pcBody.innerHTML = '<div class="pc-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading…</div>';
try {
var r = await fetch('/privacy', { credentials: 'same-origin' });
var doc = new DOMParser().parseFromString(await r.text(), 'text/html');
var pv = doc.querySelector('#main .pv');
if (!r.ok || !pv) throw new Error('bad response');
pcBody.innerHTML = '';
doc.querySelectorAll('#main style').forEach(function(st) { pcBody.appendChild(st.cloneNode(true)); });
pcBody.appendChild(document.importNode(pv, true));
pcBody.querySelectorAll('a[href]').forEach(function(a) { a.target = '_blank'; a.rel = 'noopener'; });
} catch (e) {
pcBody.innerHTML = '<div class="pc-loading">Could not load the privacy page. <a href="/privacy" target="_blank" class="lm-link">Open it in a new tab</a>.</div>';
}
pcLoaded = true;
};
var openConsentModal = async function() {
if (!pcModal) return;
pcAccept.disabled = true;
if (pcHint) pcHint.textContent = 'Scroll to the end to accept.';
pcModal.style.display = 'flex';
await loadPrivacy();
pcBody.scrollTop = 0;
// Short content that doesn't scroll counts as read
requestAnimationFrame(pcCheckEnd);
};
var closeConsentModal = function() { if (pcModal) pcModal.style.display = 'none'; };
if (pcBody) pcBody.addEventListener('scroll', pcCheckEnd, { passive: true });
if (pcAccept) pcAccept.addEventListener('click', function() {
if (consentInput) consentInput.checked = true;
closeConsentModal();
syncConsent();
});
var pcClose = document.getElementById('privacy-consent-close');
if (pcClose) pcClose.addEventListener('click', closeConsentModal);
if (consentInput) consentInput.addEventListener('click', function(e) {
// Ticking goes through the privacy modal; unticking is always allowed
if (!consentInput.checked) return syncConsent();
e.preventDefault();
openConsentModal();
});
document.querySelectorAll('#anon-setup-modal .anon-consent-open').forEach(function(a) {
a.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); openConsentModal(); });
});
syncConsent();
// Re-sync whenever the modal is opened (another tab may have accepted meanwhile)
if (overlay) new MutationObserver(function() { if (overlay.style.display !== 'none') syncConsent(); }).observe(overlay, { attributes: true, attributeFilter: ['style'] });
var createBtn = document.getElementById('anon-setup-create-btn');
if (createBtn) createBtn.addEventListener('click', async function() {
var errEl = document.getElementById('anon-setup-new-error');
if (errEl) errEl.style.display = 'none';
if (!recordConsent()) return;
createBtn.disabled = true;
createBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Saving passkey...';
try {
@@ -893,7 +1032,7 @@ async function loginWithPasskey() {
}
}
createBtn.disabled = false;
createBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Create my passkey';
createBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Create';
}
});
var authBtn = document.getElementById('anon-setup-auth-btn');
@@ -915,7 +1054,7 @@ async function loginWithPasskey() {
}
}
authBtn.disabled = false;
authBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Use my passkey';
authBtn.innerHTML = '<i class="fa-solid fa-fingerprint"></i> Authenticate';
}
});
})();
@@ -925,10 +1064,12 @@ async function loginWithPasskey() {
@if(!private_society || session)
<!-- Shortcuts Modal -->
<div id="shortcuts-modal" style="display: none;">
<div class="login-modal-content">
<button id="shortcuts-modal-close">&times;</button>
<div class="shortcuts-content">
<h2 style="text-align: center; margin-bottom: 20px;">{{ t('shortcuts.title') }}</h2>
<div class="login-modal-content sc-content">
<div class="sc-header">
<h2><i class="fa-solid fa-keyboard"></i> {{ t('shortcuts.title') }}</h2>
<button type="button" id="shortcuts-modal-close" aria-label="Close">&times;</button>
</div>
<div class="shortcuts-content sc-body">
<div class="shortcut-list">
<div class="shortcut-item"><span><kbd>k</kbd></span><span>{{ t('shortcuts.search') }}</span></div>
<div class="shortcut-item"><span><kbd>m</kbd></span><span>{{ t('shortcuts.main_page') }}</span></div>
@@ -963,6 +1104,58 @@ async function loginWithPasskey() {
</div>
</div>
<style>
/* Keyboard shortcuts modal: style guide (square, flat, accent border), fixed header, only the list scrolls */
#shortcuts-modal .sc-content {
--sc-muted: var(--text-muted, #8a8f98);
--sc-border: rgba(255, 255, 255, 0.1);
width: min(520px, 92vw); max-width: none; max-height: min(90vh, 90dvh);
display: flex; flex-direction: column; overflow: hidden;
margin: 0; padding: 0; text-align: left;
background: var(--bg) !important; border: 1px solid var(--accent) !important;
border-radius: 0 !important; box-shadow: 0 0 20px rgba(0, 0, 0, 0.5) !important;
}
#shortcuts-modal .sc-content * { border-radius: 0 !important; }
#shortcuts-modal .sc-header {
flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 16px 20px; border-bottom: 1px solid var(--sc-border);
}
#shortcuts-modal .sc-header h2 {
display: flex; align-items: center; gap: 10px; margin: 0;
font-size: 1.05em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
}
#shortcuts-modal .sc-header h2 i { color: var(--accent); }
#shortcuts-modal #shortcuts-modal-close {
position: static; width: 36px; height: 36px; margin: 0; padding: 0; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
background: none; border: 1px solid rgba(255, 255, 255, 0.25); color: var(--text-color, #fff);
font-size: 1.6em; line-height: 1; opacity: 1; cursor: pointer; transition: color 0.15s, border-color 0.15s;
}
#shortcuts-modal #shortcuts-modal-close:hover { color: var(--accent); border-color: var(--accent); }
#shortcuts-modal .sc-body {
flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
padding: 6px 20px 16px; scrollbar-width: thin; scrollbar-color: var(--accent) transparent;
}
#shortcuts-modal .shortcut-list { gap: 0; margin: 0; }
#shortcuts-modal .shortcut-item {
align-items: center; margin: 0; padding: 10px 0; border-bottom: 1px solid var(--sc-border);
}
#shortcuts-modal .shortcut-item:last-child { border-bottom: 0; }
#shortcuts-modal .shortcut-item span:last-child { color: var(--sc-muted); opacity: 1; }
#shortcuts-modal .shortcut-item kbd {
display: inline-block; min-width: 22px; padding: 2px 7px; text-align: center;
font-size: 0.85em; font-family: inherit; font-weight: 700;
background: transparent; border: 1px solid var(--accent); color: var(--accent); box-shadow: none;
}
@media (max-width: 600px) {
#shortcuts-modal { padding: 0 !important; align-items: stretch !important; }
#shortcuts-modal .sc-content {
width: 100vw; height: 100vh; height: 100dvh; max-height: none;
border: 0 !important; box-shadow: none !important;
}
#shortcuts-modal .sc-body { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
}
</style>
<div id="flash-container"></div>
@endif
+44
View File
@@ -0,0 +1,44 @@
{{-- Fullscreen modal for info pages opened from the sidebar footer (see /s/js/page-modal.js) --}}
<div id="page-modal" style="display: none;" role="dialog" aria-modal="true">
<div class="pgm-header">
<h3 class="pgm-title"></h3>
<div class="pgm-actions">
<button type="button" class="pgm-close" title="Close" aria-label="Close">&times;</button>
</div>
</div>
<div class="pgm-body"></div>
</div>
<style>
#page-modal {
/* Covers only the content area: below the navbar, between the sidebars (page-modal.js sets the insets).
Just below the sidebar drag handles (899) and sidebars (900) so they keep working while it's open. */
position: fixed; z-index: 895;
top: var(--pgm-top, 0px); right: var(--pgm-right, 0px); bottom: 0; left: var(--pgm-left, 0px);
flex-direction: column;
background: var(--bg); color: var(--text-color, #fff);
}
#page-modal .pgm-header {
flex-shrink: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px;
padding: 12px 20px; border-bottom: 1px solid var(--accent);
}
#page-modal .pgm-title {
margin: 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 1em; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em;
}
#page-modal .pgm-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
#page-modal .pgm-close {
display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px;
margin: 0; padding: 0; background: none; border: 1px solid transparent; border-radius: 0;
color: var(--text-color, #fff); font-size: 1em; line-height: 1; cursor: pointer; text-decoration: none;
transition: color 0.15s, border-color 0.15s;
}
#page-modal .pgm-close { font-size: 1.6em; border-color: rgba(255, 255, 255, 0.25); }
#page-modal .pgm-close:hover { color: var(--accent); border-color: var(--accent); }
#page-modal .pgm-body {
flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
padding-bottom: env(safe-area-inset-bottom, 0px);
}
#page-modal .pgm-loading { padding: 60px 20px; text-align: center; color: var(--text-muted, #aaa); }
#page-modal .pgm-loading a { color: var(--accent); }
</style>
<script src="/s/js/page-modal.js?v={{ ts }}"></script>
+2 -1
View File
@@ -2,6 +2,7 @@
<div id="visibility-modal" class="modal-overlay" style="display: none;">
<div class="modal-content vis-modal-content">
<div class="vis-header">
<button type="button" class="modal-x" aria-label="Close" title="Close" onclick="document.getElementById('visibility-modal-cancel').click()">&times;</button>
<h3><i class="fa-solid fa-eye"></i> Change visibility</h3>
<p>Choose who can see this post.</p>
</div>
@@ -74,7 +75,7 @@
}
#visibility-modal .vis-modal-content * { border-radius: 0 !important; }
#visibility-modal .vis-header { flex-shrink: 0; padding: 20px 24px 14px; border-bottom: 1px solid var(--vm-border); }
#visibility-modal .vis-header { position: relative; flex-shrink: 0; padding: 20px 64px 14px 24px; border-bottom: 1px solid var(--vm-border); }
#visibility-modal .vis-header h3 { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 1.2em; font-weight: 700; color: var(--vm-text); }
#visibility-modal .vis-header h3 i { color: var(--vm-accent); font-size: 0.9em; }
#visibility-modal .vis-header p { margin: 6px 0 0; font-size: 0.85em; color: var(--vm-muted); }