diff --git a/assets/css/custom.css b/assets/css/custom.css index 0fa4323..4e4c37c 100644 --- a/assets/css/custom.css +++ b/assets/css/custom.css @@ -98,6 +98,7 @@ body { overflow: hidden; display: flex; flex-direction: column; + gap: 0; /* Theme-main.css setzt body{gap:spacing-lg} → erzeugte Balken zwischen Header/main/Footer */ } body > header.site-header, body > footer { flex: none; } @@ -114,7 +115,7 @@ body > main::-webkit-scrollbar { width: 0; height: 0; display: none; } body:not(.is-home) > main { display: flex; flex-direction: column; - justify-content: safe center; + justify-content: flex-start; /* Inhalt startet direkt unter dem Header (keine Zentrier-Bänder) */ } /* Inhalt 72ch-zentriert in der vollbreiten Scroll-Fläche (Home = Vollbreite). */ body:not(.is-home) > main > * { @@ -396,8 +397,7 @@ body.is-home .journal-list::-webkit-scrollbar { width: 0; height: 0; display: no body.is-home .journal-entry { flex: 0 0 auto; } body.is-home .more { flex: none; padding: 0.4rem 10px; margin: 0; } /* Footer kompakt (~1/3): kein großer Außenabstand, knappes Padding. */ -body.is-home > footer { margin-top: 0; padding: 0.55rem 0; } -body.is-home > footer .footer-grid { row-gap: 0.2rem; } +body.is-home > footer { margin-top: 0; padding: 0.55rem 1.5rem; } @media (max-width: 720px) { /* Mobil: kein Full-Height-Rahmen — normale Seite scrollt, kein interner Scroll. */ @@ -570,7 +570,9 @@ a.byline-author:hover, a.journal-author:hover { color: var(--accent); } /* Eigene Dialog-Seite (/dialog/?thread=…) */ /* Füllt die normale Inhaltsspalte (kein eigenes max-width/Seiten-Padding → gleiche Breite wie andere Seiten) */ -.dialog-page { padding: var(--spacing-sm) 0 var(--spacing-xl); } +/* width:100% → füllt immer die ganze Inhaltsspalte (sonst schrumpft .dialog-page + als Flex-Item mit margin-inline:auto bei schmalem Inhalt, z.B. Forum-Ansicht). */ +.dialog-page { width: 100%; padding: var(--spacing-sm) 0 var(--spacing-xl); } .dialog-overview { display: flex; flex-direction: column; gap: 0.6em; } .dialog-overview-item { display: flex; justify-content: space-between; align-items: baseline; gap: 1em; @@ -584,23 +586,33 @@ a.byline-author:hover, a.journal-author:hover { color: var(--accent); } .dialog-back:empty { margin: 0; } .dialog-back a { color: var(--color-text-muted); text-decoration: none; } .dialog-back a:hover { color: var(--accent); } +/* Dialog-Navigation oben: Breadcrumb (Dialoge › Forum). */ +.dialog-crumb { display: flex; flex-wrap: wrap; align-items: center; gap: 0.45em; font-size: var(--font-size-small); } +.dialog-crumb a { color: var(--color-text-muted); text-decoration: none; } +.dialog-crumb a:hover { color: var(--accent); } +.dialog-crumb-sep { color: var(--color-text-muted); } .dialog-title { font-family: var(--font-family-serif); margin: 0 0 var(--spacing-md); } -.dialog-list { display: flex; flex-direction: column; gap: var(--spacing-md); margin-bottom: var(--spacing-lg); } +.dialog-list { display: flex; flex-direction: column; margin-bottom: var(--spacing-lg); } .dialog-empty { color: var(--color-text-muted); font-style: italic; } -.dialog-card { border: 1px solid var(--color-border); border-radius: 12px; padding: var(--spacing-md); background: var(--color-bg-secondary); } -.dialog-card-head { display: flex; align-items: center; gap: 0.7em; margin-bottom: 0.6em; } +/* Nüchterne Wortmeldung: keine Box — nur feine Trennlinie + Abstand. */ +.dialog-post { padding: 1.15em 0; border-bottom: 1px solid var(--color-border); } +.dialog-post:first-child { padding-top: 0.2em; } +.dialog-post:last-child { border-bottom: none; } +.dialog-post-head { display: flex; align-items: center; gap: 0.7em; margin-bottom: 0.5em; } .dialog-avatar { width: 40px; height: 40px; border-radius: 50%; flex: none; background: var(--color-border) center/cover no-repeat; display: grid; place-items: center; font-weight: 600; color: var(--color-text-muted); } -.dialog-meta { display: flex; flex-direction: column; line-height: 1.3; } +.dialog-ident { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; } +.dialog-nameline { display: flex; align-items: baseline; flex-wrap: wrap; gap: 0.5em; } .dialog-name { font-weight: 600; } -.dialog-time { font-size: var(--font-size-small); color: var(--color-text-muted); } +.dialog-pos { font-size: var(--font-size-small); color: var(--color-text-muted); } +.dialog-time { font-size: var(--font-size-small); color: var(--color-text-muted); margin-top: 0.1em; } .dialog-replyto { font-size: var(--font-size-small); color: var(--accent); } .dialog-body { font-family: var(--font-family-serif); line-height: 1.6; white-space: pre-wrap; } .dialog-actions { display: flex; gap: 0.8em; margin-top: 0.6em; } @@ -1355,31 +1367,23 @@ footer { background: var(--color-dark-panel); color: var(--color-dark-panel-text); /* hell & lesbar auf Schwarz */ margin-top: 0; - padding: 0.55rem 0; /* kompakt wie auf der Journal-Seite */ + /* voll-breit; horizontal bündig zum Journal-Karten-Inhalt (1.5rem) */ + padding: 0.55rem 1.5rem; border-top: none; - /* inner grid aligns with content column, same trick as header */ - display: grid; - grid-template-columns: - 1fr - min(var(--container-width), 100% - 3.5rem) - 1fr; } -footer > * { grid-column: 2; } footer a, footer a:hover, footer a:focus { border: none; border-bottom: none; text-decoration: none; } footer a { color: var(--color-dark-panel-text); } footer a:hover { color: var(--accent-soft); } footer p { margin: 0; } -/* Zwei Zeilen: oben Inhalts-Absatz (links) | Links (rechts), - unten Lizenz/Copyright (links). */ +/* Lizenzen ganz links (linksbündig), Footer-Menü ganz rechts. */ .footer-grid { - display: grid; - grid-template-columns: 1fr auto; - align-items: start; + display: flex; + justify-content: space-between; + align-items: center; column-gap: var(--spacing-lg); - row-gap: 0.2rem; } -.footer-legal { grid-row: 1; grid-column: 1; } +.footer-legal { text-align: left; } .footer-licenses { font-family: var(--font-family-mono); font-size: 0.8rem; @@ -1389,14 +1393,13 @@ footer p { margin: 0; } font-family: var(--font-family-mono); font-size: 0.75rem; color: var(--color-dark-panel-muted); - margin-top: 0.35rem; + margin-top: 0.1rem; } .footer-links { - grid-row: 1; grid-column: 2; - justify-self: end; display: flex; flex-wrap: wrap; + justify-content: flex-end; gap: 0.4rem 1.3rem; font-family: var(--font-family-display); font-size: 0.9rem; @@ -1408,14 +1411,12 @@ footer p { margin: 0; } /* Mobile: alles linksbündig stapeln */ @media (max-width: 720px) { - .footer-grid { grid-template-columns: 1fr; } - .footer-legal, - .footer-links { - grid-column: 1; - justify-self: start; + .footer-grid { + flex-direction: column; + align-items: flex-start; + row-gap: 0.5rem; } - .footer-legal { grid-row: 1; } - .footer-links { grid-row: 2; } + .footer-links { justify-content: flex-start; } } /* ------------------------------------------------------------------------ diff --git a/cms/api/src/routes/comments.js b/cms/api/src/routes/comments.js index 55aae9c..99b610a 100644 --- a/cms/api/src/routes/comments.js +++ b/cms/api/src/routes/comments.js @@ -4,7 +4,7 @@ import { profileFor, threadLocked } from '../dialog-store.js'; // Dialog: flache Wortmeldungen pro Thread (= Thread-Key), optionaler Bezug. -const COLS = 'id,thread,parent_id,author_name,author_avatar,body,created_at,deleted'; +const COLS = 'id,thread,parent_id,author_name,author_avatar,author_role,body,created_at,deleted'; // ÖFFENTLICH: Wortmeldungen eines Threads lesen. export async function listComments(c) { @@ -32,6 +32,7 @@ export async function createComment(c) { user_id: user.id, author_name: prof?.name || email.split('@')[0], author_avatar: prof?.avatar || null, + author_role: prof?.title || null, // „Position bei OPENBUREAU" (aus data/authors.json) body: body.trim(), }; const { data, error } = await supabase.from('comments').insert(row).select(COLS).single(); diff --git a/cms/db/schema.sql b/cms/db/schema.sql index 484f109..a549993 100644 --- a/cms/db/schema.sql +++ b/cms/db/schema.sql @@ -46,6 +46,8 @@ create table if not exists public.comments ( created_at timestamptz not null default now(), deleted boolean not null default false ); +-- Position/Rolle bei OPENBUREAU (optional, neben dem Namen angezeigt). +alter table public.comments add column if not exists author_role text; create index if not exists comments_thread_idx on public.comments (thread, created_at); alter table public.comments enable row level security; grant all on public.comments to anon, authenticated, service_role; diff --git a/static/dialog.js b/static/dialog.js index 5444e05..fd3c385 100644 --- a/static/dialog.js +++ b/static/dialog.js @@ -28,6 +28,11 @@ if (s < 86400) return Math.floor(s / 3600) + ' Std.'; return d.toLocaleDateString('de-CH'); } + // Volles Datum + Uhrzeit (für die Wortmeldungen in der Thread-Ansicht). + function fmtFull(ts) { + const d = new Date(ts); + return d.toLocaleDateString('de-CH') + ' · ' + d.toLocaleTimeString('de-CH', { hour: '2-digit', minute: '2-digit' }); + } const api = (p, opt) => fetch(p, opt).then(async (r) => ({ ok: r.ok, status: r.status, body: await r.json().catch(() => ({})) })); const authHdr = () => ({ Authorization: 'Bearer ' + token }); @@ -115,7 +120,7 @@ // ── Forum-Ansicht: Threads + neuer Thread ───────────────────────────────── function renderForum(slug) { root.innerHTML = ''; - if (ctxEl) { ctxEl.innerHTML = ''; const b = el('a', null, '← Dialoge'); b.href = '/dialog/'; ctxEl.appendChild(b); } + if (ctxEl) { ctxEl.innerHTML = ''; const c = el('nav', 'dialog-crumb'); const b = el('a', null, '← Dialoge'); b.href = '/dialog/'; c.appendChild(b); ctxEl.appendChild(c); } api('/api/forums/' + encodeURIComponent(slug)).then((r) => { if (!r.ok) { root.appendChild(el('p', 'dialog-empty', 'Forum nicht gefunden.')); return; } const { forum, threads } = r.body; @@ -192,9 +197,19 @@ const m = r.body; title.textContent = m.title || 'Dialog'; locked = m.locked; if (ctxEl) { ctxEl.innerHTML = ''; - const back = el('a', null, m.kind === 'library' ? '← zum Beitrag' : (m.forum ? '← ' + m.forum.name : '← Dialoge')); - back.href = m.kind === 'library' ? m.url : (m.forum ? '/dialog/?forum=' + encodeURIComponent(m.forum.slug) : '/dialog/'); - ctxEl.appendChild(back); + if (m.kind === 'library') { + const back = el('a', null, '← zum Beitrag'); back.href = m.url; ctxEl.appendChild(back); + } else { + // Breadcrumb-Navigation oben: Dialoge › Forum (beide anklickbar). + const crumb = el('nav', 'dialog-crumb'); + const a0 = el('a', null, 'Dialoge'); a0.href = '/dialog/'; crumb.appendChild(a0); + if (m.forum) { + crumb.appendChild(el('span', 'dialog-crumb-sep', '›')); + const a1 = el('a', null, m.forum.name); a1.href = '/dialog/?forum=' + encodeURIComponent(m.forum.slug); + crumb.appendChild(a1); + } + ctxEl.appendChild(crumb); + } } renderModbar(); renderComposer(); }); @@ -230,23 +245,27 @@ const names = {}; data.forEach((c) => { names[c.id] = c.author_name; }); if (!data.length) { list.appendChild(el('p', 'dialog-empty', 'Noch keine Wortmeldungen — beginne den Dialog.')); return; } data.forEach((c) => { - const card = el('article', 'dialog-card'); - const head = el('header', 'dialog-card-head'); + // Nüchtern: Avatar · Name (+ Position) · darunter Datum/Uhrzeit · Text. + const post = el('article', 'dialog-post'); + const head = el('header', 'dialog-post-head'); const av = el('span', 'dialog-avatar'); if (c.author_avatar) av.style.backgroundImage = 'url(' + c.author_avatar + ')'; else av.textContent = (c.author_name || '?').slice(0, 1).toUpperCase(); - const meta = el('div', 'dialog-meta'); - meta.append(el('span', 'dialog-name', c.author_name || 'Unbekannt'), el('time', 'dialog-time', fmt(c.created_at))); - if (c.parent_id && names[c.parent_id]) meta.appendChild(el('span', 'dialog-replyto', '↳ ' + names[c.parent_id])); - head.append(av, meta); card.appendChild(head); - card.appendChild(el('div', 'dialog-body', c.body)); + const ident = el('div', 'dialog-ident'); + const nameline = el('div', 'dialog-nameline'); + nameline.appendChild(el('span', 'dialog-name', c.author_name || 'Unbekannt')); + if (c.author_role) nameline.appendChild(el('span', 'dialog-pos', c.author_role)); + if (c.parent_id && names[c.parent_id]) nameline.appendChild(el('span', 'dialog-replyto', '↳ ' + names[c.parent_id])); + ident.append(nameline, el('time', 'dialog-time', fmtFull(c.created_at))); + head.append(av, ident); post.appendChild(head); + post.appendChild(el('div', 'dialog-body', c.body)); if (token && !c.deleted) { const actions = el('div', 'dialog-actions'); if (!locked) { const rep = el('button', null, 'Antworten'); rep.onclick = () => { replyTo = { id: c.id, name: c.author_name }; renderComposer(); if (textarea) textarea.focus(); }; actions.appendChild(rep); } const del = el('button', null, 'Löschen'); del.onclick = () => remove(c.id); actions.appendChild(del); - card.appendChild(actions); + post.appendChild(actions); } - list.appendChild(card); + list.appendChild(post); }); }