source: Klonkt/src/views/partials/track-editor.ejs@ 54ee51c

main
Last change on this file since 54ee51c was 54ee51c, checked in by roboburr <roboburr@…>, 5 weeks ago

De profielkop, de track-editor en het data-kanaal (shaer-bqr, stap 3c)

PROFIELKOP naar mod/chrome.js: chrome.ejs neemt die partial op, dus hij zit in
de OOB-swap en kwam bij elke navigatie opnieuw binnen. Hoort bij de chrome, niet
bij een pagina.

TRACK-EDITOR (475 regels, geen interpolatie) naar een eigen module. admin-audio
neemt hem op, dus die route vraagt om 'admin-audio track-editor' -- een pagina
mag meer dan een module willen.

HET DATA-KANAAL, want de rest heeft het nodig. Een module is een statisch
bestand, dus er kan geen EJS in; alles wat een script van de server nodig heeft
gaat via partials/page-data.ejs en wordt gelezen met pageData() uit mod/lib.js.

Dat is een script-element en toch geen CSP-probleem: een script met een ander
type dan JavaScript wordt niet uitgevoerd en niet opgehaald, dus script-src heeft
er niets over te zeggen. Het werkt daarmee ook in inhoud die via htmx binnenkomt.

Twee dingen die daar misgaan als je ze niet doet, en allebei nagemeten:

De kleiner-dan wordt ontsnapt. Zonder dat sluit een sluitende script-tag IN de
gegevens het blok voortijdig af en staat de rest als HTML op de pagina.
Getoetst met precies zo'n waarde.

pageData() leest ELKE KEER opnieuw en onthoudt niets. Bij een htmx-navigatie
wisselt de inhoud terwijl de module blijft leven, dus een blok dat je bij het
laden vasthoudt is een pagina later verouderd.

Alle templates compileren.

  • Property mode set to 100644
