chore: post-review hardening + changelog link tooltip

Security / robustness / a11y polish on top of the color-search + tooltips work:

- URL opening: route through validated backend commands (open_map_location
  with lat/lon bounds-checking, open_changelog_url with a fixed URL) instead
  of the frontend opener:allow-open-url capability, which is now removed.
- EXIF GPS parsing: validate coordinate ranges and require the correct
  N/S/E/W hemisphere ref byte, then clamp.
- Guard double-submit on album create / add-to-album (Lightbox, BulkActionBar,
  Sidebar) and discard stale autocomplete responses in the bulk tag editor.
- Gallery tile: stop nesting <button>s — non-interactive tile div with an
  overlay button for open/toggle; checkbox and Similar promoted with z-index
  + focus rings.
- Accessibility: keyboard handlers, focus-visible rings, and aria on album
  rows, tag-manage actions, and tile controls; Tooltip uses a block <div>
  wrapper in block mode and aria-hidden when hidden.
- Show the destination URL in a tooltip on the "Full changelog" link so the
  user can see where it goes before clicking.
- Toolbar "All" filter also clears the color filter; color-filtered views no
  longer get unfiltered newly-indexed images injected.
- Sidebar reorder: bail if the album set changed mid-drag.
- Tooling: add cargo fmt scripts; alphabetize package.json scripts.
This commit is contained in:
2026-06-28 14:17:58 +01:00
parent 90dec3b212
commit bb0038e0a1
14 changed files with 211 additions and 78 deletions
+8 -4
View File
@@ -77,7 +77,7 @@ export function Tooltip({
if (frame.current !== undefined) cancelAnimationFrame(frame.current);
frame.current = undefined;
};
const show = (event: MouseEvent<HTMLSpanElement>) => {
const show = (event: MouseEvent<HTMLElement>) => {
clear();
if (followCursor) place(event.clientX, event.clientY);
if (delay <= 0) {
@@ -90,7 +90,7 @@ export function Tooltip({
clear();
setVisible(false);
};
const move = (event: MouseEvent<HTMLSpanElement>) => {
const move = (event: MouseEvent<HTMLElement>) => {
if (!followCursor) return;
const { clientX, clientY } = event;
if (frame.current !== undefined) cancelAnimationFrame(frame.current);
@@ -102,8 +102,10 @@ export function Tooltip({
useEffect(() => clear, []);
const Wrapper = block ? "div" : "span";
return (
<span
<Wrapper
className={`${block ? "relative block w-full" : "relative inline-flex"} ${className}`}
onMouseEnter={show}
onMouseMove={followCursor ? move : undefined}
@@ -115,6 +117,7 @@ export function Tooltip({
<motion.span
ref={tooltipRef}
role="tooltip"
aria-hidden={!visible}
// Fixed (so the scroll container's overflow doesn't clip it) and
// positioned by `place()` near the cursor with viewport-edge flipping.
className={`fixed ${BASE_CLASSES}`}
@@ -131,11 +134,12 @@ export function Tooltip({
) : (
<span
role="tooltip"
aria-hidden={!visible}
className={`absolute ${STATIC_CLASSES} ${positionClasses(side, align)} ${visible ? "opacity-100" : "opacity-0"}`}
>
{label}
</span>
)}
</span>
</Wrapper>
);
}