Files
the-sunken-reach/frontend/src/components/JumpNav.tsx
T
joshuaandClaude Opus 5.5 2dcb41ae4a V1.10.1: a phone-friendly layout, easier navigation, paged encounters
- The main destinations are always on screen: Campaigns, Campaign and
  Party (for the campaign last visited, from any page) and Account. They
  are links along the top bar on a wide screen, and a bar along the
  bottom on a phone or small tablet (900px or less).
- Nothing floats over the page on a phone: the dice roller is the last
  button of the bottom bar, and quick stats sits in the top bar.
- The campaign page and the character sheet start with a row of links to
  their sections.
- On a small screen every table is a stack of labelled cards, rows of
  controls wrap, fields shrink to fit, and things to tap are bigger.
- The encounter list is paged, ten to a page, as is the archived list.
  Moving an encounter still works across pages.
- Text boxes with no type given, and native checkboxes and dropdowns, now
  match the dark theme. Long monster notes and item descriptions wrap
  inside their table cell.
- All packages at version 1.10.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:37:31 +02:00

37 lines
1.3 KiB
TypeScript

import { useEffect, useState } from 'react';
// A row of links to the sections of a long page. It finds them itself: any
// element with a `data-jump="Label"` attribute that is on the page right now
// gets a link, so sections only some people see need no special handling.
export function JumpNav() {
const [labels, setLabels] = useState<string[]>([]);
// Sections come and go as data loads, so the page is watched for them.
useEffect(() => {
const look = () => {
const found = [...document.querySelectorAll<HTMLElement>('[data-jump]')].map((el) => el.dataset.jump ?? '');
setLabels((current) => (current.join('|') === found.join('|') ? current : found));
};
const observer = new MutationObserver(look);
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-jump'] });
look();
return () => observer.disconnect();
}, []);
if (labels.length < 3) return null;
return (
<nav className="jump-nav" aria-label="Sections of this page">
{labels.map((label) => (
<button
key={label}
type="button"
className="jump-link"
onClick={() => document.querySelector(`[data-jump="${label}"]`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })}
>
{label}
</button>
))}
</nav>
);
}