File size: 9.7 KB
Line 
1<%
2// Track editor modal — mobile-first rewrite.
3//
4// Visual model:
5// Mobile (<720px): bottom-sheet that slides up from below, max 92vh,
6// rounded top corners only, drag-handle bar at top.
7// Desktop (≥720px): centered modal max-width 520px, fully-rounded.
8//
9// Self-contained styling under the .te-* namespace — no dependency on
10// the .ax-* admin primitives. Same visual language though: paper bg,
11// rule borders, accent for active states.
12//
13// Exposes window.openTrackEditor({ id, onSaved? }) — called by the
14// "✎ Bewerken" buttons on admin-audio. Fetches /admin/audio/api/:id,
15// renders the form with current values + an inline <audio> preview,
16// and PATCHes via /admin/audio/api/:id on save.
17
18const _csrf = (typeof csrfToken !== 'undefined' && csrfToken) || '';
19%>
20
21<style>
22/* ─────────────────────────────────────────────────────────────────
23 Track editor modal — namespace .te-*
24 ───────────────────────────────────────────────────────────────── */
25
26.te-backdrop {
27 position: fixed; inset: 0;
28 /* Above bottom-tab nav (z=1050) and audio-player sheet (z=1100) so the
29 editor always wins. */
30 z-index: 9000;
31 background: rgba(0, 0, 0, 0.55);
32 -webkit-backdrop-filter: blur(4px);
33 backdrop-filter: blur(4px);
34 display: flex;
35 /* Mobile: dock to bottom; desktop overrides to center via media query */
36 align-items: flex-end;
37 justify-content: center;
38 animation: te-fade 160ms ease-out;
39}
40@keyframes te-fade { from { opacity: 0; } to { opacity: 1; } }
41
42.te-modal {
43 background: var(--paper);
44 width: 100%;
45 max-height: 92vh;
46 /* Bottom-sheet shape on mobile — only top corners rounded */
47 border-radius: 16px 16px 0 0;
48 border: 1px solid var(--rule);
49 border-bottom: 0;
50 box-shadow: 0 -8px 30px rgba(0,0,0,0.4);
51 display: flex; flex-direction: column;
52 overflow: hidden;
53 animation: te-slide-up 220ms cubic-bezier(.2,.9,.3,1);
54}
55@keyframes te-slide-up {
56 from { transform: translateY(100%); }
57 to { transform: translateY(0); }
58}
59
60/* Desktop: center the modal as a card */
61@media (min-width: 720px) {
62 .te-backdrop { align-items: center; padding: 1rem; }
63 .te-modal {
64 max-width: 520px;
65 border-radius: 14px;
66 border: 1px solid var(--rule);
67 max-height: calc(100vh - 2rem);
68 box-shadow: 0 20px 60px rgba(0,0,0,0.4);
69 animation: te-fade 220ms ease-out;
70 }
71}
72
73/* Drag-handle bar at top of mobile sheet */
74.te-handle {
75 width: 100%; padding: 0.6rem 0;
76 display: flex; justify-content: center;
77 flex-shrink: 0;
78}
79.te-handle-bar {
80 width: 36px; height: 4px;
81 background: var(--rule);
82 border-radius: 99px;
83}
84@media (min-width: 720px) { .te-handle { display: none; } }
85
86.te-header {
87 display: flex; align-items: center; justify-content: space-between;
88 padding: 0.5rem 1.25rem 1rem;
89 border-bottom: 1px solid var(--rule);
90 flex-shrink: 0;
91}
92@media (min-width: 720px) { .te-header { padding: 1rem 1.25rem; } }
93.te-header h3 {
94 font-family: var(--font-display, serif);
95 margin: 0; font-size: 1.15rem; font-weight: 600;
96}
97.te-close {
98 width: 36px; height: 36px;
99 display: inline-flex; align-items: center; justify-content: center;
100 border: 0; border-radius: 8px;
101 background: transparent;
102 color: var(--ink-muted, var(--ink-soft));
103 font-size: 1.5rem; line-height: 1;
104 cursor: pointer;
105 transition: background 120ms, color 120ms;
106}
107.te-close:hover { background: var(--paper-2); color: var(--ink); }
108
109.te-body {
110 padding: 1rem 1.25rem;
111 overflow-y: auto;
112 flex: 1;
113 -webkit-overflow-scrolling: touch;
114 display: flex; flex-direction: column; gap: 1rem;
115}
116
117/* ── Inline preview player ─────────────────────────────────── */
118.te-preview {
119 display: flex; align-items: center; gap: 0.75rem;
120 padding: 0.75rem;
121 background: var(--paper-2);
122 border: 1px solid var(--rule);
123 border-radius: 10px;
124}
125.te-preview-cover {
126 width: 56px; height: 56px;
127 border-radius: 8px; overflow: hidden;
128 background: var(--paper);
129 display: inline-flex; align-items: center; justify-content: center;
130 flex-shrink: 0;
131 font-size: 1.4rem; color: var(--accent);
132}
133.te-preview-cover img { width: 100%; height: 100%; object-fit: cover; }
134.te-preview-meta {
135 flex: 1; min-width: 0;
136 display: flex; flex-direction: column; gap: 0.25rem;
137}
138.te-preview-title {
139 font-weight: 600; font-size: 0.95rem;
140 overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
141}
142.te-preview-sub {
143 font-size: 0.78rem;
144 color: var(--ink-muted, var(--ink-soft));
145}
146.te-preview-play {
147 width: 44px; height: 44px;
148 border: 1px solid var(--rule);
149 border-radius: 50%;
150 background: var(--paper);
151 color: var(--ink);
152 font-size: 1.1rem;
153 cursor: pointer;
154 display: inline-flex; align-items: center; justify-content: center;
155 flex-shrink: 0;
156 transition: border-color 120ms, color 120ms, background 120ms;
157}
158.te-preview-play:hover { border-color: var(--accent); color: var(--accent); }
159.te-preview-play.is-playing {
160 background: var(--accent); color: white; border-color: var(--accent);
161}
162
163/* ── Form fields ───────────────────────────────────────────── */
164.te-form { display: flex; flex-direction: column; gap: 0.85rem; }
165.te-row { display: grid; gap: 0.85rem; }
166.te-row-2 { grid-template-columns: 1fr 1fr; }
167@media (max-width: 480px) { .te-row-2 { grid-template-columns: 1fr; } }
168
169.te-field { display: flex; flex-direction: column; gap: 0.35rem; min-width: 0; }
170.te-field > span {
171 font-size: 0.8rem; font-weight: 600;
172 color: var(--ink-soft);
173 display: flex; align-items: baseline; gap: 0.4rem;
174}
175.te-field > span small {
176 font-weight: 400; color: var(--ink-muted); font-size: 0.95em;
177}
178.te-required { color: #dc2626; }
179.te-field input[type="text"],
180.te-field input[type="number"],
181.te-field input[type="url"] {
182 width: 100%; box-sizing: border-box;
183 padding: 0.6rem 0.75rem; min-height: 44px;
184 border: 1px solid var(--rule); border-radius: 6px;
185 background: var(--paper-2); color: var(--ink);
186 font-family: var(--font-ui, system-ui), sans-serif;
187 font-size: 0.95rem;
188 -webkit-appearance: none; appearance: none;
189 transition: border-color 120ms;
190}
191.te-field input:focus {
192 outline: 2px solid var(--accent);
193 outline-offset: -1px;
194 border-color: var(--accent);
195}
196
197/* ── Credit field with © insert button ──────────────────────────── */
198.te-credit-row { display: flex; gap: 0.4rem; align-items: stretch; }
199.te-credit-row input { flex: 1; min-width: 0; }
200.te-sym-btn {
201 flex: 0 0 auto;
202 width: 44px; min-height: 44px;
203 border: 1px solid var(--rule); border-radius: 6px;
204 background: var(--paper-2); color: var(--ink);
205 font-size: 1.1rem; cursor: pointer;
206 transition: border-color 120ms, background 120ms;
207}
208.te-sym-btn:hover { border-color: var(--accent); }
209
210/* ── Cover field with thumb + button + URL ─────────────────── */
211.te-cover-row {
212 display: grid;
213 grid-template-columns: 80px 1fr;
214 gap: 0.75rem;
215 align-items: start;
216}
217.te-cover-thumb {
218 width: 80px; height: 80px;
219 border-radius: 10px;
220 border: 1px dashed var(--rule);
221 background: var(--paper-2);
222 display: flex; align-items: center; justify-content: center;
223 overflow: hidden;
224 cursor: pointer;
225 transition: border-color 120ms;
226}
227.te-cover-thumb:hover, .te-cover-thumb.is-dragover { border-color: var(--accent); border-style: solid; }
228.te-cover-thumb img { width: 100%; height: 100%; object-fit: cover; }
229.te-cover-empty {
230 font-size: 1.6rem; opacity: 0.5;
231}
232.te-cover-actions {
233 display: flex; flex-direction: column; gap: 0.5rem;
234 min-width: 0;
235}
236.te-cover-btn-row { display: flex; gap: 0.4rem; flex-wrap: wrap; }
237.te-cover-btn {
238 display: inline-flex; align-items: center; gap: 0.35rem;
239 padding: 0.45rem 0.75rem;
240 border: 1px solid var(--rule); border-radius: 6px;
241 background: var(--paper-2); color: var(--ink);
242 font-family: var(--font-ui, system-ui); font-size: 0.85rem;
243 cursor: pointer;
244 transition: border-color 120ms;
245}
246.te-cover-btn:hover { border-color: var(--accent); }
247.te-cover-btn-remove:hover { color: #dc2626; border-color: #dc2626; }
248.te-cover-status {
249 font-size: 0.78rem;
250 color: var(--ink-muted, var(--ink-soft));
251 min-height: 1em;
252}
253.te-cover-status.is-error { color: #dc2626; }
254.te-cover-status.is-ok { color: #2a9d5e; }
255
256/* ── Footer (sticky) ───────────────────────────────────────── */
257.te-footer {
258 display: flex; align-items: center; gap: 0.5rem;
259 padding: 0.85rem 1.25rem;
260 border-top: 1px solid var(--rule);
261 background: var(--paper);
262 flex-shrink: 0;
263}
264.te-footer-spacer { flex: 1; }
265.te-btn {
266 display: inline-flex; align-items: center; justify-content: center; gap: 0.35rem;
267 padding: 0.6rem 1.1rem; min-height: 44px;
268 border: 1px solid var(--rule); border-radius: 8px;
269 background: var(--paper-2); color: var(--ink);
270 font-family: var(--font-ui, system-ui); font-size: 0.9rem; font-weight: 500;
271 cursor: pointer;
272 transition: border-color 120ms, background 120ms;
273 -webkit-tap-highlight-color: transparent;
274}
275.te-btn:hover { border-color: var(--accent); }
276.te-btn:disabled { opacity: 0.5; cursor: not-allowed; }
277.te-btn-primary {
278 background: var(--accent); color: white; border-color: var(--accent);
279}
280.te-btn-primary:hover { opacity: 0.92; }
281
282/* Lock body scroll while modal is open */
283body.te-modal-open { overflow: hidden; }
284</style>
285
286<%# Het script van deze pagina staat in assets/js/mod/track-editor.js (shaer-bqr). %>
Note: See TracBrowser for help on using the repository browser.