dialog: Position/Rolle + Breadcrumb-Nav + nüchterne Wortmeldungen, Footer voll-breit

- comments: author_role (Position bei OPENBUREAU) aus authors.json gespeichert/ausgeliefert
- schema: comments.author_role hinzugefügt
- dialog.js: Breadcrumb (Dialoge › Forum), volles Datum/Uhrzeit, Box→Trennlinien-Layout
- css: Footer voll-breit (Flex statt Grid), Balken zwischen Header/main/Footer entfernt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:37:50 +02:00
parent 7b25f644a2
commit 6d20be036a
4 changed files with 71 additions and 48 deletions
+35 -34
View File
@@ -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; }
}
/* ------------------------------------------------------------------------
+2 -1
View File
@@ -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();
+2
View File
@@ -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;
+32 -13
View File
@@ -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);
});
}