Drei Auswahlfelder liessen sich gleichzeitig aufklappen

Jedes merkte sich nur seine eigene Liste. Nebeneinander geöffnet lagen
sie übereinander, und keine wusste von den anderen. Ein Auswahlfeld ist
ein Menü, und von denen ist höchstens eines offen — jetzt modulweit
vermerkt, genau wie beim Kontextmenü. Escape schliesst zusätzlich von
überall, nicht nur mit dem Fokus in der Liste.

Dazu stand unter jeder Mediathek „0 Titel freigeben". Der Knopf wurde
über `hidden` versteckt — das Attribut wirkt über `display: none` mit der
Spezifität von Null, und `.btn` setzt `display: inline-flex`. Die Klasse
`.hidden` trägt ein `!important` und setzt sich durch.

Und das Favicon kommt jetzt aus `icon.png`, dem gerenderten App-Icon
selbst. Meine nachgezeichnete Fassung war falsch: die Grafik füllt die
Fläche, statt klein in der Mitte zu sitzen. Ein Icon, das dem echten nur
ähnelt, driftet beim nächsten Entwurf wieder auseinander.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-04 19:01:58 +02:00
co-authored by Claude Opus 5
parent 8809b9f46f
commit 4b0323f955
9 changed files with 41 additions and 38 deletions
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

