fix(web): Favoriten-Platzhalterbuchstabe verschwindet, sobald das Logo geladen ist
Tessera CI/CD / Lint & Type Check (push) Successful in 59s
Tessera CI/CD / Tests (push) Successful in 1m40s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 13m15s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m51s

Bei Logos mit durchsichtigem Hintergrund schien der Buchstabe durch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-06 11:53:18 +02:00
parent ba4c4d52b3
commit 6ccab8330c
3 changed files with 42 additions and 9 deletions
+1
View File
@@ -6,6 +6,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
### Behoben ### Behoben
- Favoriten: Bei Logos mit durchsichtigem Hintergrund schien der graue Platzhalter-Buchstabe durch. Er verschwindet jetzt, sobald das Logo geladen ist.
- Linux-App: Links, die in einem neuen Fenster aufgehen (zum Beispiel aus Notizen oder Favoriten), übergibt die App jetzt an das Öffnen-Programm Ihres Systems statt an eine mitgebrachte Kopie. Auf manchen Systemen öffnete sich dadurch vorher kein Browser. - Linux-App: Links, die in einem neuen Fenster aufgehen (zum Beispiel aus Notizen oder Favoriten), übergibt die App jetzt an das Öffnen-Programm Ihres Systems statt an eine mitgebrachte Kopie. Auf manchen Systemen öffnete sich dadurch vorher kein Browser.
## 1.10.0 – 2026-10-06 ## 1.10.0 – 2026-10-06
@@ -414,6 +414,25 @@ describe('FavoritesWidget', () => {
expect(screen.getByTestId('letter-fallback-fav-id-1')).toHaveTextContent('G'); expect(screen.getByTestId('letter-fallback-fav-id-1')).toHaveTextContent('G');
}); });
it('06.10.: geladenes Logo blendet den Buchstaben aus (durchsichtige Logos), Fehler zeigt ihn wieder', async () => {
render(<FavoritesWidget instanceId="fav-1" config={{}} isEditMode={false} />);
await waitFor(() => {
expect(screen.getByText('GitHub')).toBeInTheDocument();
});
const proxyImg = screen.getByTestId('icon-proxy-fav-id-1') as HTMLImageElement;
expect(screen.getByTestId('letter-fallback-fav-id-1')).toBeInTheDocument();
act(() => {
fireEvent.load(proxyImg);
});
expect(screen.queryByTestId('letter-fallback-fav-id-1')).not.toBeInTheDocument();
act(() => {
fireEvent.error(proxyImg);
});
expect(screen.getByTestId('letter-fallback-fav-id-1')).toHaveTextContent('G');
});
it('ausdrueckliche Symbol-Adresse (260929-lh3): Proxy -> Browser laedt iconUrl direkt (no-referrer) -> Origin-Favicon -> Buchstabe', async () => { it('ausdrueckliche Symbol-Adresse (260929-lh3): Proxy -> Browser laedt iconUrl direkt (no-referrer) -> Origin-Favicon -> Buchstabe', async () => {
mockFetch.mockResolvedValue([ mockFetch.mockResolvedValue([
{ {
@@ -543,7 +543,9 @@ function getDirectCandidates(iconUrl: string | null, url: string): string[] {
* Browser aber das Bild), dann `{origin}/favicon.ico` (entfaellt, wenn * Browser aber das Bild), dann `{origin}/favicon.ico` (entfaellt, wenn
* identisch). Erreicht auch interne Hosts, die der SSRF-Schutz des * identisch). Erreicht auch interne Hosts, die der SSRF-Schutz des
* Servers absichtlich ablehnt. * Servers absichtlich ablehnt.
* 3. `none` — der Buchstaben-Platzhalter liegt IMMER darunter. * 3. `none` — der Buchstaben-Platzhalter steht da, solange kein Bild
* geladen ist. Sobald ein Bild geladen ist, verschwindet er: bei Logos mit
* durchsichtigem Hintergrund schien er sonst durch (06.10.2026).
* *
* Bewusst KEIN Drittanbieter-Favicon-Dienst: der wuerde Hostnamen nach * Bewusst KEIN Drittanbieter-Favicon-Dienst: der wuerde Hostnamen nach
* aussen geben und interne Hosts ohnehin nicht kennen. * aussen geben und interne Hosts ohnehin nicht kennen.
@@ -590,15 +592,18 @@ function FavoriteIcon({
// 'proxy' | Index in directSrcs | Ende der Kette (Buchstabe). // 'proxy' | Index in directSrcs | Ende der Kette (Buchstabe).
const [stage, setStage] = useState<'proxy' | number>(proxySrc ? 'proxy' : 0); const [stage, setStage] = useState<'proxy' | number>(proxySrc ? 'proxy' : 0);
const directSrc = typeof stage === 'number' ? (directSrcs[stage] ?? null) : null; const directSrc = typeof stage === 'number' ? (directSrcs[stage] ?? null) : null;
const [imageLoaded, setImageLoaded] = useState(false);
return ( return (
<div className={`relative flex shrink-0 items-center justify-center bg-muted ${box}`}> <div className={`relative flex shrink-0 items-center justify-center bg-muted ${box}`}>
<span {!imageLoaded && (
data-testid={`letter-fallback-${fav.id}`} <span
className={`absolute inset-0 flex items-center justify-center font-semibold text-muted-foreground ${large ? 'text-base' : 'text-xs'}`} data-testid={`letter-fallback-${fav.id}`}
> className={`absolute inset-0 flex items-center justify-center font-semibold text-muted-foreground ${large ? 'text-base' : 'text-xs'}`}
{getFallbackLetter(fav.title)} >
</span> {getFallbackLetter(fav.title)}
</span>
)}
{stage === 'proxy' && proxySrc && ( {stage === 'proxy' && proxySrc && (
<img <img
data-testid={`icon-proxy-${fav.id}`} data-testid={`icon-proxy-${fav.id}`}
@@ -609,7 +614,11 @@ function FavoriteIcon({
height={px} height={px}
loading="lazy" loading="lazy"
className={`relative rounded-sm ${img}`} className={`relative rounded-sm ${img}`}
onError={() => setStage(0)} onLoad={() => setImageLoaded(true)}
onError={() => {
setImageLoaded(false);
setStage(0);
}}
/> />
)} )}
{directSrc && ( {directSrc && (
@@ -623,7 +632,11 @@ function FavoriteIcon({
loading="lazy" loading="lazy"
referrerPolicy="no-referrer" referrerPolicy="no-referrer"
className={`relative rounded-sm ${img}`} className={`relative rounded-sm ${img}`}
onError={() => setStage((prev) => (typeof prev === 'number' ? prev + 1 : 0))} onLoad={() => setImageLoaded(true)}
onError={() => {
setImageLoaded(false);
setStage((prev) => (typeof prev === 'number' ? prev + 1 : 0));
}}
/> />
)} )}
</div> </div>