fix(kosmogram): własne dymki zamiast natywnych — działają też po powiększeniu
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m51s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 19s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 25s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m38s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 17s
Testy / Kontrola składni wszystkich warstw (push) Successful in 9s

Rysunek niesie <title> przy każdym obiekcie i każdej linii aspektu i to
wystarczało, dopóki koło było statyczne. Po dodaniu powiększania na pełne okno
dymki przestały się pokazywać.

CO SPRAWDZIŁEM W PRZEGLĄDARCE (1280×900, komplet danych): elementy z <title>
SĄ osiągalne kursorem — 19 z 20 trafień w hit-teście — więc nic ich nie zasłania
i problem nie leży w geometrii ani w pointer-events. Której dokładnie reguły
przeglądarka używa do stłumienia natywnego dymka, nie ustaliłem.

Nie ma to jednak znaczenia, bo natywny dymek jest tu i tak kiepskim narzędziem:
pojawia się po sekundzie zwłoki, nie da się go stylować, nie działa na dotyku
i nie ma go czym wywołać z klawiatury. Własny dymek usuwa zależność od zachowania
przeglądarki i przy okazji jest czytelniejszy.

Tekst bierzemy z <title> JUŻ OBECNEGO w rysunku, a nie z drugiej kopii opisów
w JS — inaczej rozjechałyby się przy pierwszej zmianie treści. Na czas najechania
<title> jest odpinany i wieszany z powrotem po zejściu kursora: dzięki temu nigdy
nie widać dwóch dymków naraz, a czytniki ekranu zachowują nazwę dostępną.

Warstwa 1150 CELOWO pomiędzy: nad nakładką powiększonego koła (1100), bo tam
właśnie zgłoszono problem, i pod oknem postępu (1200), które ma zostać na wierzchu
podczas pisania horoskopu. Test pilnuje tej kolejności liczbowo.

Ścieżka PDF nietknięta — składa się po stronie serwera, <title> zostają w rysunku.

