- Timestamp:
- 09/30/2026 06:41:45 AM (4 hours ago)
- Branches:
- main
- Children:
- 86cfa1a
- Parents:
- 3ceaef6
- Location:
- src/views
- Files:
-
- 1 added
- 7 edited
-
pages/authorize-interaction.ejs (modified) (1 diff)
-
pages/search.ejs (modified) (1 diff)
-
partials/fedi-interact-styles.ejs (modified) (3 diffs)
-
partials/remote-actor.ejs (modified) (1 diff)
-
partials/remote-object.ejs (modified) (1 diff)
-
partials/remote-preview.ejs (added)
-
partials/topnav.ejs (modified) (2 diffs)
-
shell.ejs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/views/pages/authorize-interaction.ejs
r3ceaef6 r9eb6e1b 106 106 </form> 107 107 <% } else { %> 108 <%- include('../partials/remote-object', { target: target, uri: uri, reacted: (typeof reacted !== 'undefined' ? reacted : { liked: false, boosted: false }), siteTitle: siteTitle, cancelUrl: uri}) %>108 <%- include('../partials/remote-object', { target: target, uri: uri, reacted: (typeof reacted !== 'undefined' ? reacted : { liked: false, boosted: false }), siteTitle: siteTitle, replyOpen: typeof replyOpen !== 'undefined' && replyOpen, reportOpen: typeof reportOpen !== 'undefined' && reportOpen }) %> 109 109 <% } %> 110 110 <% } %> -
src/views/pages/search.ejs
r3ceaef6 r9eb6e1b 40 40 <p class="auth-interact-note"><%= t('search.fedi_signin') %></p> 41 41 <% } else if (remote && remoteKind === 'note') { %> 42 <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search '}) %>42 <%- include('../partials/remote-object', { target: remote, uri: remoteUri, reacted: remoteReacted, siteTitle: siteTitle, cancelUrl: _base + '/search?q=' + encodeURIComponent(query) }) %> 43 43 <% } else if (remote && remoteKind === 'actor') { %> 44 <%- include('../partials/remote-actor', { actor: remote, cancelUrl: _base + '/search',siteTitle: siteTitle }) %>44 <%- include('../partials/remote-actor', { actor: remote, siteTitle: siteTitle }) %> 45 45 <% } else { %> 46 46 <p class="auth-interact-note"><%= t('fedi.remote_notfound') %></p> -
src/views/partials/fedi-interact-styles.ejs
r3ceaef6 r9eb6e1b 7 7 .auth-interact { max-width: 640px; margin: 1rem auto; padding: 0 1rem; } 8 8 .auth-interact-title { margin: 0 0 1rem; } 9 .auth-interact-meta { color: var(--ink-soft, #888); margin: 0 0 .75rem; }10 .auth-interact-preview {11 margin: 0 0 1rem; padding: .75rem 1rem; border-left: 3px solid var(--accent, #888);12 background: color-mix(in srgb, var(--ink, #000) 4%, transparent); border-radius: 0 12px 12px 0;13 color: var(--ink, inherit);14 }15 9 .auth-interact-form { display: flex; flex-direction: column; gap: .75rem; } 16 10 .auth-interact-form textarea, .auth-interact-form input[type=url] { … … 21 15 .auth-interact-actions { display: flex; gap: .5rem; align-items: center; } 22 16 .auth-interact-note { color: var(--ink-soft, #999); font-size: .85rem; margin-top: 1rem; } 23 .auth-interact-where { margin: .25rem 0 .85rem; padding: .6rem .8rem; font-size: .85rem; line-height: 1.5; border-radius: 10px; background: color-mix(in srgb, var(--accent, #888) 12%, transparent); color: var(--ink, inherit); }24 .auth-interact-content { line-height: 1.55; }25 .auth-interact-content p { margin: .35rem 0; }26 .auth-interact-content p:first-child { margin-top: 0; }27 .auth-interact-img { max-width: 100%; height: auto; border-radius: 10px; margin: .5rem 0 0; display: block; }28 .auth-interact-orig { margin: .75rem 0 0; font-size: .85rem; }29 .auth-interact-orig a { color: var(--accent, #06c); }30 .auth-view-src { display: inline-flex; align-items: center; gap: .5rem; margin: 0 0 1.1rem; padding: .6rem 1rem;31 border: 1px solid color-mix(in srgb, var(--accent, #888) 45%, transparent); border-radius: 12px;32 background: color-mix(in srgb, var(--accent, #888) 10%, transparent); color: var(--accent, #06c);33 text-decoration: none; font-weight: 600; font-size: .92rem; transition: background .15s, border-color .15s; }34 .auth-view-src:hover { background: color-mix(in srgb, var(--accent, #888) 18%, transparent); border-color: var(--accent, #888); }35 .auth-interact-react { display: flex; gap: .6rem; flex-wrap: wrap; margin: 0 0 .25rem; }36 .auth-interact-react form { margin: 0; }37 .fedi-bigact { display: inline-flex; align-items: center; gap: .5rem; padding: .55rem 1.1rem; border-radius: 999px;38 border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent);39 background: color-mix(in srgb, var(--ink, #000) 4%, transparent);40 color: var(--ink, inherit); font: inherit; font-weight: 600; font-size: .92rem; cursor: pointer; white-space: nowrap;41 transition: background .12s, border-color .12s; }42 .fedi-bigact svg { width: 17px; height: 17px; flex: 0 0 17px; }43 .fedi-bigact-like svg { color: #e8b04b; }44 .fedi-bigact-like:hover, .fedi-bigact-like.is-on { background: color-mix(in srgb, #e8b04b 16%, transparent); border-color: color-mix(in srgb, #e8b04b 55%, transparent); }45 .fedi-bigact-boost svg { color: #2fa85a; }46 .fedi-bigact-boost:hover, .fedi-bigact-boost.is-on { background: color-mix(in srgb, #2fa85a 16%, transparent); border-color: color-mix(in srgb, #2fa85a 55%, transparent); }47 17 .fedi-manage { list-style: none; padding: 0; margin: 1.25rem 0 1rem; display: flex; flex-direction: column; gap: .7rem; } 48 18 .fedi-manage-item { padding: .9rem 1rem; border-radius: 14px; background: color-mix(in srgb, var(--ink, #000) 3.5%, transparent); border: 1px solid color-mix(in srgb, var(--ink, #000) 9%, transparent); transition: border-color .15s ease, box-shadow .15s ease; } … … 87 57 .fedi-edit-save { padding: .4rem 1rem; border: 0; border-radius: 999px; background: var(--accent); color: var(--paper, #fff); 88 58 font: inherit; font-weight: 600; font-size: .82rem; cursor: pointer; } 89 /* Poll ballot / results on the interact page */ 90 .auth-poll { display: flex; flex-direction: column; gap: 8px; margin: 0 0 1rem; } 91 .auth-poll-choice { display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-radius: 10px; cursor: pointer; 59 60 /* ── De kaart van een post of profiel van een andere server ────────────── 61 Eén kaart voor de live-preview, de zoekpagina en /authorize_interaction. 62 De telefoon is het uitgangspunt: knoppen van minstens 44 punten, en op 63 een smal scherm vallen de woorden weg en blijven de tekens staan. */ 64 .rp-card { position: relative; padding: 14px 16px 4px; border-radius: 16px; 65 border: 1px solid color-mix(in srgb, var(--ink, #000) 10%, transparent); 66 background: color-mix(in srgb, var(--ink, #000) 2.5%, var(--paper, transparent)); 67 color: var(--ink, inherit); overflow: hidden; } 68 .rp-head { display: flex; align-items: center; gap: 10px; min-width: 0; margin: 0 0 10px; } 69 .rp-avatar { flex: 0 0 40px; width: 40px; height: 40px; border-radius: 50%; object-fit: cover; 70 background: color-mix(in srgb, var(--ink, #000) 8%, transparent); } 71 .rp-profile .rp-avatar { flex-basis: 56px; width: 56px; height: 56px; } 72 .rp-avatar-empty { display: inline-flex; align-items: center; justify-content: center; 73 font-weight: 700; color: var(--ink-soft, #888); } 74 .rp-who { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; } 75 .rp-name { font-weight: 700; color: var(--ink, inherit); text-decoration: none; 76 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 77 .rp-name:hover { text-decoration: underline; } 78 .rp-handle { font-size: .82rem; color: var(--ink-soft, #888); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 79 80 .rp-body { line-height: 1.55; overflow-wrap: anywhere; } 81 .rp-body p { margin: .45rem 0; } 82 .rp-body > :first-child { margin-top: 0; } 83 .rp-body > :last-child { margin-bottom: 0; } 84 .rp-body a { color: var(--accent, #06c); } 85 .rp-body img { max-width: 100%; height: auto; } 86 87 /* Beeld als raster: één foto op volle breedte, meer als tegels, zodat een 88 lange reeks de knoppen niet van het scherm duwt. */ 89 /* Vaste rijhoogtes in plaats van verhoudingen: met verhoudingen groeit het 90 raster mee met de breedte, en werd op een desktop de eerste van drie foto's 91 zo hoog dat de knoppen onder de vouw van de uitklaplijst verdwenen. */ 92 .rp-media { display: grid; gap: 3px; margin: 10px 0 0; border-radius: 12px; overflow: hidden; } 93 .rp-media a { display: block; min-width: 0; } 94 .rp-media img { display: block; width: 100%; height: 100%; object-fit: cover; } 95 .rp-media-1 img { height: auto; max-height: 480px; } 96 .rp-media-2, .rp-media-3, .rp-media-4 { grid-template-columns: 1fr 1fr; grid-auto-rows: clamp(120px, 28vw, 220px); } 97 .rp-media-3 a:first-child { grid-column: 1 / -1; } 98 .rp-compact .rp-media-1 img { max-height: 240px; } 99 .rp-compact .rp-media-2, .rp-compact .rp-media-3, .rp-compact .rp-media-4 { grid-auto-rows: 110px; } 100 101 /* Een inhoudswaarschuwing: dicht tot je ervoor kiest. */ 102 .rp-cw > summary { list-style: none; display: flex; align-items: center; justify-content: space-between; gap: 10px; 103 min-height: 44px; padding: 8px 12px; border-radius: 10px; cursor: pointer; font-weight: 600; 104 background: color-mix(in srgb, #e8b04b 12%, transparent); border: 1px solid color-mix(in srgb, #e8b04b 35%, transparent); } 105 .rp-cw > summary::-webkit-details-marker { display: none; } 106 .rp-cw-text { min-width: 0; overflow-wrap: anywhere; } 107 .rp-cw-open { flex: 0 0 auto; font-size: .8rem; color: var(--accent, #06c); } 108 .rp-cw[open] > summary { margin: 0 0 10px; } 109 .rp-cw[open] .rp-cw-open { display: none; } 110 111 /* De rij knoppen: gelijke tekens, het origineel rechts. */ 112 .rp-actions { display: flex; align-items: center; gap: 2px; margin: 10px -8px 0; padding: 2px 0; 113 border-top: 1px solid color-mix(in srgb, var(--ink, #000) 8%, transparent); } 114 .rp-actions form { margin: 0; } 115 .rp-act { display: inline-flex; align-items: center; justify-content: center; gap: 6px; 116 min-width: 44px; min-height: 44px; padding: 0 12px; border: 0; border-radius: 999px; 117 background: transparent; color: var(--ink-soft, #888); font: inherit; font-size: .88rem; font-weight: 600; 118 text-decoration: none; cursor: pointer; white-space: nowrap; -webkit-tap-highlight-color: transparent; 119 transition: background .12s, color .12s; } 120 .rp-act svg { flex: 0 0 18px; width: 18px; height: 18px; } 121 .rp-act:hover { background: color-mix(in srgb, var(--ink, #000) 6%, transparent); color: var(--ink, inherit); } 122 .rp-act:focus-visible { outline: 2px solid var(--accent, #06c); outline-offset: 2px; } 123 .rp-act[disabled] { opacity: .55; cursor: progress; } 124 .rp-like.is-on { color: #d99a1e; } 125 .rp-like.is-on svg { fill: currentColor; } 126 .rp-boost.is-on { color: #2fa85a; } 127 .rp-reply-btn[aria-expanded="true"] { color: var(--accent, #06c); } 128 .rp-src { margin-left: auto; } 129 .rp-primary { background: var(--accent, #06c); color: var(--paper, #fff); } 130 .rp-primary:hover { background: var(--accent, #06c); color: var(--paper, #fff); filter: brightness(1.08); } 131 .rp-profile .rp-actions { border-top: 0; margin-top: 4px; padding-bottom: 8px; } 132 .rp-profile .rp-actions form { flex: 0 0 auto; } 133 134 /* Een peiling in de kaart. */ 135 .rp-poll { display: flex; flex-direction: column; gap: 6px; margin: 12px 0 0; } 136 .rp-poll-choice { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; border-radius: 10px; cursor: pointer; 92 137 border: 1px solid color-mix(in srgb, var(--ink, #000) 14%, transparent); } 93 . auth-poll-choice:hover { border-color: var(--accent); }94 . auth-poll-choice input { accent-color: var(--accent); }95 . auth-poll-res { position: relative; padding: 9px 12px; border-radius: 10px; overflow: hidden; display: flex; align-items: center; gap: 8px;138 .rp-poll-choice:hover { border-color: var(--accent, #06c); } 139 .rp-poll-choice input { accent-color: var(--accent, #06c); } 140 .rp-poll-res { position: relative; display: flex; align-items: center; gap: 8px; padding: 9px 12px; border-radius: 10px; overflow: hidden; 96 141 background: color-mix(in srgb, var(--ink, #000) 6%, transparent); } 97 .auth-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent) 22%, transparent); z-index: 0; } 98 .auth-poll-name { position: relative; z-index: 1; flex: 1; font-size: .95rem; } 99 .auth-poll-pct { position: relative; z-index: 1; font-variant-numeric: tabular-nums; font-weight: 500; } 100 .auth-poll-foot { font-size: .82rem; color: var(--ink-soft, #888); margin: .1rem 0 0; } 101 .auth-report { margin: .6rem 0 0; } 102 .auth-report summary { cursor: pointer; font-size: .85rem; color: var(--ink-soft, #888); } 103 .auth-report summary:hover, .auth-report[open] summary { color: #c0392b; } 104 .auth-report[open] summary { margin-bottom: .5rem; } 105 .auth-report-note { font-size: .82rem; color: var(--ink-muted, #999); margin: 0 0 .5rem; } 106 /* Het gezicht van een profiel dat we net hebben opgehaald. */ 107 .remote-actor { display: flex; align-items: center; gap: .75rem; } 108 .remote-actor .auth-interact-meta { margin: 0; } 109 .remote-actor-icon { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; flex: 0 0 48px; 110 background: color-mix(in srgb, var(--ink, #000) 8%, transparent); } 142 .rp-poll-fill { position: absolute; inset: 0 auto 0 0; background: color-mix(in srgb, var(--accent, #06c) 22%, transparent); } 143 .rp-poll-name { position: relative; flex: 1; } 144 .rp-poll-pct { position: relative; font-variant-numeric: tabular-nums; font-weight: 600; } 145 .rp-poll-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; flex-wrap: wrap; 146 font-size: .82rem; color: var(--ink-soft, #888); } 147 148 /* Wat er onder de kaart staat, op de volle pagina. */ 149 .rp-reply { margin: 12px 0 0; } 150 .rp-reply-where { margin: 0 0 8px; font-size: .85rem; line-height: 1.5; color: var(--ink-soft, #888); } 151 .rp-reply-foot { display: flex; justify-content: flex-end; margin-top: 4px; } 152 .rp-as { margin: 12px 2px 0; font-size: .8rem; color: var(--ink-soft, #999); } 153 .rp-report { margin: 6px 2px 0; } 154 .rp-report > summary { display: inline-block; padding: 8px 0; cursor: pointer; font-size: .8rem; color: var(--ink-soft, #888); } 155 .rp-report > summary:hover, .rp-report[open] > summary { color: #c0392b; } 156 .rp-report-form { display: flex; flex-direction: column; gap: .6rem; } 157 .rp-report-note { margin: 0; font-size: .82rem; color: var(--ink-soft, #999); } 158 .rp-report-form textarea { width: 100%; box-sizing: border-box; padding: .6rem .8rem; font: inherit; resize: vertical; border-radius: 12px; 159 border: 1px solid color-mix(in srgb, var(--ink, #000) 18%, transparent); background: var(--paper, #fff); color: var(--ink, #000); } 160 161 /* De live-preview: open-link onder de kaart, en de kaart zonder rand-marge. */ 162 .rp-open { display: block; padding: 10px 4px 2px; text-align: right; font-weight: 600; font-size: .88rem; 163 color: var(--accent, #06c); text-decoration: none; } 164 .rp-open:hover { text-decoration: underline; } 165 .ss-remote { padding: 4px 0 6px; } 166 .ss-remote-note { padding: .8rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; } 167 .ss-remote-loading { display: flex; align-items: center; gap: 10px; padding: .9rem .6rem; color: var(--ink-soft, var(--ink-muted)); font-size: .9rem; } 168 .ss-remote-loading::before { content: ''; width: 16px; height: 16px; border-radius: 50%; flex: 0 0 16px; 169 border: 2px solid color-mix(in srgb, var(--accent, #06c) 30%, transparent); border-top-color: var(--accent, #06c); 170 animation: ss-spin .8s linear infinite; } 171 @keyframes ss-spin { to { transform: rotate(360deg); } } 172 @media (prefers-reduced-motion: reduce) { .ss-remote-loading::before { animation: none; } } 173 174 /* Smal scherm: alleen de tekens, gelijk verdeeld over de breedte. De woorden 175 blijven bestaan voor wie het scherm voorgelezen krijgt. */ 176 @media (max-width: 480px) { 177 .rp-card { padding: 12px 12px 2px; border-radius: 14px; } 178 .rp-actions { margin: 8px -4px 0; justify-content: space-between; } 179 .rp-act .rp-act-label { position: absolute; width: 1px; height: 1px; overflow: hidden; 180 clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; } 181 .rp-src { margin-left: 0; } 182 .rp-media-1 img { max-height: 360px; } 183 } 111 184 </style> -
src/views/partials/remote-actor.ejs
r3ceaef6 r9eb6e1b 2 2 Een PERSOON van een andere server: wie het is, en de knop om te volgen. 3 3 4 Zelfde reden als remote-object: de zoekbalk en /authorize_interaction laten5 hetzelfde zien, dus staat het op één plek. Het volgen zelf gaat naar6 /authorize_interaction/follow, want daar zit de poort van de guardians7 (shaer-p729) en die hoort niet twee keer geschreven te worden.4 Dezelfde kaart als een post (remote-object), zodat een profiel en een post in 5 de zoekbalk er als familie uitzien en niet als twee formulieren. Het volgen 6 zelf gaat naar /authorize_interaction/follow, want daar zit de poort van de 7 guardians (shaer-p729) en die hoort niet twee keer geschreven te worden. 8 8 9 Verwacht: actor (uit resolveRemoteActor). Optioneel: cancelUrl, siteTitle. 9 Verwacht: actor (uit resolveRemoteActor). Optioneel: compact (de 10 live-preview), siteTitle. 10 11 %> 11 <% const _annuleer = (typeof cancelUrl !== 'undefined' && cancelUrl) ? cancelUrl : '/'; %> 12 <div class="remote-actor"> 13 <% if (actor.actor_icon) { %> 14 <img class="remote-actor-icon" src="<%= actor.actor_icon %>" alt="" loading="lazy" width="48" height="48"> 15 <% } %> 16 <p class="auth-interact-meta"> 17 <%= t('fedi.follow_intro') %> 18 <a href="<%= actor.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= actor.actor_name %></strong></a> 19 <span class="fedi-handle"><%= actor.actor_handle %></span> 20 </p> 21 </div> 22 <form method="post" action="/authorize_interaction/follow" class="auth-interact-form"> 23 <input type="hidden" name="uri" value="<%= actor.actor_uri %>"> 24 <div class="auth-interact-actions"> 25 <button type="submit" class="btn btn-primary"><%= t('fedi.follow_btn') %></button> 26 <a href="<%= _annuleer %>" class="btn"><%= t('comments.cancel') %></a> 27 </div> 28 </form> 29 <p class="auth-interact-note"><%= t('fedi.remote_as', { site: typeof siteTitle !== 'undefined' ? siteTitle : '' }) %></p> 12 <% 13 const _compact = !!locals.compact; 14 const _initial = String(actor.actor_name || actor.actor_handle || '?').replace(/^@/, '').slice(0, 1).toUpperCase(); 15 %> 16 <article class="rp-card rp-profile<%= _compact ? ' rp-compact' : '' %>"> 17 <header class="rp-head"> 18 <% if (actor.actor_icon) { %> 19 <img class="rp-avatar" src="<%= actor.actor_icon %>" alt="" width="56" height="56" loading="lazy"> 20 <% } else { %> 21 <span class="rp-avatar rp-avatar-empty" aria-hidden="true"><%= _initial %></span> 22 <% } %> 23 <div class="rp-who"> 24 <a class="rp-name" href="<%= actor.actor_url || actor.actor_uri %>" rel="nofollow noopener" target="_blank"><%= actor.actor_name || actor.actor_handle %></a> 25 <span class="rp-handle"><%= actor.actor_handle %></span> 26 </div> 27 </header> 28 <footer class="rp-actions"> 29 <form method="post" action="/authorize_interaction/follow"> 30 <input type="hidden" name="uri" value="<%= actor.actor_uri %>"> 31 <button type="submit" class="rp-act rp-primary"> 32 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><line x1="19" y1="8" x2="19" y2="14"/><line x1="22" y1="11" x2="16" y2="11"/></svg> 33 <span><%= t('fedi.follow_btn') %></span> 34 </button> 35 </form> 36 <a class="rp-act rp-src" href="<%= actor.actor_url || actor.actor_uri %>" target="_blank" rel="nofollow noopener" title="<%= t('fedi.view_profile') %>"> 37 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg> 38 <span class="rp-act-label"><%= t('fedi.profile_short') %></span> 39 </a> 40 </footer> 41 </article> 42 <% if (!_compact) { %> 43 <p class="rp-as"><%= t('fedi.remote_as', { site: typeof siteTitle !== 'undefined' ? siteTitle : '' }) %></p> 44 <% } %> -
src/views/partials/remote-object.ejs
r3ceaef6 r9eb6e1b 1 1 <%# 2 Een object van een andere server: hoe heteruit ziet, en wat je ermee kunt.2 Een post van een andere server: hoe hij eruit ziet, en wat je ermee kunt. 3 3 4 EEN PLEK, TWEE INGANGEN (de zoekbalk en /authorize_interaction). Dit stond 5 alleen op die tweede pagina, en toen de zoekfunctie een URL leerde herkennen 6 zou het een tweede render worden -- en daarmee een tweede plek waar een poort, 7 een sanitizer of een knop vergeten kan worden. De formulieren posten dus ook 8 vanaf de zoekpagina naar /authorize_interaction/..., want daar zit het werk en 9 daar zit de rechtencontrole. 4 EEN PLEK, DRIE INGANGEN: de live-preview onder de zoekbalk, de zoekpagina en 5 /authorize_interaction. Eén render, zodat een poort, een sanitizer of een knop 6 niet op één plek vergeten kan worden. De formulieren posten overal naar 7 /authorize_interaction/..., want daar zit het werk en de rechtencontrole. 8 9 Hij ziet eruit als een POST, niet als een formulier over een post: kop met 10 gezicht, de inhoud, en één rij knoppen eronder. Wat daar vroeger omheen stond 11 (een zin "je reageert op", een aparte knop naar het origineel, twee grote 12 pillen, "of reageer:" en een altijd open antwoordvenster) is teruggebracht tot 13 die ene rij. Antwoorden klapt pas open als je erom vraagt, melden staat 14 onderaan en niet in de weg. 10 15 11 16 Verwacht: target (uit resolveRemoteNote), uri, reacted {liked,boosted}. 12 Optioneel: mayReply (false = alleen lezen en reageren, geen antwoordvenster), 13 cancelUrl (waar Annuleren heen gaat, standaard het origineel), siteTitle. 17 Optioneel: 18 compact de live-preview: geen antwoordvenster en geen meldformulier; die 19 twee staan een tik verder op de volle pagina. Een editor in een 20 uitklaplijst is op een telefoon geen plek om te schrijven. 21 mayReply false = alleen lezen en reageren, geen antwoordknop 22 replyOpen het antwoordvenster staat al open (?reply=1, de terugval zonder JS) 23 reportOpen idem voor melden (?report=1) 24 cancelUrl waar Annuleren heen gaat zonder JS, standaard de volle pagina 25 siteTitle 14 26 %> 15 <% const _cancel = (typeof cancelUrl !== 'undefined' && cancelUrl) ? cancelUrl : uri; %> 16 <p class="auth-interact-meta"> 17 <%= t('fedi.remote_replying_to') %> 18 <a href="<%= target.actor_url %>" rel="nofollow noopener" target="_blank"><strong><%= target.actor_name %></strong></a> 19 <span class="fedi-handle"><%= target.actor_handle %></span> 20 </p> 21 <blockquote class="auth-interact-preview"> 22 <% if (target.content) { %><div class="auth-interact-content"><%- target.content %></div> 23 <% } else if (target.preview) { %><%= target.preview %><% } %> 24 <% (target.images || []).forEach(function(im){ %> 25 <img class="auth-interact-img" src="<%= im %>" alt="" loading="lazy"> 26 <% }); %> 27 </blockquote> 28 <% if (target.url || uri) { %> 29 <a class="auth-view-src" href="<%= target.url || uri %>" target="_blank" rel="nofollow noopener"> 30 <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg> 31 <span><%= t('fedi.remote_view_original') %></span> 27 <% 28 const _compact = !!locals.compact; 29 const _mayReply = locals.mayReply !== false; 30 const _full = '/authorize_interaction?uri=' + encodeURIComponent(uri); 31 const _cancel = locals.cancelUrl || _full; 32 const _liked = !!(locals.reacted && reacted.liked); 33 const _boosted = !!(locals.reacted && reacted.boosted); 34 const _src = target.url || uri; 35 // Een inhoudswaarschuwing wordt gerespecteerd. De oude render toonde alles 36 // gewoon open, ook wat de schrijver achter een waarschuwing had gezet. 37 const _warn = target.cw || (target.sensitive ? t('fedi.sensitive') : ''); 38 const _imgs = (target.images || []).slice(0, 4); 39 const _initial = String(target.actor_name || target.actor_handle || '?').replace(/^@/, '').slice(0, 1).toUpperCase(); 40 %> 41 <article class="rp-card<%= _compact ? ' rp-compact' : '' %>"> 42 <header class="rp-head"> 43 <% if (target.actor_icon) { %> 44 <img class="rp-avatar" src="<%= target.actor_icon %>" alt="" width="40" height="40" loading="lazy"> 45 <% } else { %> 46 <span class="rp-avatar rp-avatar-empty" aria-hidden="true"><%= _initial %></span> 47 <% } %> 48 <div class="rp-who"> 49 <a class="rp-name" href="<%= target.actor_url || target.actor_uri %>" rel="nofollow noopener" target="_blank"><%= target.actor_name || target.actor_handle %></a> 50 <span class="rp-handle"><%= target.actor_handle %></span> 51 </div> 52 </header> 53 54 <% if (_warn) { %> 55 <details class="rp-cw"> 56 <summary><span class="rp-cw-text"><%= _warn %></span><span class="rp-cw-open"><%= t('fedi.cw_open') %></span></summary> 57 <% } %> 58 <% if (target.content) { %> 59 <div class="rp-body"><%- target.content %></div> 60 <% } else if (target.preview) { %> 61 <div class="rp-body"><p><%= target.preview %></p></div> 62 <% } %> 63 <% if (_imgs.length) { %> 64 <div class="rp-media rp-media-<%= _imgs.length %>"> 65 <% _imgs.forEach(function (im) { %> 66 <a href="<%= im %>" target="_blank" rel="nofollow noopener"><img src="<%= im %>" alt="" loading="lazy"></a> 67 <% }); %> 68 </div> 69 <% } %> 70 <% if (_warn) { %> 71 </details> 72 <% } %> 73 74 <% if (target.poll) { var _pl = target.poll; var _tot = _pl.options.reduce(function(s,o){return s+(o.count||0);},0); var _vn = (_pl.voters!=null?_pl.voters:_tot); %> 75 <% if (!_pl.closed) { %> 76 <form method="post" action="/authorize_interaction/vote" class="rp-poll"> 77 <input type="hidden" name="uri" value="<%= uri %>"> 78 <% _pl.options.forEach(function(o){ %> 79 <label class="rp-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label> 80 <% }); %> 81 <div class="rp-poll-foot"> 82 <span><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %><% if (_pl.endTime) { %> · <%= t('poll.closes') %> <%= new Date(_pl.endTime).toLocaleString() %><% } %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></span> 83 <button type="submit" class="rp-act rp-primary"><%= t('poll.vote') %></button> 84 </div> 85 </form> 86 <% } else { %> 87 <div class="rp-poll"> 88 <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %> 89 <div class="rp-poll-res"><span class="rp-poll-fill" style="width:<%= _pct %>%"></span><span class="rp-poll-name"><%= o.name %></span><span class="rp-poll-pct"><%= _pct %>%</span></div> 90 <% }); %> 91 <div class="rp-poll-foot"><span><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.closed') %></span></div> 92 </div> 93 <% } %> 94 <% } %> 95 96 <%# Eén rij. Op een smalle telefoon vallen de woorden weg en blijven de 97 tekens staan, elk minstens 44 punten groot: een duim is geen muis. De 98 woorden blijven voor een schermlezer wel gewoon bestaan. %> 99 <footer class="rp-actions"> 100 <% if (_mayReply) { %> 101 <a class="rp-act rp-reply-btn" href="<%= _full %>&reply=1#rp-reply" data-rp-reply<% if (!_compact) { %> aria-controls="rp-reply" aria-expanded="<%= locals.replyOpen ? 'true' : 'false' %>"<% } %>> 102 <svg 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> 103 <span class="rp-act-label"><%= t('fedi.reply_short') %></span> 32 104 </a> 33 105 <% } %> 34 <% if (target.poll) { var _pl = target.poll; var _tot = _pl.options.reduce(function(s,o){return s+(o.count||0);},0); var _vn = (_pl.voters!=null?_pl.voters:_tot); %> 35 <% if (!_pl.closed) { %> 36 <form method="post" action="/authorize_interaction/vote" class="auth-poll"> 37 <input type="hidden" name="uri" value="<%= uri %>"> 38 <% _pl.options.forEach(function(o){ %> 39 <label class="auth-poll-choice"><input type="<%= _pl.multiple ? 'checkbox' : 'radio' %>" name="choice" value="<%= o.name %>"><span><%= o.name %></span></label> 40 <% }); %> 41 <div class="auth-interact-actions"> 42 <button type="submit" class="btn btn-primary"><%= t('poll.vote') %></button> 43 </div> 44 <p class="auth-poll-foot"><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.open') %><% if (_pl.endTime) { %> · <%= t('poll.closes') %> <%= new Date(_pl.endTime).toLocaleString() %><% } %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></p> 45 </form> 46 <% } else { %> 47 <div class="auth-poll"> 48 <% _pl.options.forEach(function(o){ var _pct = _tot ? Math.round((o.count||0)*100/_tot) : 0; %> 49 <div class="auth-poll-res"><span class="auth-poll-fill" style="width:<%= _pct %>%"></span><span class="auth-poll-name"><%= o.name %></span><span class="auth-poll-pct"><%= _pct %>%</span></div> 50 <% }); %> 51 <p class="auth-poll-foot"><%= _vn %> <%= _vn === 1 ? t('poll.voter_one') : t('poll.voter_many') %> · <%= t('poll.closed') %><% if (_pl.multiple) { %> · <%= t('poll.multiple') %><% } %></p> 52 </div> 53 <% } %> 54 <% } %> 55 <% var _liked = (typeof reacted !== 'undefined' && reacted.liked); var _boosted = (typeof reacted !== 'undefined' && reacted.boosted); %> 56 <div class="auth-interact-react"> 57 <form method="post" action="/authorize_interaction/like" class="fedi-react-form"> 58 <input type="hidden" name="uri" value="<%= uri %>"> 59 <button type="submit" class="fedi-bigact fedi-bigact-like<%= _liked ? ' is-on' : '' %>" data-on="<%= t('fedi.unlike_short') %>" data-off="<%= t('fedi.like_short') %>"> 60 <svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg> 61 <span class="fedi-bigact-label"><%= _liked ? t('fedi.unlike_short') : t('fedi.like_short') %></span> 62 </button> 63 </form> 64 <form method="post" action="/authorize_interaction/boost" class="fedi-react-form"> 65 <input type="hidden" name="uri" value="<%= uri %>"> 66 <button type="submit" class="fedi-bigact fedi-bigact-boost<%= _boosted ? ' is-on' : '' %>" data-on="<%= t('tl.unboost') %>" data-off="<%= t('fedi.boost_short') %>"> 67 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> 68 <span class="fedi-bigact-label"><%= _boosted ? t('tl.unboost') : t('fedi.boost_short') %></span> 69 </button> 70 </form> 71 </div> 72 <% if (locals.mayReply !== false) { %> 73 <p class="auth-interact-or"><%= t('fedi.or_reply') %></p> 74 <p class="auth-interact-where">💬 <%= t('fedi.reply_where') %></p> 75 <div class="auth-interact-form"> 76 <%- include('./reply-editor', { 77 action: '/authorize_interaction', 78 hiddenFields: [{ name: 'uri', value: uri }], 79 placeholder: t('fedi.reply_ph'), 80 submitLabel: t('fedi.send'), 81 rows: 4, 82 participants: (target && target.actor_uri) ? [{ 83 uri: target.actor_uri, 84 url: target.actor_url || target.actor_uri, 85 handle: target.actor_handle || '', 86 }] : [], 87 }) %> 88 <div class="auth-interact-actions"><a href="<%= _cancel %>" class="btn"><%= t('comments.cancel') %></a></div> 89 </div> 90 <% } %> 91 <p class="auth-interact-note"><%= t('fedi.remote_as', { site: siteTitle }) %></p> 92 <details class="auth-report"> 93 <summary>🚩 <%= t('fedi.report_open') %></summary> 94 <form method="post" action="/authorize_interaction/report" class="auth-interact-form"> 95 <input type="hidden" name="uri" value="<%= uri %>"> 96 <input type="hidden" name="actor_uri" value="<%= target.actor_uri %>"> 97 <p class="auth-report-note"><%= t('fedi.report_where') %></p> 98 <textarea name="reason" rows="3" maxlength="3000" placeholder="<%= t('fedi.report_ph') %>"></textarea> 99 <div class="auth-interact-actions"> 100 <button type="submit" class="btn btn-danger"><%= t('fedi.report_send') %></button> 101 </div> 102 </form> 103 </details> 106 <form method="post" action="/authorize_interaction/like" class="fedi-react-form"> 107 <input type="hidden" name="uri" value="<%= uri %>"> 108 <button type="submit" class="rp-act rp-like<%= _liked ? ' is-on' : '' %>" aria-pressed="<%= _liked %>" data-on="<%= t('fedi.unlike_short') %>" data-off="<%= t('fedi.like_short') %>"> 109 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 2.6l2.9 5.88 6.49.95-4.7 4.58 1.11 6.46L12 17.96l-5.8 3.06 1.1-6.46-4.69-4.58 6.49-.95z"/></svg> 110 <span class="rp-act-label"><%= _liked ? t('fedi.unlike_short') : t('fedi.like_short') %></span> 111 </button> 112 </form> 113 <form method="post" action="/authorize_interaction/boost" class="fedi-react-form"> 114 <input type="hidden" name="uri" value="<%= uri %>"> 115 <button type="submit" class="rp-act rp-boost<%= _boosted ? ' is-on' : '' %>" aria-pressed="<%= _boosted %>" data-on="<%= t('tl.unboost') %>" data-off="<%= t('fedi.boost_short') %>"> 116 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="17 1 21 5 17 9"/><path d="M3 11V9a4 4 0 0 1 4-4h14"/><polyline points="7 23 3 19 7 15"/><path d="M21 13v2a4 4 0 0 1-4 4H3"/></svg> 117 <span class="rp-act-label"><%= _boosted ? t('tl.unboost') : t('fedi.boost_short') %></span> 118 </button> 119 </form> 120 <a class="rp-act rp-src" href="<%= _src %>" target="_blank" rel="nofollow noopener" title="<%= t('fedi.remote_view_original') %>"> 121 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7"/><path d="M7 7h10v10"/></svg> 122 <span class="rp-act-label"><%= t('fedi.src_short') %></span> 123 </a> 124 </footer> 125 </article> 126 127 <% if (_compact) { %> 128 <a class="rp-open" href="<%= _full %>"><%= t('fedi.open_full') %> →</a> 129 <% } else { %> 130 <% if (_mayReply) { %> 131 <%# Dicht tot je op Reageer tikt. Zonder JS opent ?reply=1 hem, zodat de 132 knop ook dan ergens heen gaat. %> 133 <section class="rp-reply" id="rp-reply"<%= locals.replyOpen ? '' : ' hidden' %>> 134 <p class="rp-reply-where"><%= t('fedi.reply_where') %></p> 135 <%- include('./reply-editor', { 136 action: '/authorize_interaction', 137 hiddenFields: [{ name: 'uri', value: uri }], 138 placeholder: t('fedi.reply_ph'), 139 submitLabel: t('fedi.send'), 140 rows: 4, 141 participants: (target && target.actor_uri) ? [{ 142 uri: target.actor_uri, 143 url: target.actor_url || target.actor_uri, 144 handle: target.actor_handle || '', 145 }] : [], 146 }) %> 147 <div class="rp-reply-foot"><a href="<%= _cancel %>" class="rp-act" data-rp-reply-close><%= t('comments.cancel') %></a></div> 148 </section> 149 <% } %> 150 <p class="rp-as"><%= t('fedi.remote_as', { site: siteTitle }) %></p> 151 <details class="rp-report" id="rp-report"<%= locals.reportOpen ? ' open' : '' %>> 152 <summary><%= t('fedi.report_open') %></summary> 153 <form method="post" action="/authorize_interaction/report" class="rp-report-form"> 154 <input type="hidden" name="uri" value="<%= uri %>"> 155 <input type="hidden" name="actor_uri" value="<%= target.actor_uri %>"> 156 <p class="rp-report-note"><%= t('fedi.report_where') %></p> 157 <textarea name="reason" rows="3" maxlength="3000" placeholder="<%= t('fedi.report_ph') %>"></textarea> 158 <div><button type="submit" class="btn btn-danger"><%= t('fedi.report_send') %></button></div> 159 </form> 160 </details> 161 <% } %> -
src/views/partials/topnav.ejs
r3ceaef6 r9eb6e1b 178 178 <%# De teksten staan op het element dat ze gebruikt, niet in het script: een 179 179 module is een statisch bestand en kan geen vertaling interpoleren. %> 180 <div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data- action="<%= _siteUrlBase %>/search"181 data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty') }) %>">180 <div id="search-overlay" class="search-overlay" hidden data-suggest="<%= _siteUrlBase %>/search/suggest" data-remote="<%= _siteUrlBase %>/search/remote" data-action="<%= _siteUrlBase %>/search" 181 data-i18n="<%= JSON.stringify({ posts: t('search.section_posts'), tracks: t('search.section_tracks'), events: t('search.section_events'), pages: t('search.section_pages'), all: t('search.suggest_all'), none: t('search.suggest_empty'), looking: t('search.fedi_looking') }) %>"> 182 182 <div class="container"> 183 183 <form class="search-overlay-form" method="get" action="<%= _siteUrlBase %>/search"> … … 467 467 } 468 468 /* Live-resultaten dropdown */ 469 .search-suggest { margin-top: .6rem; max-height: 60vh; overflow-y: auto; display: flex; flex-direction: column; gap: .25rem; }469 .search-suggest { margin-top: .6rem; max-height: 60vh; max-height: 70dvh; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; display: flex; flex-direction: column; gap: .25rem; } 470 470 .search-suggest:empty { display: none; } 471 471 .ss-group { padding: .25rem 0; } -
src/views/shell.ejs
r3ceaef6 r9eb6e1b 130 130 // ontwikkeling een jaar gecachet, dus zonder bump bereikt een reparatie 131 131 // iedereen behalve wie de fout al heeft. 132 const MOD_V = 6 4;132 const MOD_V = 65; 133 133 %><!DOCTYPE html> 134 134 <html lang="<%- _e(lang) %>" data-palette="<%- _e((typeof palette !== 'undefined' && palette) ? palette : (safeSite.palette || 'klonkt')) %>">
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)