Chat: Avatare koennen Profilbilder tragen

avatarKreis() zeigt ein Foto, sobald eines unter
assets/eigene/portrait-<kuerzel>.<endung> liegt -- portrait-sk, portrait-mb,
portrait-tw, portrait-ms -- rund beschnitten, mit demselben Rand wie der
Kuerzelkreis. Ist keines da, steht wie bisher das Kuerzel im Kreis. Hochladen
geht ueber "Eigene Bilder" in der Seite; preloadAssets() holt alles aus
assets/eigene ohnehin mit, der Export bekommt sie damit als Daten-URL.

Mit einem Testbild durchgefahren und wieder entfernt.

Keine Fotos im Repo, und zwar mit Absicht: das Pitch-Paket laedt seine
Portraits von Unsplash -- vier Aufnahmen echter, erkennbarer Menschen. Die
Lizenz deckt das Foto, nicht das Recht am eigenen Bild. Ein fremdes Gesicht als
"Sandra Koch, Projektleitung" in einen oeffentlichen Beitrag zu setzen, ist
etwas anderes als ein Klickdummy fuer einen Kundentermin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 5 committed 2026-08-27 19:57:00 +02:00
1 parent a94c1985fc
commit 20d26d3383
2 files changed
+54 -6

No files matched your search

+14
View File
@@ -117,6 +117,20 @@ Familien in einer Liste, und keine davon sagte etwas, das die Zeile nicht schon
sagt. Die Kürzel unterscheiden die Personen, dafür braucht es keine vier
Farbtöne.
**Profilbilder sind vorbereitet, aber keine drin.** `avatarKreis()` zeigt ein
Foto, sobald eines unter `assets/eigene/portrait-<kürzel>.<endung>` liegt (also
`portrait-sk`, `portrait-mb`, `portrait-tw`, `portrait-ms`), rund beschnitten
mit demselben Rand wie der Kürzelkreis; sonst steht wie bisher das Kürzel da.
Hochladen geht über „Eigene Bilder" in der Seite, `preloadAssets()` holt alles
aus `assets/eigene` ohnehin mit, und der Export bekommt sie damit als Daten-URL
statt als Verweis.
Warum keine im Repo: das Pitch-Paket lädt seine Portraits von Unsplash
(`PORTRAITS` in `GeneralTeamChatPage.tsx`, vier Aufnahmen echter, erkennbarer
Menschen). Die Lizenz deckt das Foto, nicht das Recht am eigenen Bild. Ein
fremdes Gesicht als „Sandra Koch, Projektleitung" in einen öffentlichen Beitrag
zu setzen, ist etwas anderes als ein Klickdummy für einen Kundentermin.
**Die Plakette für Ungelesenes trägt eine Zahl und ist grün.** In der Quelle
ist `unread` ein Wahrheitswert und der Punkt violett. Beides ist geändert:
violett ist im Produkt die Farbe der Projekte, das Kanalsymbol in derselben
+40 -6
View File
@@ -2603,6 +2603,44 @@ if (location.search.indexOf("nur=anim") >= 0)
ohnehin die Farbe der Plakette daneben. */
const MARKE = { flaeche: "#DCFCE4", rand: "#B7EDC6", zeichen: "#148134" };
/* Profilbilder. Solange keine da sind, steht das Kuerzel im Kreis -- so wie
jetzt. Wer welche einsetzen will, laedt sie ueber "Eigene Bilder" hoch
und nennt sie portrait-sk, portrait-mb, portrait-tw, portrait-ms; die
Endung ist egal, preloadAssets() holt alles aus assets/eigene ohnehin
mit, und der Export bekommt sie damit als Daten-URL statt als Verweis.
Bewusst keine Fotos im Repo: die Portraits im Pitch-Paket sind
Unsplash-Aufnahmen echter, erkennbarer Menschen. Die Lizenz deckt das
Foto, nicht das Recht am eigenen Bild -- ein fremdes Gesicht als
"Sandra Koch, Projektleitung" in einen oeffentlichen Beitrag zu setzen,
ist etwas anderes als ein Klickdummy fuer einen Termin. */
let pfNr = 0;
const portrait = kuerzel => {
if (!kuerzel) return null;
const su = "/eigene/portrait-" + kuerzel.toLowerCase() + ".";
const t = Object.keys(assets).find(k => k.includes(su));
return t ? assets[t] : null;
};
/* Ein runder Avatar: Foto, wenn eines da ist, sonst das Kuerzel. Der Rand
liegt in beiden Faellen darueber, damit die Kante gleich aussieht. */
function avatarKreis(cx, cy, r, kuerzel) {
const bild = portrait(kuerzel);
if (!bild) {
return '<circle cx="' + n1(cx) + '" cy="' + n1(cy) + '" r="' + r + '" fill="' +
MARKE.flaeche + '" stroke="' + MARKE.rand + '" stroke-width="1.2"/>' +
uiText(cx, cy + r * 0.3, kuerzel, r * 0.75, 800, MARKE.zeichen, { anker: "middle" });
}
const id = "pfoto" + (++pfNr);
return '<clipPath id="' + id + '"><circle cx="' + n1(cx) + '" cy="' + n1(cy) +
'" r="' + r + '"/></clipPath>' +
'<image href="' + bild + '" x="' + n1(cx - r) + '" y="' + n1(cy - r) + '" width="' +
n1(2 * r) + '" height="' + n1(2 * r) +
'" preserveAspectRatio="xMidYMid slice" clip-path="url(#' + id + ')"/>' +
'<circle cx="' + n1(cx) + '" cy="' + n1(cy) + '" r="' + r +
'" fill="none" stroke="' + MARKE.rand + '" stroke-width="1.2"/>';
}
/* Die Chatliste links. type steuert das Sinnbild davor: ein Projekt bekommt
den Aktenkoffer auf farbigem Grund, eine Gruppe die Personen, eine
Direktnachricht das Kuerzel des Menschen -- so macht es ChatAvatar. */
@@ -2655,9 +2693,7 @@ if (location.search.indexOf("nur=anim") >= 0)
}
const mx = x + 16, my = y + ZEILE_H / 2;
if (c.art === "direkt") {
g += '<circle cx="' + n1(mx + 20) + '" cy="' + n1(my) + '" r="20" fill="' +
MARKE.flaeche + '" stroke="' + MARKE.rand + '" stroke-width="1.2"/>' +
uiText(mx + 20, my + 6, c.kuerzel, 15, 800, MARKE.zeichen, { anker: "middle" });
g += avatarKreis(mx + 20, my, 20, c.kuerzel);
} else {
g += '<rect x="' + n1(mx) + '" y="' + n1(my - 20) + '" width="40" height="40" rx="11" ' +
'fill="' + MARKE.flaeche + '" stroke="' + MARKE.rand + '" stroke-width="1.2"/>';
@@ -2781,9 +2817,7 @@ if (location.search.indexOf("nur=anim") >= 0)
const bx = m.mein ? x + b - ma.breite : x + 52;
let g = "";
if (!m.mein) {
g += '<circle cx="' + n1(x + 20) + '" cy="' + n1(y + 22) + '" r="20" fill="' +
MARKE.flaeche + '" stroke="' + MARKE.rand + '" stroke-width="1.2"/>' +
uiText(x + 20, y + 28, m.kuerzel, 15, 800, MARKE.zeichen, { anker: "middle" });
g += avatarKreis(x + 20, y + 22, 20, m.kuerzel);
}
const kontur = rundRechteck(bx, y, ma.breite, ma.hoehe, 14);
g += '<g filter="url(#hgschatten)"><path d="' + kontur + '" fill="' +