Zweryfikowane w obu stanach koła: dymek pokazuje „Sun · 12°30'00'' · dom 1" nad
glifem i „Sun trygon Moon · orb 3.64°" nad linią aspektu, po powiększeniu też
(kursor-lupa mu nie przeszkadza), a po zejściu kursora wszystkie 20 <title>
wraca na miejsce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #67.
This commit is contained in:
2026-08-06 16:31:07 +02:00
parent 8b6ecc727d
commit baf4e0e38a
4 changed files with 184 additions and 0 deletions
@@ -81,6 +81,20 @@ svg.declination text.glyph, svg.antiscia text.glyph { font-size: inherit; }
max-width: min(96vw, 92vh); width: min(96vw, 92vh); height: auto; cursor: zoom-out; max-width: min(96vw, 92vh); width: min(96vw, 92vh); height: auto; cursor: zoom-out;
} }
body.wheel-zoom-open { overflow: hidden; } /* nie przewijaj treści pod spodem */ body.wheel-zoom-open { overflow: hidden; } /* nie przewijaj treści pod spodem */
/* Dymek kosmogramu. z-index 1150 CELOWO pomiędzy: ponad nakładką powiększonego
koła (1100), żeby działał także po powiększeniu, ale PONIŻEJ okna postępu
(1200), które ma zostać na wierzchu. pointer-events: none — dymek chodzi za
kursorem i nie może sam siebie zasłonić ani przechwycić najechania. */
.wheel-tip {
position: fixed; z-index: 1150; pointer-events: none;
max-width: 22rem; padding: .4rem .6rem;
background: var(--panel); color: var(--fg, inherit);
border: 1px solid var(--line); border-radius: 8px;
font-size: .85rem; line-height: 1.35;
box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
}
.wheel-tip[hidden] { display: none; }
form { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; } form { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 1.25rem; }
.row { display: flex; gap: .5rem; } .row { display: flex; gap: .5rem; }
.row input[type=text] { flex: 1; } .row input[type=text] { flex: 1; }
@@ -0,0 +1,97 @@
// Dymki na kosmogramie — własne, zamiast natywnych z <title> (PRE-12/PRE-25).
//
// DLACZEGO NIE ZOSTAWIAMY NATYWNYCH. Rysunek niesie <title> przy każdym obiekcie
// i każdej linii aspektu i to wystarczało, dopóki koło było statyczne. Po dodaniu
// powiększania na pełne okno dymki przestały się pokazywać — a niezależnie od tego,
// która reguła przeglądarki je tłumi, natywny dymek i tak jest tu kiepskim
// narzędziem: pojawia się po sekundzie zwłoki, nie da się go stylować, nie działa
// na dotyku i nie ma go czym wywołać z klawiatury.
//
// Sprawdzone w przeglądarce (1280×900, komplet 13 systemów): elementy z <title>
// SĄ osiągalne kursorem — 19 z 20 trafień w hit-teście — więc nic ich nie zasłania
// i problem nie leży w geometrii ani w pointer-events.
//
// <title> ZOSTAJE w rysunku: to nazwa dostępna dla czytników ekranu, a PDF składa
// się po stronie serwera, więc ta ścieżka jest nietknięta. Na czas najechania
// odpinamy go i wieszamy z powrotem po zejściu kursora — dzięki temu nigdy nie
// widać dwóch dymków naraz, gdyby przeglądarka jednak swój pokazała.
(function () {
'use strict';
var OFFSET = 14; // odsunięcie od kursora, żeby dymek nie siedział pod nim
var stash = null; // { owner, node } — odpięty <title> do przywieszenia
function ready(fn) {
if (document.readyState !== 'loading') fn();
else document.addEventListener('DOMContentLoaded', fn);
}
ready(function () {
var wheels = document.querySelectorAll('svg.wheel');
if (!wheels.length) return;
var tip = document.createElement('div');
tip.className = 'wheel-tip';
tip.setAttribute('role', 'tooltip');
tip.hidden = true;
document.body.appendChild(tip);
function restore() {
if (!stash) return;
stash.owner.appendChild(stash.node);
stash = null;
}
function hide() {
tip.hidden = true;
restore();
}
// Element z własnym <title>, licząc od celu zdarzenia w górę. Aspekty to
// cienkie linie, a glify siedzą w <text> — oba trzymają <title> jako dziecko.
function owner(el, root) {
while (el && el !== root) {
if (el.querySelector && el.querySelector(':scope > title')) return el;
el = el.parentNode;
}
return null;
}
function place(x, y) {
// Ustawiamy najpierw, potem mierzymy — inaczej nie znamy rozmiaru dymka.
tip.style.left = '0px';
tip.style.top = '0px';
var w = tip.offsetWidth, h = tip.offsetHeight;
var left = x + OFFSET, top = y + OFFSET;
if (left + w > window.innerWidth - 4) left = x - OFFSET - w; // odbij w lewo
if (top + h > window.innerHeight - 4) top = y - OFFSET - h; // odbij w górę
tip.style.left = Math.max(4, left) + 'px';
tip.style.top = Math.max(4, top) + 'px';
}
Array.prototype.forEach.call(wheels, function (svg) {
svg.addEventListener('mousemove', function (e) {
var el = owner(e.target, svg);
if (!el) { hide(); return; }
if (!stash || stash.owner !== el) {
restore();
var node = el.querySelector(':scope > title');
tip.textContent = node.textContent;
stash = { owner: el, node: node };
el.removeChild(node);
tip.hidden = false;
}
place(e.clientX, e.clientY);
});
svg.addEventListener('mouseleave', hide);
});
// Dymek nie może przeżyć przewinięcia ani powiększenia — zostałby wiszący
// w miejscu, w którym nic już nie ma.
window.addEventListener('scroll', hide, true);
window.addEventListener('resize', hide);
document.addEventListener('click', hide, true);
});
})();
@@ -13,6 +13,7 @@
{# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na {# Powiększanie kosmogramu (PRE-25) — globalnie, bo koło pojawi się też na
zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #} zakładce „Skompiluj"; skrypt sam sprawdza, czy jest co powiększać. #}
<script defer src="{{ static('wheelzoom.js') }}"></script> <script defer src="{{ static('wheelzoom.js') }}"></script>
<script defer src="{{ static('wheeltip.js') }}"></script>
</head> </head>
<body> <body>
<main> <main>
@@ -0,0 +1,72 @@
"""Dymki na kosmogramie (PRE-12/PRE-25).
Rysunek niesie <title> przy każdym obiekcie i każdej linii aspektu, ale natywne
dymki przeglądarki przestały się pokazywać po dodaniu powiększania na pełne okno.
Sprawdzone w przeglądarce: elementy z <title> SĄ osiągalne kursorem (19 z 20 trafień
w hit-teście), więc nic ich nie zasłaniało — problem był w samym natywnym dymku.
Test strukturalny, jak przy wheelzoom: bez uruchamiania przeglądarki nie zbadamy
zachowania JS-a. Pilnujemy więc tego, co da się zepsuć po cichu i co ma znaczenie:
kolejności warstw, zdejmowania i ODWIESZANIA <title> oraz wpięcia skryptu.
Zachowanie sprawdzone ręcznie w przeglądarce przy 1280×900, w obu stanach koła.
"""
import pathlib
import re
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
JS = (APP / "static" / "wheeltip.js").read_text(encoding="utf-8")
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
def test_script_is_loaded_globally():
"""W base.html, bo koło jest też na zakładce „Skompiluj"."""
assert "wheeltip.js" in BASE
def test_tooltip_sits_above_the_zoom_overlay_but_below_the_progress_window():
"""Warstwy: nakładka powiększonego koła < dymek < okno postępu.
Dymek MUSI być nad nakładką, inaczej po powiększeniu — czyli dokładnie tam,
gdzie zgłoszono problem — byłby niewidoczny. I POD oknem postępu, bo gdy trwa
pisanie horoskopu, log operacji ma zostać na wierzchu."""
tip = re.search(r"\.wheel-tip\s*\{[^}]*z-index:\s*(\d+)", CSS)
zoom = re.search(r"\.wheel-fig\.zoomed\s*\{[^}]*z-index:\s*(\d+)", CSS)
progress = re.search(r"\.progress-overlay\s*\{[^}]*z-index:\s*(\d+)", CSS)
assert tip and zoom and progress, "brakuje którejś reguły z-index"
assert int(zoom.group(1)) < int(tip.group(1)) < int(progress.group(1)), \
f"warstwy: nakładka {zoom.group(1)}, dymek {tip.group(1)}, postęp {progress.group(1)}"
def test_tooltip_never_swallows_the_pointer():
"""Dymek chodzi za kursorem, więc bez pointer-events:none zasłaniałby sam
sobie element, nad którym stoi — i migotał."""
rule = re.search(r"\.wheel-tip\s*\{([^}]*)\}", CSS)
assert rule and "pointer-events: none" in rule.group(1)
def test_title_is_detached_only_for_the_hover_and_always_put_back():
"""<title> zostaje w rysunku — to nazwa dostępna dla czytników ekranu.
Na czas najechania odpinamy go, żeby nie było dwóch dymków naraz, ale MUSI
wrócić. Brak odwieszania okradałby czytniki ekranu po pierwszym najechaniu,
i to po cichu — na ekranie nic by nie było widać."""
assert "removeChild" in JS, "dymek natywny nie jest tłumiony"
assert "appendChild(stash.node)" in JS, "brak odwieszania <title>"
for leaving in ("mouseleave", "scroll", "resize"):
assert leaving in JS, f"brak sprzątania przy: {leaving}"
def test_tooltip_reads_the_title_that_is_already_in_the_drawing():
"""Źródłem tekstu jest <title> z SVG, nie druga kopia opisów w JS —
inaczej rozjechałyby się przy pierwszej zmianie treści dymka."""
assert ":scope > title" in JS
assert "textContent" in JS
def test_pdf_path_is_untouched():
"""PDF składa się po stronie serwera, więc żaden dymek go nie dotyczy —
<title> zostają w rysunku i nic w chartwheel.py się nie zmienia."""
wheel = (APP / "chartwheel.py").read_text(encoding="utf-8")
assert "<title>" in wheel
assert "wheeltip" not in wheel