| 1 | <%
|
|---|
| 2 | // Post editor — split into card sections for visual hierarchy:
|
|---|
| 3 | // - Identity : title, slug, excerpt
|
|---|
| 4 | // - Cover : thumbnail + URL + custom upload button
|
|---|
| 5 | // - Content : markdown textarea with attached toolbar
|
|---|
| 6 | // - Meta : status, type, pin, noindex, tags
|
|---|
| 7 | //
|
|---|
| 8 | // All behavior (IDs, names, upload endpoints) is preserved — only layout
|
|---|
| 9 | // and styling changed.
|
|---|
| 10 | const _hasCover = !!post.cover_image_url;
|
|---|
| 11 | // In hub mode, save/create/cancel must carry the /user/<slug>/ prefix, otherwise
|
|---|
| 12 | // the request falls back to the primary site (siteUrlBase empty) and the post
|
|---|
| 13 | // renders headerless (bareChrome) after saving. In solo mode _base is empty.
|
|---|
| 14 | const _base = (typeof siteUrlBase !== 'undefined' && siteUrlBase) ? siteUrlBase : '';
|
|---|
| 15 | %>
|
|---|
| 16 | <div class="container post-edit-page">
|
|---|
| 17 | <h1><%= isNew ? t('pedit.title_new') : t('pedit.title_edit') %></h1>
|
|---|
| 18 |
|
|---|
| 19 | <form method="post" action="<%= _base %><%= isNew ? '/posts/create' : '/posts/' + post.slug + '/save' %>" class="post-edit-form">
|
|---|
| 20 |
|
|---|
| 21 | <%# ── TYPE (segmented) + type-specific inputs ────────────────
|
|---|
| 22 | Post type lives at the TOP (above the body), and the selected type
|
|---|
| 23 | reveals its own input: Audio → inline upload, Video → embed URL,
|
|---|
| 24 | Foto → cover/insert hint. %>
|
|---|
| 25 | <% var ptype = post.type || 'post'; %>
|
|---|
| 26 | <% var TYPE_ICONS = { post: '📝', foto: '📷', video: '🎬', audio: '🎵' }; %>
|
|---|
| 27 | <section class="pe-card pe-type-card">
|
|---|
| 28 | <div class="pe-section-title"><%= t('pedit.s_type') %></div>
|
|---|
| 29 | <input type="hidden" name="type" id="pe-type-input" value="<%= ptype %>">
|
|---|
| 30 | <div class="pe-typeseg" role="radiogroup" aria-label="<%= t('pedit.s_type') %>">
|
|---|
| 31 | <% ['post','foto','video','audio'].forEach(function (tt) { %>
|
|---|
| 32 | <button type="button" class="pe-typeseg-btn<%= ptype === tt ? ' is-active' : '' %>"
|
|---|
| 33 | data-type="<%= tt %>" role="radio" aria-checked="<%= ptype === tt ? 'true' : 'false' %>">
|
|---|
| 34 | <span class="pe-typeseg-ic" aria-hidden="true"><%= TYPE_ICONS[tt] %></span>
|
|---|
| 35 | <span><%= t('pedit.type_' + tt) %></span>
|
|---|
| 36 | </button>
|
|---|
| 37 | <% }); %>
|
|---|
| 38 | </div>
|
|---|
| 39 |
|
|---|
| 40 | <%# Audio: inline upload (transcodes + drops [[track]] into the post) %>
|
|---|
| 41 | <div class="pe-type-panel" data-panel="audio" hidden>
|
|---|
| 42 | <div class="pe-audio-up" id="pe-audio-drop" tabindex="0" role="button" aria-label="<%= t('pedit.audio_up_drop') %>">
|
|---|
| 43 | <input type="file" id="pe-audio-file" accept="audio/*,.mp3,.m4a,.ogg,.opus,.flac,.wav" multiple hidden>
|
|---|
| 44 | <span class="pe-audio-up-ic" aria-hidden="true">🎵</span>
|
|---|
| 45 | <strong><%= t('pedit.audio_up_drop') %></strong>
|
|---|
| 46 | <small><%= t('pedit.audio_up_hint') %></small>
|
|---|
| 47 | </div>
|
|---|
| 48 | <ul class="pe-audio-list" id="pe-audio-list"></ul>
|
|---|
| 49 | </div>
|
|---|
| 50 |
|
|---|
| 51 | <%# Video: paste a URL → embed chip %>
|
|---|
| 52 | <div class="pe-type-panel" data-panel="video" hidden>
|
|---|
| 53 | <label class="pe-field">
|
|---|
| 54 | <span><%= t('pedit.video_up_title') %></span>
|
|---|
| 55 | <div class="pe-video-row">
|
|---|
| 56 | <input type="url" id="pe-video-url" inputmode="url" autocapitalize="none" spellcheck="false"
|
|---|
| 57 | placeholder="<%= t('pedit.video_up_ph') %>">
|
|---|
| 58 | <button type="button" class="pe-btn pe-btn-secondary" id="pe-video-insert"><%= t('pedit.video_up_btn') %></button>
|
|---|
| 59 | </div>
|
|---|
| 60 | </label>
|
|---|
| 61 | </div>
|
|---|
| 62 |
|
|---|
| 63 | <%# Foto: light hint toward cover + insert-image %>
|
|---|
| 64 | <div class="pe-type-panel" data-panel="foto" hidden>
|
|---|
| 65 | <p class="pe-foto-hint">🖼 <%= t('pedit.foto_up_hint') %></p>
|
|---|
| 66 | </div>
|
|---|
| 67 | </section>
|
|---|
| 68 |
|
|---|
| 69 | <%# ── IDENTITY ─────────────────────────────────────────────── %>
|
|---|
| 70 | <section class="pe-card">
|
|---|
| 71 | <label class="pe-field">
|
|---|
| 72 | <span><%= t('pedit.f_title') %></span>
|
|---|
| 73 | <input type="text" name="title" value="<%= post.title %>" required>
|
|---|
| 74 | </label>
|
|---|
| 75 | <div class="pe-row pe-row-2">
|
|---|
| 76 | <label class="pe-field">
|
|---|
| 77 | <span><%= t('pedit.f_slug') %></span>
|
|---|
| 78 | <input type="text" name="slug" value="<%= post.slug %>" placeholder="<%= t('pedit.slug_placeholder') %>">
|
|---|
| 79 | </label>
|
|---|
| 80 | <label class="pe-field">
|
|---|
| 81 | <span><%= t('pedit.f_tags') %> <small><%= t('pedit.tags_hint') %></small></span>
|
|---|
| 82 | <input type="text" name="tags" value="<%= Array.isArray(post.tags) ? post.tags.join(', ') : '' %>">
|
|---|
| 83 | </label>
|
|---|
| 84 | </div>
|
|---|
| 85 | <label class="pe-field">
|
|---|
| 86 | <span><%= t('pedit.f_excerpt') %></span>
|
|---|
| 87 | <textarea name="excerpt" rows="2"><%= post.excerpt %></textarea>
|
|---|
| 88 | <small class="pe-hint" style="opacity:.7"><%- t('pedit.excerpt_hint') %></small>
|
|---|
| 89 | </label>
|
|---|
| 90 | <% var _postLang = (typeof post.language !== 'undefined' && post.language) ? post.language : (typeof lang !== 'undefined' ? lang : 'en'); %>
|
|---|
| 91 | <label class="pe-field">
|
|---|
| 92 | <span><%= t('pedit.f_language') %> <small><%= t('pedit.language_hint') %></small></span>
|
|---|
| 93 | <select name="language" id="pe-language" style="max-width:220px">
|
|---|
| 94 | <% [['en','English'],['nl','Nederlands'],['de','Deutsch'],['fr','Français'],['es','Español'],['it','Italiano'],['pt','Português'],['pl','Polski'],['ru','Русский'],['uk','Українська'],['sv','Svenska'],['da','Dansk'],['nb','Norsk'],['fi','Suomi'],['tr','Türkçe'],['ja','日本語'],['zh','中文'],['ar','العربية']].forEach(function (l) { %>
|
|---|
| 95 | <option value="<%= l[0] %>" <%= l[0] === _postLang ? 'selected' : '' %>><%= l[1] %></option>
|
|---|
| 96 | <% }); %>
|
|---|
| 97 | </select>
|
|---|
| 98 | </label>
|
|---|
| 99 | </section>
|
|---|
| 100 |
|
|---|
| 101 | <%# ── COVER ────────────────────────────────────────────────── %>
|
|---|
| 102 | <section class="pe-card">
|
|---|
| 103 | <div class="pe-section-title"><%= t('pedit.s_cover') %></div>
|
|---|
| 104 | <div class="pe-cover-row">
|
|---|
| 105 | <div class="pe-cover-thumb" id="cover-preview-wrap" <%= _hasCover ? '' : 'data-empty' %>>
|
|---|
| 106 | <img src="<%= _hasCover ? post.cover_image_url : '' %>" alt="" id="cover-preview-img" <%= _hasCover ? '' : 'hidden' %>>
|
|---|
| 107 | <% if (!_hasCover) { %><span class="pe-cover-empty">🖼</span><% } %>
|
|---|
| 108 | </div>
|
|---|
| 109 | <div class="pe-cover-actions">
|
|---|
| 110 | <label class="pe-field">
|
|---|
| 111 | <span><%= t('pedit.f_cover_url') %></span>
|
|---|
| 112 | <input type="text" name="cover_image_url" id="cover-url-field"
|
|---|
| 113 | value="<%= post.cover_image_url || '' %>"
|
|---|
| 114 | inputmode="url" autocapitalize="none" spellcheck="false"
|
|---|
| 115 | placeholder="<%= t('pedit.cover_url_placeholder') %>">
|
|---|
| 116 | </label>
|
|---|
| 117 | <label class="pe-field">
|
|---|
| 118 | <span><%= t('pedit.f_cover_alt') %></span>
|
|---|
| 119 | <input type="text" name="cover_alt" id="cover-alt-field" maxlength="1500"
|
|---|
| 120 | value="<%= post.cover_alt || '' %>"
|
|---|
| 121 | placeholder="<%= t('pedit.cover_alt_placeholder') %>">
|
|---|
| 122 | </label>
|
|---|
| 123 | <%# Muted loop MP4 for an animated cover (auto-made from an animated WebP). Hidden — set by the uploader. %>
|
|---|
| 124 | <input type="hidden" name="cover_video_url" id="cover-video-field" value="<%= post.cover_video_url || '' %>">
|
|---|
| 125 | <div class="pe-cover-upload">
|
|---|
| 126 | <button type="button" class="pe-btn pe-btn-secondary" id="cover-upload-trigger">
|
|---|
| 127 | 📷 <%= t('pedit.cover_upload_btn') %>
|
|---|
| 128 | </button>
|
|---|
| 129 | <input type="file" id="cover-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
|---|
| 130 | <small id="cover-upload-status" class="pe-status"></small>
|
|---|
| 131 | </div>
|
|---|
| 132 | </div>
|
|---|
| 133 | </div>
|
|---|
| 134 | </section>
|
|---|
| 135 |
|
|---|
| 136 | <%# ── CONTENT ──────────────────────────────────────────────── %>
|
|---|
| 137 | <section class="pe-card pe-card-content">
|
|---|
| 138 | <div class="pe-section-title">
|
|---|
| 139 | <%= t('pedit.s_content') %>
|
|---|
| 140 | <small class="pe-content-hint"><%= t('pedit.content_hint') %></small>
|
|---|
| 141 | </div>
|
|---|
| 142 | <div class="pe-editor-frame">
|
|---|
| 143 | <div class="pe-toolbar" id="pe-toolbar" role="toolbar" aria-label="Format">
|
|---|
| 144 | <button type="button" id="pe-fs-done" class="pe-fs-done" title="<%= t('pedit.tb_done_title') %>">✓ <%= t('pedit.tb_done') %></button>
|
|---|
| 145 | <button type="button" data-cmd="bold" title="<%= t('pedit.tb_bold_title') %>" aria-label="<%= t('pedit.tb_bold') %>">
|
|---|
| 146 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h8a4 4 0 0 1 0 8H6z"/><path d="M6 12h9a4 4 0 0 1 0 8H6z"/></svg>
|
|---|
| 147 | </button>
|
|---|
| 148 | <button type="button" data-cmd="italic" title="<%= t('pedit.tb_italic_title') %>" aria-label="<%= t('pedit.tb_italic') %>">
|
|---|
| 149 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="19" y1="4" x2="10" y2="4"/><line x1="14" y1="20" x2="5" y2="20"/><line x1="15" y1="4" x2="9" y2="20"/></svg>
|
|---|
| 150 | </button>
|
|---|
| 151 | <button type="button" data-cmd="underline" title="<%= t('pedit.tb_underline') %>" aria-label="<%= t('pedit.tb_underline') %>">
|
|---|
| 152 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4v6a6 6 0 0 0 12 0V4"/><line x1="4" y1="20" x2="20" y2="20"/></svg>
|
|---|
| 153 | </button>
|
|---|
| 154 | <span class="pe-toolbar-sep" aria-hidden="true"></span>
|
|---|
| 155 | <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="h2" title="<%= t('pedit.tb_h2') %>">H2</button>
|
|---|
| 156 | <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="h3" title="<%= t('pedit.tb_h3') %>">H3</button>
|
|---|
| 157 | <button type="button" class="pe-tb-text" data-cmd="formatBlock" data-arg="p" title="<%= t('pedit.tb_p') %>">¶</button>
|
|---|
| 158 | <span class="pe-toolbar-sep" aria-hidden="true"></span>
|
|---|
| 159 | <button type="button" data-cmd="insertUnorderedList" title="<%= t('pedit.tb_ul') %>" aria-label="<%= t('pedit.tb_ul') %>">
|
|---|
| 160 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="9" y1="6" x2="20" y2="6"/><line x1="9" y1="12" x2="20" y2="12"/><line x1="9" y1="18" x2="20" y2="18"/><circle cx="4.5" cy="6" r="1.2"/><circle cx="4.5" cy="12" r="1.2"/><circle cx="4.5" cy="18" r="1.2"/></svg>
|
|---|
| 161 | </button>
|
|---|
| 162 | <button type="button" data-cmd="insertOrderedList" title="<%= t('pedit.tb_ol') %>" aria-label="<%= t('pedit.tb_ol') %>">
|
|---|
| 163 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="10" y1="6" x2="21" y2="6"/><line x1="10" y1="12" x2="21" y2="12"/><line x1="10" y1="18" x2="21" y2="18"/><path d="M4 6h1v4"/><path d="M4 10h2"/><path d="M6 18H4c0-1 2-2 2-3s-1-1.5-2-1"/></svg>
|
|---|
| 164 | </button>
|
|---|
| 165 | <button type="button" data-cmd="formatBlock" data-arg="blockquote" title="<%= t('pedit.tb_quote') %>" aria-label="<%= t('pedit.tb_quote') %>">
|
|---|
| 166 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2H4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h2.5C6 17.5 4 19 3 19v2z"/><path d="M14 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2h-4c-1.25 0-2 .75-2 2v6c0 1.25.75 2 2 2h2.5c-.5 4.5-2.5 6-3.5 6v2z"/></svg>
|
|---|
| 167 | </button>
|
|---|
| 168 | <button type="button" data-cmd="link-prompt" title="<%= t('pedit.tb_link_title') %>" aria-label="<%= t('pedit.tb_link') %>">
|
|---|
| 169 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
|---|
| 170 | </button>
|
|---|
| 171 | <button type="button" data-cmd="code-wrap" title="<%= t('pedit.tb_code_title') %>" aria-label="<%= t('pedit.tb_code') %>">
|
|---|
| 172 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
|---|
| 173 | </button>
|
|---|
| 174 | <span class="pe-toolbar-sep" aria-hidden="true"></span>
|
|---|
| 175 | <button type="button" id="insert-image-btn" title="<%= t('pedit.tb_image_title') %>" aria-label="<%= t('pedit.tb_image') %>">
|
|---|
| 176 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>
|
|---|
| 177 | </button>
|
|---|
| 178 | <button type="button" id="insert-track-btn" title="<%= t('pedit.tb_track_title') %>" aria-label="<%= t('pedit.tb_track') %>">
|
|---|
| 179 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
|---|
| 180 | </button>
|
|---|
| 181 | <button type="button" id="insert-playlist-btn" title="<%= t('pedit.tb_playlist_title') %>" aria-label="<%= t('pedit.tb_playlist') %>">
|
|---|
| 182 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="15" y2="18"/><polygon points="3 5 3 13 9 9"/></svg>
|
|---|
| 183 | </button>
|
|---|
| 184 | <button type="button" id="insert-embed-btn" title="<%= t('pedit.tb_embed_title') %>" aria-label="<%= t('pedit.tb_embed') %>">
|
|---|
| 185 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="20" height="16" rx="2"/><polygon points="10 9 15.5 12 10 15"/></svg>
|
|---|
| 186 | </button>
|
|---|
| 187 | <span class="pe-toolbar-spacer"></span>
|
|---|
| 188 | <button type="button" data-cmd="removeFormat" title="<%= t('pedit.tb_clear') %>" aria-label="<%= t('pedit.tb_clear') %>">
|
|---|
| 189 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M5 5l14 14" stroke-linecap="round"/></svg>
|
|---|
| 190 | </button>
|
|---|
| 191 | <button type="button" id="pe-fullscreen-btn" title="<%= t('pedit.tb_fullscreen') %>" aria-label="<%= t('pedit.tb_fullscreen') %>" aria-pressed="false">
|
|---|
| 192 | <svg class="fs-icon-expand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
|---|
| 193 | <svg class="fs-icon-compress" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
|---|
| 194 | </button>
|
|---|
| 195 | </div>
|
|---|
| 196 |
|
|---|
| 197 | <div id="content-editor"
|
|---|
| 198 | class="pe-editor"
|
|---|
| 199 | contenteditable="true"
|
|---|
| 200 | role="textbox"
|
|---|
| 201 | aria-multiline="true"
|
|---|
| 202 | aria-label="<%= t('pedit.editor_aria') %>"></div>
|
|---|
| 203 |
|
|---|
| 204 | <%# Sticky "tap to edit" hint (only visible on touch + non-fullscreen
|
|---|
| 205 | via CSS). Purely visual (pointer-events:none); tapping the field opens fullscreen. %>
|
|---|
| 206 | <div class="pe-edit-hint" aria-hidden="true">✎ <%= t('pedit.tap_to_edit') %></div>
|
|---|
| 207 |
|
|---|
| 208 | <%# Hidden field is what actually submits to the server. The visible
|
|---|
| 209 | contenteditable's serialized HTML is copied here on submit. %>
|
|---|
| 210 | <input type="hidden" name="content" id="content-hidden" value="">
|
|---|
| 211 |
|
|---|
| 212 | <%# Initial content is injected as a JSON string in a script tag so we
|
|---|
| 213 | can set innerHTML safely from JS. EJS-escaping HTML directly into
|
|---|
| 214 | the editor would render entity-escaped tags as text. %>
|
|---|
| 215 | <script id="initial-content" type="application/json"><%- JSON.stringify(post.content || '').replace(/</g, '\\u003c') %></script>
|
|---|
| 216 |
|
|---|
| 217 | <div class="pe-editor-status">
|
|---|
| 218 | <small id="content-upload-status" class="pe-status"></small>
|
|---|
| 219 | <span class="pe-char-count"><span id="char-count">0</span> <%= t('pedit.chars') %></span>
|
|---|
| 220 | </div>
|
|---|
| 221 |
|
|---|
| 222 | <input type="file" id="content-upload-field" accept="image/jpeg,image/png,image/webp,image/gif" hidden>
|
|---|
| 223 | </div>
|
|---|
| 224 | </section>
|
|---|
| 225 |
|
|---|
| 226 | <%# ── META ─────────────────────────────────────────────────── %>
|
|---|
| 227 | <section class="pe-card">
|
|---|
| 228 | <div class="pe-section-title"><%= t('pedit.s_publication') %></div>
|
|---|
| 229 | <label class="pe-field">
|
|---|
| 230 | <span><%= t('pedit.f_status') %></span>
|
|---|
| 231 | <% var _st = isNew ? 'published' : (post.status || 'draft'); %>
|
|---|
| 232 | <select name="status">
|
|---|
| 233 | <option value="published" <%= _st === 'published' ? 'selected' : '' %>><%= t('pedit.status_published') %></option>
|
|---|
| 234 | <option value="draft" <%= _st === 'draft' ? 'selected' : '' %>><%= t('pedit.status_draft') %></option>
|
|---|
| 235 | <option value="archived" <%= _st === 'archived' ? 'selected' : '' %>><%= t('pedit.status_archived') %></option>
|
|---|
| 236 | </select>
|
|---|
| 237 | </label>
|
|---|
| 238 | <div class="pe-checkboxes">
|
|---|
| 239 | <div class="pe-pin">
|
|---|
| 240 | <label class="pe-checkbox">
|
|---|
| 241 | <input type="checkbox" id="pin-toggle" <%= (Number(post.pinned) > 0) ? 'checked' : '' %>>
|
|---|
| 242 | <span>📌 <%= t('pedit.pin_label') %></span>
|
|---|
| 243 | </label>
|
|---|
| 244 | <input type="hidden" name="pinned" id="pin-rank" value="<%= post.pinned || 0 %>">
|
|---|
| 245 | <div class="pe-pin-pos" id="pin-pos" <%= (Number(post.pinned) > 0) ? '' : 'hidden' %>>
|
|---|
| 246 | <span class="pe-pin-steps">
|
|---|
| 247 | <button type="button" class="pe-pin-btn" id="pin-up" aria-label="<%= t('pedit.pin_up') %>">▲</button>
|
|---|
| 248 | <button type="button" class="pe-pin-btn" id="pin-down" aria-label="<%= t('pedit.pin_down') %>">▼</button>
|
|---|
| 249 | </span>
|
|---|
| 250 | <span class="pe-pin-label" id="pin-label"></span>
|
|---|
| 251 | </div>
|
|---|
| 252 | </div>
|
|---|
| 253 | <label class="pe-checkbox">
|
|---|
| 254 | <input type="checkbox" name="noindex" value="1" <%= post.noindex ? 'checked' : '' %>>
|
|---|
| 255 | <span>🚫 <%= t('pedit.noindex_label') %></span>
|
|---|
| 256 | </label>
|
|---|
| 257 | <label class="pe-checkbox">
|
|---|
| 258 | <input type="checkbox" name="nsfw" value="1" id="pe-nsfw" <%= post.nsfw ? 'checked' : '' %>>
|
|---|
| 259 | <span>🔞 <%= t('pedit.nsfw_label') %></span>
|
|---|
| 260 | </label>
|
|---|
| 261 | <input type="text" name="content_warning" id="pe-cw" value="<%= post.content_warning || '' %>" maxlength="200"
|
|---|
| 262 | placeholder="<%= t('pedit.nsfw_cw_ph') %>"
|
|---|
| 263 | style="width:100%;box-sizing:border-box;margin:6px 0 2px;font-size:13px;padding:7px 9px">
|
|---|
| 264 | <% var _fediOpen = (typeof fediOpenAudio !== 'undefined' && fediOpenAudio); %>
|
|---|
| 265 | <label class="pe-checkbox">
|
|---|
| 266 | <%# One-way: once opened, the file has federated — re-gating would be false security, so the box locks. %>
|
|---|
| 267 | <input type="checkbox" name="fedi_open_audio" value="1" id="pe-fedi-audio" <%= _fediOpen ? 'checked disabled' : '' %>>
|
|---|
| 268 | <span>🌐 <%= t('pedit.fedi_audio_label') %></span>
|
|---|
| 269 | </label>
|
|---|
| 270 | <% if (_fediOpen) { %>
|
|---|
| 271 | <p style="font-size:12px;opacity:.7;margin:2px 0 0 26px"><%= t('pedit.fedi_audio_locked') %></p>
|
|---|
| 272 | <% } else { %>
|
|---|
| 273 | <p id="pe-fedi-audio-warn" style="font-size:12px;color:#c0392b;margin:2px 0 0 26px" hidden>⚠️ <%= t('pedit.fedi_audio_oneway') %></p>
|
|---|
| 274 | <%# De scripts van deze pagina staan in assets/js/mod/post-edit.js; de gegevens via partials/page-data.ejs (shaer-bqr). %>
|
|---|
| 275 | <% } %>
|
|---|
| 276 |
|
|---|
| 277 | <% // Poll (federates as an AS2 Question). Free feature. A poll with votes is frozen.
|
|---|
| 278 | var _poll = null; try { _poll = post.poll_json ? JSON.parse(post.poll_json) : null; } catch (e) { _poll = null; }
|
|---|
| 279 | var _pollLocked = (typeof pollLocked !== 'undefined' && pollLocked);
|
|---|
| 280 | var _pollOpts = (_poll && Array.isArray(_poll.options) && _poll.options.length) ? _poll.options : [{ name: '' }, { name: '' }]; %>
|
|---|
| 281 | <label class="pe-checkbox" style="margin-top:8px">
|
|---|
| 282 | <input type="checkbox" name="poll_enabled" value="1" id="pe-poll-toggle" <%= _poll ? 'checked' : '' %> <%= _pollLocked ? 'disabled' : '' %>>
|
|---|
| 283 | <span>📊 <%= t('pedit.poll_label') %></span>
|
|---|
| 284 | </label>
|
|---|
| 285 | <style>
|
|---|
| 286 | .pe-poll { margin-top: 6px; }
|
|---|
| 287 | .pe-poll-locked { font-size: 12px; opacity: .7; margin: 0 0 6px; }
|
|---|
| 288 | .pe-poll-row { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; }
|
|---|
| 289 | .pe-poll-opt { flex: 1; min-width: 0; box-sizing: border-box; font-size: 13px; padding: 7px 9px; border-radius: 7px; border: 1px solid var(--rule, rgba(128,128,128,.35)); background: transparent; color: inherit; }
|
|---|
| 290 | .pe-poll-opt:focus { outline: none; border-color: var(--accent); }
|
|---|
| 291 | .pe-poll-del { flex: 0 0 auto; width: 30px; height: 32px; display: inline-flex; align-items: center; justify-content: center; font-size: 18px; line-height: 1; border: 1px solid var(--rule, rgba(128,128,128,.35)); border-radius: 7px; background: transparent; color: var(--ink-muted, #999); cursor: pointer; transition: color .12s, border-color .12s; }
|
|---|
| 292 | .pe-poll-del:hover { border-color: #c0392b; color: #c0392b; }
|
|---|
| 293 | .pe-poll-del[hidden] { display: none; }
|
|---|
| 294 | .pe-poll-add { display: block; width: fit-content; font-size: 12.5px; padding: 6px 12px; border-radius: 7px; border: 1px dashed var(--rule, rgba(128,128,128,.45)); background: transparent; color: inherit; cursor: pointer; margin: 0 0 10px; transition: color .12s, border-color .12s; }
|
|---|
| 295 | .pe-poll-add:hover { border-color: var(--accent); color: var(--accent); }
|
|---|
| 296 | .pe-poll-add:disabled { opacity: .4; cursor: default; border-style: dashed; }
|
|---|
| 297 | .pe-poll-durlabel { display: block; font-size: 12.5px; opacity: .8; margin: 8px 0 4px; }
|
|---|
| 298 | .pe-poll-dur { padding: 7px 9px; border-radius: 7px; border: 1px solid var(--rule, rgba(128,128,128,.35)); background: transparent; color: inherit; font-size: 13px; }
|
|---|
| 299 | </style>
|
|---|
| 300 | <%# NB: keep the condition INSIDE the quoted attribute — escaped output (equals-tag)
|
|---|
| 301 | turns a whole emitted style attribute into entity soup (fields showed always). %>
|
|---|
| 302 | <div id="pe-poll-fields" class="pe-poll" style="<%= _poll ? '' : 'display:none' %>">
|
|---|
| 303 | <% if (_pollLocked) { %><p class="pe-poll-locked"><%= t('pedit.poll_locked') %></p><% } %>
|
|---|
| 304 | <div id="pe-poll-opts" data-ph="<%= t('pedit.poll_option_ph') %>" data-del="<%= t('pedit.poll_remove') %>">
|
|---|
| 305 | <% _pollOpts.forEach(function (o) { %>
|
|---|
| 306 | <div class="pe-poll-row">
|
|---|
| 307 | <input type="text" name="poll_option" class="pe-poll-opt" maxlength="100" value="<%= (o && o.name) || '' %>" placeholder="<%= t('pedit.poll_option_ph') %>" <%= _pollLocked ? 'disabled' : '' %>>
|
|---|
| 308 | <% if (!_pollLocked) { %><button type="button" class="pe-poll-del" aria-label="<%= t('pedit.poll_remove') %>" title="<%= t('pedit.poll_remove') %>">×</button><% } %>
|
|---|
| 309 | </div>
|
|---|
| 310 | <% }); %>
|
|---|
| 311 | </div>
|
|---|
| 312 | <% if (!_pollLocked) { %><button type="button" id="pe-poll-add" class="pe-poll-add">+ <%= t('pedit.poll_add') %></button><% } %>
|
|---|
| 313 | <label class="pe-checkbox">
|
|---|
| 314 | <input type="checkbox" name="poll_multiple" value="1" <%= (_poll && _poll.multiple) ? 'checked' : '' %> <%= _pollLocked ? 'disabled' : '' %>>
|
|---|
| 315 | <span><%= t('pedit.poll_multiple') %></span>
|
|---|
| 316 | </label>
|
|---|
| 317 | <label class="pe-poll-durlabel"><%= t('pedit.poll_duration') %></label>
|
|---|
| 318 | <select name="poll_duration" class="pe-poll-dur" <%= _pollLocked ? 'disabled' : '' %>>
|
|---|
| 319 | <% [['300','5m'],['1800','30m'],['3600','1h'],['21600','6h'],['43200','12h'],['86400','1d'],['259200','3d'],['604800','7d']].forEach(function (d) { %>
|
|---|
| 320 | <option value="<%= d[0] %>" <%= d[0] === '86400' ? 'selected' : '' %>><%= t('pedit.poll_dur_' + d[1]) %></option>
|
|---|
| 321 | <% }); %>
|
|---|
| 322 | </select>
|
|---|
| 323 | </div>
|
|---|
| 324 |
|
|---|
| 325 | <% if (typeof premiumUnlocked === 'undefined' || premiumUnlocked) { %>
|
|---|
| 326 | <label class="pe-checkbox">
|
|---|
| 327 | <input type="checkbox" name="fan_only" value="1" <%= post.fan_only ? 'checked' : '' %>>
|
|---|
| 328 | <span>🔒 <%= t('pedit.fan_only_label') %></span>
|
|---|
| 329 | </label>
|
|---|
| 330 | <label class="pe-checkbox" style="margin-top:8px">
|
|---|
| 331 | <input type="checkbox" name="paid" value="1" id="pe-paid" <%= post.paid ? 'checked' : '' %>>
|
|---|
| 332 | <span>💶 Betaalde post (supporters ontgrendelen met Patreon)</span>
|
|---|
| 333 | </label>
|
|---|
| 334 | <div id="pe-paid-price" style="margin:6px 0 0 26px;<%= post.paid ? '' : 'display:none' %>">
|
|---|
| 335 | <label style="font-size:.85rem;opacity:.8">Vereist steunbedrag (euro, leeg = standaard)
|
|---|
| 336 | <input type="text" name="paid_min_eur" inputmode="decimal" style="width:100px"
|
|---|
| 337 | value="<%= post.paid_min_cents ? (post.paid_min_cents/100).toFixed(2) : '' %>">
|
|---|
| 338 | </label>
|
|---|
| 339 | </div>
|
|---|
| 340 |
|
|---|
| 341 | <% var _scheduled = !!(post.publish_at && (post.status === 'scheduled' || Date.parse(String(post.publish_at).replace(' ', 'T')) > Date.now())); %>
|
|---|
| 342 | <label class="pe-checkbox" style="margin-top:8px">
|
|---|
| 343 | <input type="checkbox" name="schedule_enabled" value="1" id="pe-sched-toggle" <%= _scheduled ? 'checked' : '' %>>
|
|---|
| 344 | <span>📅 <%= t('pedit.schedule_label') %></span>
|
|---|
| 345 | </label>
|
|---|
| 346 | <div id="pe-sched-fields" style="margin-top:6px;<%= _scheduled ? '' : 'display:none' %>">
|
|---|
| 347 | <label style="display:block;font-size:12.5px;opacity:.8;margin-bottom:4px"><%= t('pedit.publish_at_label') %></label>
|
|---|
| 348 | <input type="datetime-local" id="pe-publish-at" name="publish_at" <%= _scheduled ? '' : 'disabled' %> data-iso="<%= post.publish_at || '' %>" value="" style="padding:8px 10px;border-radius:8px;border:1px solid var(--rule,rgba(128,128,128,.4));background:transparent;color:inherit">
|
|---|
| 349 | <% if (post.status === 'scheduled' && post.publish_at) { %><div style="font-size:12px;opacity:.7;margin-top:4px"><span id="pe-sched-when" data-iso="<%= post.publish_at %>" data-label="<%= t('pedit.scheduled_prefix') %>">⏳ <%= t('pedit.scheduled_for', { d: post.publish_at }) %></span></div><% } %>
|
|---|
| 350 | <div style="font-size:11.5px;opacity:.65;margin-top:4px"><%= t('pedit.schedule_hint') %></div>
|
|---|
| 351 | </div>
|
|---|
| 352 |
|
|---|
| 353 | <% } %>
|
|---|
| 354 | </div>
|
|---|
| 355 | </section>
|
|---|
| 356 |
|
|---|
| 357 | <%# ── ACTIONS (sticky at bottom) ──────────────────────────── %>
|
|---|
| 358 | <div class="pe-actions">
|
|---|
| 359 | <a href="<%= _base %><%= isNew ? '/' : '/' + post.slug %>" class="pe-btn"><%= t('pedit.cancel') %></a>
|
|---|
| 360 | <div class="pe-actions-spacer"></div>
|
|---|
| 361 | <% if (!isNew && post.status !== 'published') { %>
|
|---|
| 362 | <button type="submit" name="action" value="publish" class="pe-btn pe-btn-success">📤 <%= t('pedit.publish') %></button>
|
|---|
| 363 | <% } %>
|
|---|
| 364 | <button type="submit" class="pe-btn pe-btn-primary">💾 <%= t('pedit.save') %></button>
|
|---|
| 365 | </div>
|
|---|
| 366 | </form>
|
|---|
| 367 | </div>
|
|---|
| 368 |
|
|---|
| 369 | <style>
|
|---|
| 370 | /* ─── Page wrapper ──────────────────────────────────────────────── */
|
|---|
| 371 | .post-edit-page {
|
|---|
| 372 | max-width: 880px;
|
|---|
| 373 | margin: 1.5rem auto 6rem; /* extra bottom for sticky-actions clearance */
|
|---|
| 374 | padding: 0 1rem;
|
|---|
| 375 | }
|
|---|
| 376 | .post-edit-page h1 {
|
|---|
| 377 | font-family: var(--font-display, serif);
|
|---|
| 378 | margin: 0 0 1.25rem;
|
|---|
| 379 | font-size: 1.75rem;
|
|---|
| 380 | }
|
|---|
| 381 |
|
|---|
| 382 | .post-edit-form {
|
|---|
| 383 | display: flex; flex-direction: column;
|
|---|
| 384 | gap: 1rem;
|
|---|
| 385 | }
|
|---|
| 386 |
|
|---|
| 387 | /* ─── Card sections ─────────────────────────────────────────────── */
|
|---|
| 388 | .pe-card {
|
|---|
| 389 | background: var(--paper);
|
|---|
| 390 | border: 1px solid var(--rule);
|
|---|
| 391 | border-radius: 12px;
|
|---|
| 392 | padding: 1.25rem;
|
|---|
| 393 | display: flex; flex-direction: column;
|
|---|
| 394 | gap: 0.85rem;
|
|---|
| 395 | }
|
|---|
| 396 | .pe-card-content { padding-bottom: 0; overflow: hidden; } /* editor flush to bottom */
|
|---|
| 397 | .pe-section-title {
|
|---|
| 398 | font-size: 0.8rem;
|
|---|
| 399 | font-weight: 600;
|
|---|
| 400 | text-transform: uppercase;
|
|---|
| 401 | letter-spacing: 0.05em;
|
|---|
| 402 | color: var(--ink-soft);
|
|---|
| 403 | display: flex; align-items: baseline; gap: 0.5rem;
|
|---|
| 404 | flex-wrap: wrap;
|
|---|
| 405 | }
|
|---|
| 406 | .pe-section-title small {
|
|---|
| 407 | font-size: 0.75rem; font-weight: 400;
|
|---|
| 408 | letter-spacing: 0; text-transform: none;
|
|---|
| 409 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 410 | }
|
|---|
| 411 |
|
|---|
| 412 | /* ─── Field primitives ──────────────────────────────────────────── */
|
|---|
| 413 | .pe-field {
|
|---|
| 414 | display: flex; flex-direction: column;
|
|---|
| 415 | gap: 0.35rem;
|
|---|
| 416 | min-width: 0; /* allow grid items to shrink */
|
|---|
| 417 | }
|
|---|
| 418 | .pe-field > span {
|
|---|
| 419 | font-size: 0.8rem;
|
|---|
| 420 | font-weight: 600;
|
|---|
| 421 | color: var(--ink-soft);
|
|---|
| 422 | display: flex; align-items: baseline; gap: 0.4rem;
|
|---|
| 423 | }
|
|---|
| 424 | .pe-field > span small {
|
|---|
| 425 | font-weight: 400; color: var(--ink-muted);
|
|---|
| 426 | }
|
|---|
| 427 | .pe-field input,
|
|---|
| 428 | .pe-field textarea,
|
|---|
| 429 | .pe-field select {
|
|---|
| 430 | width: 100%;
|
|---|
| 431 | box-sizing: border-box;
|
|---|
| 432 | display: block;
|
|---|
| 433 | padding: 0.6rem 0.75rem;
|
|---|
| 434 | border: 1px solid var(--rule);
|
|---|
| 435 | border-radius: 6px;
|
|---|
| 436 | background: var(--paper-2);
|
|---|
| 437 | color: var(--ink);
|
|---|
| 438 | font-family: var(--font-ui, system-ui), sans-serif;
|
|---|
| 439 | font-size: 0.95rem;
|
|---|
| 440 | -webkit-appearance: none;
|
|---|
| 441 | appearance: none;
|
|---|
| 442 | transition: border-color 120ms;
|
|---|
| 443 | }
|
|---|
| 444 | .pe-field input:focus,
|
|---|
| 445 | .pe-field textarea:focus,
|
|---|
| 446 | .pe-field select:focus {
|
|---|
| 447 | outline: 2px solid var(--accent);
|
|---|
| 448 | outline-offset: -1px;
|
|---|
| 449 | border-color: var(--accent);
|
|---|
| 450 | }
|
|---|
| 451 | .pe-field textarea {
|
|---|
| 452 | font-family: var(--font-mono, monospace);
|
|---|
| 453 | resize: vertical;
|
|---|
| 454 | }
|
|---|
| 455 | .pe-field select {
|
|---|
| 456 | /* Restore the dropdown arrow we removed with appearance:none */
|
|---|
| 457 | background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'/></svg>");
|
|---|
| 458 | background-repeat: no-repeat;
|
|---|
| 459 | background-position: right 0.75rem center;
|
|---|
| 460 | padding-right: 2rem;
|
|---|
| 461 | }
|
|---|
| 462 |
|
|---|
| 463 | .pe-row {
|
|---|
| 464 | display: grid;
|
|---|
| 465 | gap: 0.75rem;
|
|---|
| 466 | }
|
|---|
| 467 | .pe-row-2 { grid-template-columns: 1fr 1fr; }
|
|---|
| 468 | @media (max-width: 600px) {
|
|---|
| 469 | .pe-row-2 { grid-template-columns: 1fr; }
|
|---|
| 470 | }
|
|---|
| 471 |
|
|---|
| 472 | /* ─── Cover field ───────────────────────────────────────────────── */
|
|---|
| 473 | .pe-cover-row {
|
|---|
| 474 | display: grid;
|
|---|
| 475 | grid-template-columns: 120px 1fr;
|
|---|
| 476 | gap: 1rem;
|
|---|
| 477 | align-items: start;
|
|---|
| 478 | }
|
|---|
| 479 | .pe-cover-thumb {
|
|---|
| 480 | width: 120px; height: 120px;
|
|---|
| 481 | border-radius: 10px;
|
|---|
| 482 | overflow: hidden;
|
|---|
| 483 | background: var(--paper-2);
|
|---|
| 484 | border: 1px solid var(--rule);
|
|---|
| 485 | display: flex; align-items: center; justify-content: center;
|
|---|
| 486 | position: relative;
|
|---|
| 487 | }
|
|---|
| 488 | .pe-cover-thumb[data-empty] {
|
|---|
| 489 | border-style: dashed;
|
|---|
| 490 | }
|
|---|
| 491 | .pe-cover-thumb img {
|
|---|
| 492 | width: 100%; height: 100%;
|
|---|
| 493 | object-fit: cover; display: block;
|
|---|
| 494 | }
|
|---|
| 495 | /* Honor HTML hidden — our display:block above otherwise renders an empty
|
|---|
| 496 | broken-image icon when no cover is set. */
|
|---|
| 497 | .pe-cover-thumb img[hidden] { display: none; }
|
|---|
| 498 | .pe-cover-empty {
|
|---|
| 499 | font-size: 2rem;
|
|---|
| 500 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 501 | opacity: 0.5;
|
|---|
| 502 | }
|
|---|
| 503 | .pe-cover-actions {
|
|---|
| 504 | display: flex; flex-direction: column;
|
|---|
| 505 | gap: 0.6rem;
|
|---|
| 506 | min-width: 0;
|
|---|
| 507 | }
|
|---|
| 508 | .pe-cover-upload {
|
|---|
| 509 | display: flex; align-items: center; gap: 0.6rem;
|
|---|
| 510 | flex-wrap: wrap;
|
|---|
| 511 | }
|
|---|
| 512 | @media (max-width: 600px) {
|
|---|
| 513 | .pe-cover-row { grid-template-columns: 88px 1fr; }
|
|---|
| 514 | .pe-cover-thumb { width: 88px; height: 88px; }
|
|---|
| 515 | }
|
|---|
| 516 |
|
|---|
| 517 | /* ─── Post type: segmented control + type-aware panels ──────────── */
|
|---|
| 518 | .pe-typeseg {
|
|---|
| 519 | display: grid;
|
|---|
| 520 | grid-template-columns: repeat(4, 1fr);
|
|---|
| 521 | gap: 0.4rem;
|
|---|
| 522 | }
|
|---|
| 523 | .pe-typeseg-btn {
|
|---|
| 524 | display: flex; flex-direction: column; align-items: center; gap: 0.25rem;
|
|---|
| 525 | padding: 0.7rem 0.4rem;
|
|---|
| 526 | border: 1px solid var(--rule);
|
|---|
| 527 | border-radius: 9px;
|
|---|
| 528 | background: var(--paper-2);
|
|---|
| 529 | color: var(--ink-soft);
|
|---|
| 530 | font-size: 0.85rem; font-weight: 600;
|
|---|
| 531 | cursor: pointer;
|
|---|
| 532 | transition: border-color 120ms, background 120ms, color 120ms;
|
|---|
| 533 | }
|
|---|
| 534 | .pe-typeseg-btn:hover { border-color: var(--accent); color: var(--ink); }
|
|---|
| 535 | .pe-typeseg-btn .pe-typeseg-ic { font-size: 1.3rem; line-height: 1; }
|
|---|
| 536 | .pe-typeseg-btn.is-active {
|
|---|
| 537 | border-color: var(--accent);
|
|---|
| 538 | background: color-mix(in srgb, var(--accent) 14%, var(--paper-2));
|
|---|
| 539 | color: var(--ink);
|
|---|
| 540 | }
|
|---|
| 541 | .pe-typeseg-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
|---|
| 542 | @media (max-width: 480px) {
|
|---|
| 543 | .pe-typeseg-btn { font-size: 0.78rem; padding: 0.6rem 0.25rem; }
|
|---|
| 544 | .pe-typeseg-btn .pe-typeseg-ic { font-size: 1.15rem; }
|
|---|
| 545 | }
|
|---|
| 546 |
|
|---|
| 547 | .pe-type-panel { margin-top: 0.2rem; }
|
|---|
| 548 |
|
|---|
| 549 | /* Audio drop/upload zone */
|
|---|
| 550 | .pe-audio-up {
|
|---|
| 551 | display: flex; flex-direction: column; align-items: center; gap: 0.3rem;
|
|---|
| 552 | text-align: center;
|
|---|
| 553 | padding: 1.4rem 1rem;
|
|---|
| 554 | border: 2px dashed var(--rule);
|
|---|
| 555 | border-radius: 10px;
|
|---|
| 556 | background: var(--paper-2);
|
|---|
| 557 | color: var(--ink-soft);
|
|---|
| 558 | cursor: pointer;
|
|---|
| 559 | transition: border-color 120ms, background 120ms;
|
|---|
| 560 | }
|
|---|
| 561 | .pe-audio-up:hover,
|
|---|
| 562 | .pe-audio-up:focus-visible { border-color: var(--accent); outline: none; }
|
|---|
| 563 | .pe-audio-up.is-drag {
|
|---|
| 564 | border-color: var(--accent);
|
|---|
| 565 | background: color-mix(in srgb, var(--accent) 12%, var(--paper-2));
|
|---|
| 566 | }
|
|---|
| 567 | .pe-audio-up .pe-audio-up-ic { font-size: 1.7rem; line-height: 1; }
|
|---|
| 568 | .pe-audio-up strong { color: var(--ink); font-size: 0.95rem; }
|
|---|
| 569 | .pe-audio-up small { font-size: 0.78rem; color: var(--ink-muted, var(--ink-soft)); max-width: 42ch; }
|
|---|
| 570 |
|
|---|
| 571 | .pe-audio-list { list-style: none; margin: 0.7rem 0 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; }
|
|---|
| 572 | .pe-audio-item {
|
|---|
| 573 | display: flex; align-items: center; justify-content: space-between; gap: 0.75rem;
|
|---|
| 574 | padding: 0.5rem 0.7rem;
|
|---|
| 575 | border: 1px solid var(--rule);
|
|---|
| 576 | border-radius: 7px;
|
|---|
| 577 | background: var(--paper-2);
|
|---|
| 578 | font-size: 0.85rem;
|
|---|
| 579 | }
|
|---|
| 580 | .pe-audio-item-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|---|
| 581 | .pe-audio-item-state { flex: none; font-size: 0.8rem; color: var(--ink-soft); }
|
|---|
| 582 | .pe-audio-item.is-done { border-color: color-mix(in srgb, var(--accent) 50%, var(--rule)); }
|
|---|
| 583 | .pe-audio-item.is-done .pe-audio-item-state { color: var(--accent); }
|
|---|
| 584 | .pe-audio-item.is-fail .pe-audio-item-state { color: #d9534f; }
|
|---|
| 585 |
|
|---|
| 586 | /* Video URL row */
|
|---|
| 587 | .pe-video-row { display: flex; gap: 0.5rem; }
|
|---|
| 588 | .pe-video-row input { flex: 1 1 auto; min-width: 0; }
|
|---|
| 589 | .pe-video-row input {
|
|---|
| 590 | box-sizing: border-box; padding: 0.6rem 0.75rem;
|
|---|
| 591 | border: 1px solid var(--rule); border-radius: 6px;
|
|---|
| 592 | background: var(--paper-2); color: var(--ink); font-size: 0.95rem;
|
|---|
| 593 | }
|
|---|
| 594 | .pe-video-row input:focus { outline: 2px solid var(--accent); outline-offset: -1px; border-color: var(--accent); }
|
|---|
| 595 | .pe-video-row .pe-btn { flex: none; }
|
|---|
| 596 |
|
|---|
| 597 | .pe-foto-hint { margin: 0; font-size: 0.88rem; color: var(--ink-soft); }
|
|---|
| 598 |
|
|---|
| 599 | /* ─── Editor frame (WYSIWYG: top toolbar, contenteditable body, status bar) ─── */
|
|---|
| 600 | .pe-editor-frame {
|
|---|
| 601 | margin: 0 -1.25rem -1.25rem; /* break out of card padding for flush edges */
|
|---|
| 602 | border-top: 1px solid var(--rule);
|
|---|
| 603 | background: var(--paper);
|
|---|
| 604 | display: flex; flex-direction: column;
|
|---|
| 605 | }
|
|---|
| 606 |
|
|---|
| 607 | /* Top toolbar — sticks to the top of the viewport while editing */
|
|---|
| 608 | .pe-toolbar {
|
|---|
| 609 | display: flex; align-items: center; gap: .15rem;
|
|---|
| 610 | flex-wrap: wrap;
|
|---|
| 611 | padding: .4rem .75rem;
|
|---|
| 612 | background: color-mix(in srgb, var(--paper) 92%, transparent);
|
|---|
| 613 | backdrop-filter: blur(10px);
|
|---|
| 614 | -webkit-backdrop-filter: blur(10px);
|
|---|
| 615 | border-bottom: 1px solid var(--rule);
|
|---|
| 616 | position: sticky;
|
|---|
| 617 | top: 0;
|
|---|
| 618 | z-index: 10;
|
|---|
| 619 | }
|
|---|
| 620 | .pe-toolbar button {
|
|---|
| 621 | display: inline-flex; align-items: center; justify-content: center;
|
|---|
| 622 | width: 32px; height: 32px;
|
|---|
| 623 | padding: 0;
|
|---|
| 624 | touch-action: manipulation; /* snappy taps on mobile, no double-tap zoom */
|
|---|
| 625 | -webkit-user-select: none; user-select: none;
|
|---|
| 626 | background: transparent;
|
|---|
| 627 | border: 1px solid transparent;
|
|---|
| 628 | border-radius: 6px;
|
|---|
| 629 | color: var(--ink);
|
|---|
| 630 | cursor: pointer;
|
|---|
| 631 | font-family: var(--font-ui, system-ui), sans-serif;
|
|---|
| 632 | font-weight: 600; font-size: .85rem;
|
|---|
| 633 | transition: background var(--transition), color var(--transition), border-color var(--transition);
|
|---|
| 634 | -webkit-tap-highlight-color: transparent;
|
|---|
| 635 | }
|
|---|
| 636 | /* Hover only on real hover-capable devices — on touch :hover otherwise "sticks"
|
|---|
| 637 | after a tap, making the active/inactive state unreadable. */
|
|---|
| 638 | @media (hover: hover) {
|
|---|
| 639 | .pe-toolbar button:hover {
|
|---|
| 640 | background: var(--paper-2);
|
|---|
| 641 | color: var(--accent);
|
|---|
| 642 | }
|
|---|
| 643 | }
|
|---|
| 644 | .pe-toolbar button:active { transform: scale(.94); }
|
|---|
| 645 | /* Active formatting = unmistakably filled with the accent colour. On mobile
|
|---|
| 646 | it's immediately clear when e.g. bold is ON (tap again = off). */
|
|---|
| 647 | .pe-toolbar button.is-active {
|
|---|
| 648 | background: var(--accent);
|
|---|
| 649 | color: #fff;
|
|---|
| 650 | border-color: var(--accent);
|
|---|
| 651 | }
|
|---|
| 652 | .pe-toolbar button.is-active svg { color: #fff; }
|
|---|
| 653 | .pe-toolbar button svg { width: 16px; height: 16px; }
|
|---|
| 654 | .pe-toolbar button.pe-tb-text { font-family: var(--font-display, serif); font-weight: 700; }
|
|---|
| 655 | .pe-toolbar-sep {
|
|---|
| 656 | width: 1px; height: 18px;
|
|---|
| 657 | background: var(--rule);
|
|---|
| 658 | margin: 0 .35rem;
|
|---|
| 659 | flex-shrink: 0;
|
|---|
| 660 | }
|
|---|
| 661 | .pe-toolbar-spacer { flex: 1; }
|
|---|
| 662 |
|
|---|
| 663 | /* ─── Full-screen writing mode ─── */
|
|---|
| 664 | .fs-icon-compress { display: none; }
|
|---|
| 665 | .pe-editor-frame.pe-fs .fs-icon-expand { display: none; }
|
|---|
| 666 | .pe-editor-frame.pe-fs .fs-icon-compress { display: inline; }
|
|---|
| 667 | .pe-editor-frame.pe-fs {
|
|---|
| 668 | position: fixed; inset: 0; z-index: 1000;
|
|---|
| 669 | margin: 0; border-top: 0;
|
|---|
| 670 | height: 100dvh;
|
|---|
| 671 | background: var(--paper);
|
|---|
| 672 | overflow: hidden; /* only the text field scrolls, not the frame itself */
|
|---|
| 673 | }
|
|---|
| 674 | /* In fullscreen the writing field fills the remaining space and scrolls itself
|
|---|
| 675 | (max-height: none overrides the mobile box limit below). */
|
|---|
| 676 | .pe-editor-frame.pe-fs .pe-editor {
|
|---|
| 677 | flex: 1 1 auto; min-height: 0; height: auto; max-height: none;
|
|---|
| 678 | }
|
|---|
| 679 | /* iOS: in fullscreen the toolbar sits at the very top → push it below the
|
|---|
| 680 | camera / Dynamic Island with the top safe-area inset. */
|
|---|
| 681 | .pe-editor-frame.pe-fs .pe-toolbar { padding-top: calc(.4rem + env(safe-area-inset-top, 0px)); }
|
|---|
| 682 | /* NB: deliberately NO overflow:hidden on html/body in fullscreen — that could get
|
|---|
| 683 | stuck (e.g. leaving fullscreen via navigation) and would block scrolling on the
|
|---|
| 684 | whole page. The fullscreen frame (position:fixed, inset:0) already covers the page,
|
|---|
| 685 | and on touch scrollbars are hidden → no double bar needed. */
|
|---|
| 686 |
|
|---|
| 687 | /* "Done" button: only visible in fullscreen (left side of toolbar), clear
|
|---|
| 688 | accent-pill instead of a square icon. */
|
|---|
| 689 | /* Hide rule more specific than ".pe-toolbar button" (otherwise that wins and the
|
|---|
| 690 | Done button also shows inline as a small button). Only shown in fullscreen → large. */
|
|---|
| 691 | .pe-toolbar button.pe-fs-done { display: none; }
|
|---|
| 692 | .pe-editor-frame.pe-fs .pe-fs-done {
|
|---|
| 693 | display: inline-flex; align-items: center; gap: .35rem;
|
|---|
| 694 | width: auto !important; height: auto !important; min-height: 40px;
|
|---|
| 695 | padding: .55rem 1.3rem !important; margin-right: .5rem;
|
|---|
| 696 | background: var(--accent); color: #fff;
|
|---|
| 697 | font-weight: 700; font-size: 1.05rem; border-radius: 999px;
|
|---|
| 698 | }
|
|---|
| 699 | .pe-editor-frame.pe-fs .pe-fs-done:hover { background: var(--accent); color: #fff; }
|
|---|
| 700 |
|
|---|
| 701 | /* Editor body — looks like prose, behaves like a textarea */
|
|---|
| 702 | .pe-editor {
|
|---|
| 703 | width: 100%;
|
|---|
| 704 | box-sizing: border-box;
|
|---|
| 705 | min-height: 420px;
|
|---|
| 706 | padding: 1.25rem 1.5rem;
|
|---|
| 707 | border: 0;
|
|---|
| 708 | background: transparent;
|
|---|
| 709 | color: var(--ink);
|
|---|
| 710 | font-family: var(--font-body, system-ui), serif;
|
|---|
| 711 | font-size: 1.0625rem;
|
|---|
| 712 | line-height: 1.65;
|
|---|
| 713 | outline: none;
|
|---|
| 714 | overflow-y: auto;
|
|---|
| 715 | }
|
|---|
| 716 | .pe-editor:focus { outline: none; }
|
|---|
| 717 | /* Inline (non-fullscreen) the writing field simply grows with the content — no
|
|---|
| 718 | internal scroll-box (scroll-within-scroll is confusing). On mobile/tablet typing
|
|---|
| 719 | always goes fullscreen (see JS), where the field fills the page and is the sole
|
|---|
| 720 | scroller. */
|
|---|
| 721 | .pe-editor-frame:not(.pe-fs) .pe-editor { overflow: visible; }
|
|---|
| 722 |
|
|---|
| 723 | /* Touch: the inline content field is not a text field but a tap target → fullscreen
|
|---|
| 724 | editing. A "✎ Tap to edit" pill sticks to the bottom of the container,
|
|---|
| 725 | so the hint is always visible without sitting in the middle of the text. */
|
|---|
| 726 | .pe-editor.pe-tap-to-edit { cursor: pointer; }
|
|---|
| 727 | .pe-edit-hint { display: none; }
|
|---|
| 728 | @media (pointer: coarse) {
|
|---|
| 729 | .pe-editor-frame:not(.pe-fs) .pe-edit-hint {
|
|---|
| 730 | display: block;
|
|---|
| 731 | position: sticky;
|
|---|
| 732 | bottom: 4.5rem; /* above the sticky Save/Cancel bar */
|
|---|
| 733 | width: max-content;
|
|---|
| 734 | max-width: calc(100% - 2rem);
|
|---|
| 735 | margin: .4rem auto;
|
|---|
| 736 | background: var(--accent); color: #fff;
|
|---|
| 737 | font-size: .9rem; font-weight: 700; padding: .5rem 1.1rem; border-radius: 999px;
|
|---|
| 738 | box-shadow: 0 4px 14px rgba(0,0,0,.35);
|
|---|
| 739 | pointer-events: none; text-align: center; white-space: nowrap;
|
|---|
| 740 | }
|
|---|
| 741 | /* Not relevant on touch (drag/select belongs to inline editing on desktop). */
|
|---|
| 742 | .pe-content-hint { display: none; }
|
|---|
| 743 | /* Inline (non-fullscreen) on touch: keep it simple — you don't edit here anyway,
|
|---|
| 744 | so no formatting toolbar and no border. Just the content preview + the
|
|---|
| 745 | "tap to edit" pill. The toolbar only appears in fullscreen. */
|
|---|
| 746 | .pe-editor-frame:not(.pe-fs) .pe-toolbar { display: none; }
|
|---|
| 747 | .pe-editor-frame:not(.pe-fs) { border-top: 0; }
|
|---|
| 748 | .pe-editor-frame:not(.pe-fs) .pe-editor { min-height: 8rem; }
|
|---|
| 749 | }
|
|---|
| 750 | .pe-editor.is-dragover {
|
|---|
| 751 | outline: 2px dashed var(--accent);
|
|---|
| 752 | outline-offset: -10px;
|
|---|
| 753 | }
|
|---|
| 754 | /* Inline prose styles inside the editor — match the public post styling so
|
|---|
| 755 | what you see is roughly what you'll get. Margins are tighter than on the
|
|---|
| 756 | live site since this is a confined writing space. */
|
|---|
| 757 | .pe-editor h1, .pe-editor h2, .pe-editor h3, .pe-editor h4 {
|
|---|
| 758 | font-family: var(--font-display, serif);
|
|---|
| 759 | line-height: 1.2;
|
|---|
| 760 | margin: 1.1rem 0 .35rem;
|
|---|
| 761 | }
|
|---|
| 762 | .pe-editor h1 { font-size: 1.85rem; }
|
|---|
| 763 | .pe-editor h2 { font-size: 1.45rem; }
|
|---|
| 764 | .pe-editor h3 { font-size: 1.2rem; }
|
|---|
| 765 | .pe-editor p { margin: 0 0 .85em; }
|
|---|
| 766 | .pe-editor ul, .pe-editor ol { margin: 0 0 .85em 1.4em; padding: 0; }
|
|---|
| 767 | .pe-editor li { margin: .15em 0; }
|
|---|
| 768 | .pe-editor blockquote {
|
|---|
| 769 | margin: 1em 0;
|
|---|
| 770 | padding: .15em 0 .15em 1em;
|
|---|
| 771 | border-left: 3px solid var(--accent);
|
|---|
| 772 | color: var(--ink-soft, var(--ink));
|
|---|
| 773 | font-style: italic;
|
|---|
| 774 | }
|
|---|
| 775 | .pe-editor code {
|
|---|
| 776 | background: var(--paper-2);
|
|---|
| 777 | border: 1px solid var(--rule);
|
|---|
| 778 | border-radius: 4px;
|
|---|
| 779 | padding: .1em .35em;
|
|---|
| 780 | font-family: var(--font-mono, ui-monospace, monospace);
|
|---|
| 781 | font-size: .92em;
|
|---|
| 782 | }
|
|---|
| 783 | .pe-editor a { color: var(--accent); text-decoration: underline; text-underline-offset: 3px; }
|
|---|
| 784 | .pe-editor img {
|
|---|
| 785 | max-width: 100%; height: auto;
|
|---|
| 786 | border-radius: 6px;
|
|---|
| 787 | display: block;
|
|---|
| 788 | margin: 1em auto;
|
|---|
| 789 | }
|
|---|
| 790 |
|
|---|
| 791 | /* Shortcode chips — visible inline placeholder in the editor for
|
|---|
| 792 | [[track:UUID]] / [[album:Name]] / [[playlist:slug]]. They serialize back
|
|---|
| 793 | to plain shortcode text on submit. */
|
|---|
| 794 | .sc-chip {
|
|---|
| 795 | display: inline-flex; align-items: center; gap: .3rem;
|
|---|
| 796 | padding: .15em .5em;
|
|---|
| 797 | margin: 0 .15em;
|
|---|
| 798 | background: color-mix(in srgb, var(--accent) 10%, var(--paper-2));
|
|---|
| 799 | border: 1px solid color-mix(in srgb, var(--accent) 35%, var(--rule));
|
|---|
| 800 | border-radius: 999px;
|
|---|
| 801 | color: var(--accent);
|
|---|
| 802 | font-family: var(--font-ui, system-ui), sans-serif;
|
|---|
| 803 | font-size: .85em;
|
|---|
| 804 | font-weight: 600;
|
|---|
| 805 | font-style: normal;
|
|---|
| 806 | white-space: nowrap;
|
|---|
| 807 | user-select: none;
|
|---|
| 808 | cursor: default;
|
|---|
| 809 | vertical-align: baseline;
|
|---|
| 810 | }
|
|---|
| 811 | .sc-chip-icon { display: inline-flex; opacity: .8; }
|
|---|
| 812 | .sc-chip-icon svg { width: 12px; height: 12px; }
|
|---|
| 813 |
|
|---|
| 814 | /* Status bar below the editor */
|
|---|
| 815 | .pe-editor-status {
|
|---|
| 816 | display: flex; align-items: center; justify-content: space-between;
|
|---|
| 817 | gap: .75rem;
|
|---|
| 818 | padding: .45rem 1rem;
|
|---|
| 819 | border-top: 1px solid var(--rule);
|
|---|
| 820 | background: var(--paper-2);
|
|---|
| 821 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 822 | font-size: .78rem;
|
|---|
| 823 | }
|
|---|
| 824 | .pe-char-count { font-variant-numeric: tabular-nums; }
|
|---|
| 825 |
|
|---|
| 826 | /* ─── Track picker modal (P59) ─────────────────────────────────
|
|---|
| 827 | Mobile-first: full-screen bottom-sheet on phones, centered card
|
|---|
| 828 | on desktop. Lock body scroll while open via .tp-locked on body. */
|
|---|
| 829 | .tp-modal {
|
|---|
| 830 | position: fixed; inset: 0;
|
|---|
| 831 | z-index: 1000;
|
|---|
| 832 | display: flex; align-items: stretch; justify-content: center;
|
|---|
| 833 | /* Avoid the iOS home-indicator + the keyboard when search is focused */
|
|---|
| 834 | padding: env(safe-area-inset-top) 0 env(safe-area-inset-bottom);
|
|---|
| 835 | }
|
|---|
| 836 | .tp-modal[hidden] { display: none; }
|
|---|
| 837 | .tp-backdrop {
|
|---|
| 838 | position: absolute; inset: 0;
|
|---|
| 839 | background: color-mix(in srgb, var(--ink) 55%, transparent);
|
|---|
| 840 | backdrop-filter: blur(4px);
|
|---|
| 841 | -webkit-backdrop-filter: blur(4px);
|
|---|
| 842 | animation: tp-bd-in .18s ease-out;
|
|---|
| 843 | }
|
|---|
| 844 | @keyframes tp-bd-in { from { opacity: 0 } to { opacity: 1 } }
|
|---|
| 845 |
|
|---|
| 846 | /* Mobile: sheet docks to the bottom and fills viewport almost completely */
|
|---|
| 847 | .tp-sheet {
|
|---|
| 848 | position: relative;
|
|---|
| 849 | margin-top: auto;
|
|---|
| 850 | width: 100%;
|
|---|
| 851 | max-height: calc(100dvh - env(safe-area-inset-top) - 1rem);
|
|---|
| 852 | background: var(--paper);
|
|---|
| 853 | border-top: 1px solid var(--rule);
|
|---|
| 854 | border-radius: 16px 16px 0 0;
|
|---|
| 855 | box-shadow:
|
|---|
| 856 | 0 -2px 8px color-mix(in srgb, var(--ink) 8%, transparent),
|
|---|
| 857 | 0 -16px 48px color-mix(in srgb, var(--ink) 18%, transparent);
|
|---|
| 858 | display: flex; flex-direction: column;
|
|---|
| 859 | animation: tp-sheet-up .22s cubic-bezier(.2, .8, .25, 1);
|
|---|
| 860 | overflow: hidden;
|
|---|
| 861 | }
|
|---|
| 862 | @keyframes tp-sheet-up {
|
|---|
| 863 | from { transform: translateY(100%); opacity: .8; }
|
|---|
| 864 | to { transform: translateY(0); opacity: 1; }
|
|---|
| 865 | }
|
|---|
| 866 |
|
|---|
| 867 | /* Header — title + close, with a small grab-handle bar on mobile */
|
|---|
| 868 | .tp-header {
|
|---|
| 869 | display: flex; align-items: center; justify-content: space-between;
|
|---|
| 870 | gap: .75rem;
|
|---|
| 871 | padding: .85rem 1rem .65rem;
|
|---|
| 872 | border-bottom: 1px solid var(--rule);
|
|---|
| 873 | position: relative;
|
|---|
| 874 | }
|
|---|
| 875 | .tp-header::before {
|
|---|
| 876 | /* Grab-handle: visible on mobile only */
|
|---|
| 877 | content: '';
|
|---|
| 878 | position: absolute;
|
|---|
| 879 | top: .35rem; left: 50%;
|
|---|
| 880 | transform: translateX(-50%);
|
|---|
| 881 | width: 38px; height: 4px;
|
|---|
| 882 | background: var(--rule-2, var(--rule));
|
|---|
| 883 | border-radius: 2px;
|
|---|
| 884 | }
|
|---|
| 885 | .tp-h2 {
|
|---|
| 886 | margin: .35rem 0 0;
|
|---|
| 887 | font-family: var(--font-display, serif);
|
|---|
| 888 | font-size: 1.15rem; font-weight: 600;
|
|---|
| 889 | color: var(--ink);
|
|---|
| 890 | line-height: 1.1;
|
|---|
| 891 | }
|
|---|
| 892 | .tp-close {
|
|---|
| 893 | width: 32px; height: 32px; border-radius: 8px;
|
|---|
| 894 | display: inline-flex; align-items: center; justify-content: center;
|
|---|
| 895 | background: transparent; border: 1px solid transparent;
|
|---|
| 896 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 897 | cursor: pointer;
|
|---|
| 898 | transition: background var(--transition), color var(--transition), border-color var(--transition);
|
|---|
| 899 | -webkit-tap-highlight-color: transparent;
|
|---|
| 900 | margin-top: .35rem;
|
|---|
| 901 | }
|
|---|
| 902 | .tp-close:hover, .tp-close:focus-visible {
|
|---|
| 903 | background: var(--paper-2);
|
|---|
| 904 | color: var(--ink);
|
|---|
| 905 | outline: none;
|
|---|
| 906 | }
|
|---|
| 907 | .tp-close svg { width: 18px; height: 18px; }
|
|---|
| 908 |
|
|---|
| 909 | /* Search row — sticky just below header so list scrolls underneath */
|
|---|
| 910 | .tp-search-row {
|
|---|
| 911 | position: relative;
|
|---|
| 912 | padding: .65rem 1rem;
|
|---|
| 913 | border-bottom: 1px solid var(--rule);
|
|---|
| 914 | background: var(--paper);
|
|---|
| 915 | }
|
|---|
| 916 | .tp-search-icon {
|
|---|
| 917 | position: absolute;
|
|---|
| 918 | top: 50%; left: 1.65rem;
|
|---|
| 919 | transform: translateY(-50%);
|
|---|
| 920 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 921 | pointer-events: none;
|
|---|
| 922 | display: inline-flex;
|
|---|
| 923 | }
|
|---|
| 924 | .tp-search-icon svg { width: 16px; height: 16px; }
|
|---|
| 925 | .tp-search {
|
|---|
| 926 | width: 100%; box-sizing: border-box;
|
|---|
| 927 | padding: .65rem .85rem .65rem 2.4rem;
|
|---|
| 928 | font-family: var(--font-ui, system-ui), sans-serif;
|
|---|
| 929 | font-size: 1rem;
|
|---|
| 930 | background: var(--paper-2);
|
|---|
| 931 | color: var(--ink);
|
|---|
| 932 | border: 1px solid var(--rule);
|
|---|
| 933 | border-radius: 9px;
|
|---|
| 934 | -webkit-appearance: none;
|
|---|
| 935 | appearance: none;
|
|---|
| 936 | }
|
|---|
| 937 | .tp-search:focus {
|
|---|
| 938 | outline: none;
|
|---|
| 939 | border-color: var(--accent);
|
|---|
| 940 | box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
|
|---|
| 941 | }
|
|---|
| 942 |
|
|---|
| 943 | /* List — vertically scrollable, takes remaining sheet height */
|
|---|
| 944 | .tp-list {
|
|---|
| 945 | flex: 1 1 auto;
|
|---|
| 946 | overflow-y: auto;
|
|---|
| 947 | -webkit-overflow-scrolling: touch;
|
|---|
| 948 | padding: .35rem .35rem 1rem;
|
|---|
| 949 | }
|
|---|
| 950 | .tp-empty {
|
|---|
| 951 | padding: 1.5rem 1rem;
|
|---|
| 952 | text-align: center;
|
|---|
| 953 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 954 | font-size: .9rem;
|
|---|
| 955 | }
|
|---|
| 956 |
|
|---|
| 957 | /* Track row — tap target ≥ 56px for mobile */
|
|---|
| 958 | .tp-row {
|
|---|
| 959 | display: grid;
|
|---|
| 960 | grid-template-columns: 44px 1fr auto;
|
|---|
| 961 | align-items: center;
|
|---|
| 962 | gap: .75rem;
|
|---|
| 963 | width: 100%;
|
|---|
| 964 | padding: .55rem .65rem;
|
|---|
| 965 | border: 0; background: transparent;
|
|---|
| 966 | border-radius: 10px;
|
|---|
| 967 | text-align: left;
|
|---|
| 968 | cursor: pointer;
|
|---|
| 969 | color: inherit;
|
|---|
| 970 | font: inherit;
|
|---|
| 971 | transition: background var(--transition);
|
|---|
| 972 | -webkit-tap-highlight-color: transparent;
|
|---|
| 973 | }
|
|---|
| 974 | .tp-row:hover, .tp-row:focus-visible {
|
|---|
| 975 | background: color-mix(in srgb, var(--accent) 8%, var(--paper-2));
|
|---|
| 976 | outline: none;
|
|---|
| 977 | }
|
|---|
| 978 | .tp-row:active { transform: scale(.98); }
|
|---|
| 979 | .tp-row[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
|
|---|
| 980 |
|
|---|
| 981 | .tp-cover {
|
|---|
| 982 | width: 44px; height: 44px;
|
|---|
| 983 | border-radius: 6px;
|
|---|
| 984 | background-size: cover; background-position: center;
|
|---|
| 985 | background-color: var(--rule);
|
|---|
| 986 | display: inline-flex; align-items: center; justify-content: center;
|
|---|
| 987 | flex-shrink: 0;
|
|---|
| 988 | }
|
|---|
| 989 | .tp-cover-empty {
|
|---|
| 990 | background: linear-gradient(135deg,
|
|---|
| 991 | color-mix(in srgb, var(--accent) 30%, var(--paper-2)),
|
|---|
| 992 | color-mix(in srgb, var(--accent) 8%, var(--paper-2)));
|
|---|
| 993 | color: var(--accent);
|
|---|
| 994 | }
|
|---|
| 995 | .tp-cover-empty svg { width: 18px; height: 18px; opacity: .8; }
|
|---|
| 996 |
|
|---|
| 997 | .tp-meta {
|
|---|
| 998 | display: flex; flex-direction: column;
|
|---|
| 999 | min-width: 0;
|
|---|
| 1000 | line-height: 1.25;
|
|---|
| 1001 | }
|
|---|
| 1002 | .tp-row-title {
|
|---|
| 1003 | font-weight: 600; font-size: .94rem;
|
|---|
| 1004 | color: var(--ink);
|
|---|
| 1005 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|---|
| 1006 | }
|
|---|
| 1007 | .tp-row-artist {
|
|---|
| 1008 | font-size: .8rem;
|
|---|
| 1009 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 1010 | white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|---|
| 1011 | margin-top: .1rem;
|
|---|
| 1012 | }
|
|---|
| 1013 | .tp-duration {
|
|---|
| 1014 | font-variant-numeric: tabular-nums;
|
|---|
| 1015 | font-size: .8rem;
|
|---|
| 1016 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 1017 | flex-shrink: 0;
|
|---|
| 1018 | }
|
|---|
| 1019 |
|
|---|
| 1020 | /* Body lock when modal open */
|
|---|
| 1021 | body.tp-locked { overflow: hidden; touch-action: none; }
|
|---|
| 1022 |
|
|---|
| 1023 | /* Desktop: centered card, max-width ~520px */
|
|---|
| 1024 | @media (min-width: 640px) {
|
|---|
| 1025 | .tp-modal {
|
|---|
| 1026 | align-items: center;
|
|---|
| 1027 | padding: 1.5rem;
|
|---|
| 1028 | }
|
|---|
| 1029 | .tp-sheet {
|
|---|
| 1030 | margin-top: 0;
|
|---|
| 1031 | width: 100%;
|
|---|
| 1032 | max-width: 520px;
|
|---|
| 1033 | max-height: 80dvh;
|
|---|
| 1034 | border-radius: 14px;
|
|---|
| 1035 | border: 1px solid var(--rule);
|
|---|
| 1036 | animation: tp-sheet-pop .2s cubic-bezier(.2, .8, .25, 1);
|
|---|
| 1037 | }
|
|---|
| 1038 | .tp-header::before { display: none; } /* hide grab-handle on desktop */
|
|---|
| 1039 | .tp-header { padding: 1rem 1.25rem .75rem; }
|
|---|
| 1040 | .tp-h2 { margin-top: 0; font-size: 1.25rem; }
|
|---|
| 1041 | .tp-close { margin-top: 0; }
|
|---|
| 1042 | @keyframes tp-sheet-pop {
|
|---|
| 1043 | from { transform: translateY(-8px) scale(.97); opacity: 0; }
|
|---|
| 1044 | to { transform: translateY(0) scale(1); opacity: 1; }
|
|---|
| 1045 | }
|
|---|
| 1046 | }
|
|---|
| 1047 |
|
|---|
| 1048 | /* ─── Buttons ───────────────────────────────────────────────────── */
|
|---|
| 1049 | .pe-btn {
|
|---|
| 1050 | display: inline-flex; align-items: center; gap: 0.4rem;
|
|---|
| 1051 | padding: 0.55rem 1rem;
|
|---|
| 1052 | border: 1px solid var(--rule);
|
|---|
| 1053 | background: var(--paper-2);
|
|---|
| 1054 | color: var(--ink);
|
|---|
| 1055 | font-family: var(--font-ui, system-ui), sans-serif;
|
|---|
| 1056 | font-size: 0.9rem; font-weight: 500;
|
|---|
| 1057 | text-decoration: none;
|
|---|
| 1058 | border-radius: 6px;
|
|---|
| 1059 | cursor: pointer;
|
|---|
| 1060 | transition: background 120ms, border-color 120ms;
|
|---|
| 1061 | min-height: 40px;
|
|---|
| 1062 | -webkit-tap-highlight-color: transparent;
|
|---|
| 1063 | }
|
|---|
| 1064 | .pe-btn:hover { border-color: var(--accent); }
|
|---|
| 1065 | .pe-btn:active { transform: scale(0.97); }
|
|---|
| 1066 | .pe-btn-tiny {
|
|---|
| 1067 | padding: 0.35rem 0.7rem;
|
|---|
| 1068 | font-size: 0.85rem;
|
|---|
| 1069 | min-height: 32px;
|
|---|
| 1070 | }
|
|---|
| 1071 | .pe-btn-secondary { background: var(--paper-2); }
|
|---|
| 1072 | .pe-btn-primary {
|
|---|
| 1073 | background: var(--accent); color: white;
|
|---|
| 1074 | border-color: var(--accent);
|
|---|
| 1075 | }
|
|---|
| 1076 | .pe-btn-primary:hover { opacity: 0.92; border-color: var(--accent); }
|
|---|
| 1077 | .pe-btn-success {
|
|---|
| 1078 | background: #16a34a; color: white;
|
|---|
| 1079 | border-color: #16a34a;
|
|---|
| 1080 | }
|
|---|
| 1081 | .pe-btn-success:hover { opacity: 0.92; border-color: #16a34a; }
|
|---|
| 1082 |
|
|---|
| 1083 | .pe-status {
|
|---|
| 1084 | color: var(--ink-muted, var(--ink-soft));
|
|---|
| 1085 | font-size: 0.8rem;
|
|---|
| 1086 | }
|
|---|
| 1087 | .pe-status.is-error { color: #dc2626; }
|
|---|
| 1088 |
|
|---|
| 1089 | /* ─── Checkboxes (clean inline row) ─────────────────────────────── */
|
|---|
| 1090 | .pe-checkboxes {
|
|---|
| 1091 | display: flex; flex-direction: column;
|
|---|
| 1092 | gap: 0.5rem;
|
|---|
| 1093 | padding-top: 0.25rem;
|
|---|
| 1094 | }
|
|---|
| 1095 | .pe-checkbox {
|
|---|
| 1096 | display: inline-flex; align-items: center; gap: 0.5rem;
|
|---|
| 1097 | cursor: pointer;
|
|---|
| 1098 | font-size: 0.95rem;
|
|---|
| 1099 | color: var(--ink);
|
|---|
| 1100 | user-select: none;
|
|---|
| 1101 | }
|
|---|
| 1102 | .pe-checkbox input[type="checkbox"] {
|
|---|
| 1103 | width: 18px; height: 18px;
|
|---|
| 1104 | margin: 0;
|
|---|
| 1105 | cursor: pointer;
|
|---|
| 1106 | accent-color: var(--accent);
|
|---|
| 1107 | }
|
|---|
| 1108 |
|
|---|
| 1109 | /* Pin: checkbox + ▲▼-stepper with description (instead of a raw rank number). */
|
|---|
| 1110 | .pe-pin { display: flex; flex-direction: column; gap: 0.45rem; }
|
|---|
| 1111 | .pe-pin-pos { display: flex; align-items: center; gap: 0.55rem; padding-left: 1.65rem; }
|
|---|
| 1112 | .pe-pin-pos[hidden] { display: none; }
|
|---|
| 1113 | .pe-pin-steps { display: inline-flex; border: 1px solid var(--rule); border-radius: 6px; overflow: hidden; }
|
|---|
| 1114 | .pe-pin-btn {
|
|---|
| 1115 | width: 30px; height: 28px; padding: 0;
|
|---|
| 1116 | border: none; background: var(--paper-2); color: var(--ink);
|
|---|
| 1117 | cursor: pointer; font-size: 0.68rem; line-height: 1;
|
|---|
| 1118 | display: inline-flex; align-items: center; justify-content: center;
|
|---|
| 1119 | }
|
|---|
| 1120 | .pe-pin-btn + .pe-pin-btn { border-left: 1px solid var(--rule); }
|
|---|
| 1121 | .pe-pin-btn:hover { background: color-mix(in srgb, var(--accent) 14%, var(--paper-2)); color: var(--accent); }
|
|---|
| 1122 | .pe-pin-btn:disabled { opacity: 0.35; cursor: default; }
|
|---|
| 1123 | .pe-pin-label { font-size: 0.85rem; color: var(--accent); font-weight: 600; }
|
|---|
| 1124 |
|
|---|
| 1125 | /* ─── Sticky action footer ──────────────────────────────────────── */
|
|---|
| 1126 | .pe-actions {
|
|---|
| 1127 | position: sticky;
|
|---|
| 1128 | bottom: 0;
|
|---|
| 1129 | display: flex; align-items: center;
|
|---|
| 1130 | gap: 0.5rem;
|
|---|
| 1131 | padding: 0.85rem 1rem;
|
|---|
| 1132 | margin: 0.5rem -1rem 0; /* extend to viewport edges on a narrow container */
|
|---|
| 1133 | background: color-mix(in srgb, var(--paper) 94%, transparent);
|
|---|
| 1134 | -webkit-backdrop-filter: blur(8px);
|
|---|
| 1135 | backdrop-filter: blur(8px);
|
|---|
| 1136 | border-top: 1px solid var(--rule);
|
|---|
| 1137 | z-index: 10;
|
|---|
| 1138 | }
|
|---|
| 1139 | .pe-actions-spacer { flex: 1; }
|
|---|
| 1140 |
|
|---|
| 1141 | /* The editor is a focus screen: hide the mobile site tab bar (Home/Search/…)
|
|---|
| 1142 | so two bars don't stack at the bottom (Save bar + tab bar). The
|
|---|
| 1143 | Save/Cancel bar then becomes the only bottom bar → plain bottom:0,
|
|---|
| 1144 | no tab offset needed. On desktop the tab bar is already hidden. */
|
|---|
| 1145 | body:has(.post-edit-page) .bottom-tab { display: none; }
|
|---|
| 1146 | /* Audio player (if playing) no longer lifted 56px for the now-hidden
|
|---|
| 1147 | tab bar, otherwise it would float above the action bar. */
|
|---|
| 1148 | body:has(.post-edit-page) .audio-player { bottom: 0; }
|
|---|
| 1149 |
|
|---|
| 1150 | /* ── Image editor (rotate / crop / mirror) ── */
|
|---|
| 1151 | .imed-backdrop {
|
|---|
| 1152 | position: fixed; inset: 0; z-index: 9999;
|
|---|
| 1153 | display: flex; align-items: center; justify-content: center;
|
|---|
| 1154 | background: rgba(0,0,0,.62); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
|
|---|
| 1155 | padding: 14px;
|
|---|
| 1156 | }
|
|---|
| 1157 | .imed-modal {
|
|---|
| 1158 | display: flex; flex-direction: column; gap: 12px;
|
|---|
| 1159 | width: 100%; max-width: 640px; max-height: 92vh;
|
|---|
| 1160 | background: var(--paper, #fff); color: var(--ink, #111);
|
|---|
| 1161 | border: 1px solid var(--rule, rgba(128,128,128,.3)); border-radius: 14px;
|
|---|
| 1162 | padding: 14px; box-sizing: border-box;
|
|---|
| 1163 | }
|
|---|
| 1164 | .imed-stage {
|
|---|
| 1165 | height: 58vh; flex: 0 0 auto;
|
|---|
| 1166 | background: var(--paper-2, rgba(128,128,128,.08)); border-radius: 10px; overflow: hidden;
|
|---|
| 1167 | }
|
|---|
| 1168 | .imed-stage img { display: block; max-width: 100%; }
|
|---|
| 1169 | /* Cropper's container must fill the full stage height — otherwise it collapses
|
|---|
| 1170 | in a flex column without an explicit height → empty edit window (no image). */
|
|---|
| 1171 | .imed-stage .cropper-container { width: 100% !important; height: 100% !important; }
|
|---|
| 1172 | .imed-tools {
|
|---|
| 1173 | display: flex; flex-wrap: wrap; gap: 6px; justify-content: center;
|
|---|
| 1174 | }
|
|---|
| 1175 | .imed-tools button {
|
|---|
| 1176 | width: 42px; height: 42px; font-size: 18px; line-height: 1;
|
|---|
| 1177 | border: 1px solid var(--rule, rgba(128,128,128,.4)); border-radius: 10px;
|
|---|
| 1178 | background: var(--paper-2, transparent); color: inherit; cursor: pointer;
|
|---|
| 1179 | }
|
|---|
| 1180 | .imed-tools button:hover { border-color: var(--accent); }
|
|---|
| 1181 | .imed-actions { display: flex; gap: 8px; justify-content: flex-end; }
|
|---|
| 1182 | </style>
|
|---|
| 1183 |
|
|---|
| 1184 |
|
|---|
| 1185 |
|
|---|
| 1186 | <%# ── Track picker modal (P59). Mobile-first: full-screen sheet on small
|
|---|
| 1187 | viewports, centered modal on ≥640px. Populated lazily on first open. %>
|
|---|
| 1188 | <% if (typeof user !== 'undefined' && user) { %>
|
|---|
| 1189 | <div id="track-picker" class="tp-modal" hidden aria-hidden="true">
|
|---|
| 1190 | <div class="tp-backdrop" data-tp-close></div>
|
|---|
| 1191 | <div class="tp-sheet" role="dialog" aria-modal="true" aria-labelledby="tp-title">
|
|---|
| 1192 | <header class="tp-header">
|
|---|
| 1193 | <h2 id="tp-title" class="tp-h2"><%= t('pedit.tp_title') %></h2>
|
|---|
| 1194 | <button type="button" class="tp-close" data-tp-close aria-label="<%= t('pedit.tp_close') %>">
|
|---|
| 1195 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
|---|
| 1196 | </button>
|
|---|
| 1197 | </header>
|
|---|
| 1198 | <div class="tp-search-row">
|
|---|
| 1199 | <span class="tp-search-icon" aria-hidden="true">
|
|---|
| 1200 | <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
|---|
| 1201 | </span>
|
|---|
| 1202 | <input type="search" class="tp-search" id="tp-search" placeholder="<%= t('pedit.tp_search_placeholder') %>" autocomplete="off" inputmode="search">
|
|---|
| 1203 | </div>
|
|---|
| 1204 | <div class="tp-list" id="tp-list" role="listbox" aria-label="<%= t('pedit.tp_list_aria') %>">
|
|---|
| 1205 | <div class="tp-empty" id="tp-empty"><%= t('pedit.js_tracks_loading') %></div>
|
|---|
| 1206 | </div>
|
|---|
| 1207 | </div>
|
|---|
| 1208 | </div>
|
|---|
| 1209 | <% } %>
|
|---|
| 1210 |
|
|---|
| 1211 | <%# Playlist editor modal — included so its global window.openPlaylistEditor
|
|---|
| 1212 | is available when the toolbar button fires. Only renders if user can post. %>
|
|---|
| 1213 | <% if (typeof user !== 'undefined' && user) { %>
|
|---|
| 1214 | <%- include('../partials/playlist-editor', { csrfToken: (typeof csrfToken !== 'undefined' ? csrfToken : '') }) %>
|
|---|
| 1215 | <% } %>
|
|---|
| 1216 | <%- include('../partials/page-data', { pageData: { _timezone: (typeof timezone !== 'undefined' ? timezone : ''), apply: t('imed.apply'), audio_up_busy: t('pedit.audio_up_busy'), audio_up_done: t('pedit.audio_up_done'), audio_up_fail: t('pedit.audio_up_fail'), cancel: t('imed.cancel'), chip_album: t('pedit.chip_album'), chip_playlist: t('pedit.chip_playlist'), chip_track: t('pedit.chip_track'), flip_h: t('imed.flip_h'), flip_v: t('imed.flip_v'), js_embed_invalid: t('pedit.js_embed_invalid'), js_embed_prompt: t('pedit.js_embed_prompt'), js_failed: t('pedit.js_failed'), js_inserted: t('pedit.js_inserted'), js_link_prompt: t('pedit.js_link_prompt'), js_no_tracks_found: t('pedit.js_no_tracks_found'), js_no_tracks_yet: t('pedit.js_no_tracks_yet'), js_playlist_choose: t('pedit.js_playlist_choose'), js_playlist_editor_missing: t('pedit.js_playlist_editor_missing'), js_playlist_existing: t('pedit.js_playlist_existing'), js_tracks_load_fail: t('pedit.js_tracks_load_fail'), js_tracks_loading: t('pedit.js_tracks_loading'), js_uploaded: t('pedit.js_uploaded'), js_uploading: t('pedit.js_uploading'), pin_nth_suffix: t('pedit.pin_nth_suffix'), pin_top: t('pedit.pin_top'), reset: t('imed.reset'), rotate_left: t('imed.rotate_left'), rotate_right: t('imed.rotate_right'), tb_done: t('pedit.tb_done'), tb_fullscreen: t('pedit.tb_fullscreen'), title: t('imed.title'), zoom_in: t('imed.zoom_in'), zoom_out: t('imed.zoom_out') } }) %>
|
|---|