+10 -8
View File
@@ -6,16 +6,18 @@
<meta name="theme-color" content="#0B0B0D">
<title>Flaro</title>
<!-- **Dasselbe Zeichen wie das App-Icon**, erzeugt aus
`Flaro/AppIcon.icon` — gleiche Grafik, gleiche Farben, von Display-P3
nach sRGB umgerechnet. Zwei Icons, die sich ähnlich sehen, driften
auseinander; eins, das aus demselben Ordner kommt, nicht.
<!-- **Das App-Icon selbst**, aus `icon.png` im Projektordner skaliert —
die Fassung, die Icon Composer rendert, mitsamt Glas und Bevel.
SVG zuerst, weil es in jeder Grösse scharf bleibt. Das PNG daneben
für alles, was kein SVG-Favicon kann — und für den Startbildschirm
auf dem Telefon, der ohnehin nur PNG nimmt. -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
Nachgezeichnet war es vorher, und das ging daneben: die Grafik füllt
die Fläche, statt klein in der Mitte zu sitzen. Ein Icon, das dem
echten nur ähnelt, driftet beim nächsten Entwurf wieder auseinander.
Bei 16 und 32 Bildpunkten verliert sich die Glasoptik — das liegt am
Entwurf, nicht am Export: ein Icon für 1024 Punkte hat dort mehr
Feinheit, als ein Browsertab zeigen kann. -->
<link rel="icon" type="image/png" sizes="32x32" href="/icon-32.png">
<link rel="icon" type="image/png" sizes="192x192" href="/icon-192.png">
<link rel="apple-touch-icon" href="/icon-180.png">
<link rel="manifest" href="/manifest.webmanifest">
<!-- **Inter, mit der Systemschrift als Rückfall.** `display=swap`, damit der
-28
View File
@@ -1,28 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Flaro">
<!--
Aus Flaro/AppIcon.icon erzeugt, nicht nachgezeichnet: dieselbe Grafik
(Assets/flare (1).svg) und dieselben Farben, von Display-P3 nach sRGB
umgerechnet. Ändert sich das App-Icon, wird diese Datei neu erzeugt.
#9860FF ist der helle Hintergrund des Icons, #261653 der dunkle.
Der Verlauf ahmt Icon Composers „automatic gradient" nach: oben heller.
-->
<defs>
<linearGradient id="grund" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#B98CFF"/>
<stop offset="1" stop-color="#8243F5"/>
</linearGradient>
</defs>
<rect width="64" height="64" rx="14" fill="url(#grund)"/>
<!--
Die Glyphe weiss statt im Originalton (#900AFF). Auf dem Plakat eines
App-Icons trägt sie das Glas; bei 16 Bildpunkten im Browsertab wäre
Violett auf Violett ein Fleck.
Skaliert: 24er-Raster auf 40 Punkte, zentriert.
-->
<g transform="translate(12 12) scale(1.6667)" fill="#FFFFFF">
<path clip-rule="evenodd" fill-rule="evenodd" d="m11.0721 5.13152c.3321-.84203 1.5237-.84203 1.8558 0l1.6803 4.26031 4.2603 1.68027c.842.3321.842 1.5237 0 1.8558l-4.2603 1.6803-1.6803 4.2603c-.3321.842-1.5237.842-1.8558 0l-1.68027-4.2603-4.26031-1.6803c-.84203-.3321-.84203-1.5237 0-1.8558l4.26031-1.68027zm.9279 1.73562-1.2928 3.27806c-.1014.2571-.3049.4606-.562.562l-3.27806 1.2928 3.27806 1.2928c.2571.1014.4606.3049.562.562l1.2928 3.2781 1.2928-3.2781c.1014-.2571.3049-.4606.562-.562l3.2781-1.2928-3.2781-1.2928c-.2571-.1014-.4606-.3049-.562-.562z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 213 KiB

+25 -1
View File
@@ -28,6 +28,20 @@ export type Auswahlfeld = {
setzen: (optionen: Option[]) => void;
};
/** Das eine offene Auswahlfeld — oder keines.
*
* **Modulweit, nicht je Feld.** Vorher merkte sich jedes Feld nur seine
* eigene Liste; drei Felder nebeneinander liessen sich alle drei aufklappen
* und überlagerten einander. Ein Auswahlfeld ist ein Menü, und von denen ist
* immer höchstens eines offen — genauso hält es das Kontextmenü.
*/
let offenesFeld: { schliessen: () => void } | null = null;
export function alleAuswahlenSchliessen(): void {
offenesFeld?.schliessen();
offenesFeld = null;
}
export function auswahl(optionen: Option[], vorgabe = ''): Auswahlfeld {
let liste = [...optionen];
let wert = vorgabe;
@@ -56,8 +70,11 @@ export function auswahl(optionen: Option[], vorgabe = ''): Auswahlfeld {
offen?.remove();
offen = null;
knopf.setAttribute('aria-expanded', 'false');
if (offenesFeld === steuerung) offenesFeld = null;
}
const steuerung = { schliessen };
function waehlen(neu: string): void {
const geaendert = neu !== wert;
wert = neu;
@@ -70,6 +87,10 @@ export function auswahl(optionen: Option[], vorgabe = ''): Auswahlfeld {
function oeffnen(): void {
if (offen) { schliessen(); return; }
// Erst das andere zu, dann dieses auf.
alleAuswahlenSchliessen();
offenesFeld = steuerung;
const box = document.createElement('div');
box.className = 'auswahl-liste';
box.setAttribute('role', 'listbox');
@@ -172,8 +193,11 @@ export function auswahl(optionen: Option[], vorgabe = ''): Auswahlfeld {
// Kontextmenü. Je Feld registriert und nicht global: es gibt wenige davon,
// und beim Entfernen des Feldes gehen sie mit.
document.addEventListener('click', e => {
if (offen && !offen.contains(e.target as Node) && e.target !== knopf) schliessen();
if (offen && !offen.contains(e.target as Node) && !knopf.contains(e.target as Node)) {
schliessen();
}
});
document.addEventListener('keydown', e => { if (e.key === 'Escape' && offen) schliessen(); });
window.addEventListener('scroll', () => schliessen(), { passive: true, capture: true });
return {
+6 -1
View File
@@ -999,7 +999,12 @@ function bibliotheksbereich(
knoepfe.appendChild(freigeben);
function freigabeSchreiben(): void {
freigeben.hidden = aktuell.pendingCount === 0;
// **`hidden` reicht hier nicht.** Das Attribut wirkt über
// `display: none` mit der Spezifität von Null — `.btn` setzt
// `display: inline-flex` und gewinnt. Der Knopf stand deshalb als
// „0 Titel freigeben" da, obwohl nichts wartet. `.hidden` trägt ein
// `!important` und setzt sich durch.
freigeben.classList.toggle('hidden', aktuell.pendingCount === 0);
freigeben.textContent = `${aktuell.pendingCount} Titel freigeben`;
}
freigabeSchreiben();