A dialog in a short window scrolls within it, the tabbed one stays put as its tabs change, and the report slip closes with Escape

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-29 12:21:27 -04:00
co-authored by Claude Opus 5.5
parent 8ba02006ca
commit f131f292ae
3 changed files with 19 additions and 6 deletions
@@ -48,6 +48,8 @@
z-index: 20; z-index: 20;
background: rgba(0, 0, 0, 0.55); background: rgba(0, 0, 0, 0.55);
display: grid; display: grid;
/* One row the window's height, so the dialog's max-height: 100% is a real limit and it scrolls within. */
grid-template-rows: minmax(0, 1fr);
place-items: center; place-items: center;
padding: 16px; padding: 16px;
} }
@@ -54,6 +54,8 @@
} }
</script> </script>
<svelte:window onkeydown={(e) => open && e.key === 'Escape' && close()} />
{#if open} {#if open}
<div class="scrim" role="presentation" onclick={(ev) => ev.target === ev.currentTarget && close()}> <div class="scrim" role="presentation" onclick={(ev) => ev.target === ev.currentTarget && close()}>
<form class="slip" aria-labelledby="report-heading" onsubmit={send}> <form class="slip" aria-labelledby="report-heading" onsubmit={send}>
@@ -92,6 +94,8 @@
z-index: 20; z-index: 20;
background: rgba(0, 0, 0, 0.55); background: rgba(0, 0, 0, 0.55);
display: grid; display: grid;
/* One row the window's height, so the dialog's max-height: 100% is a real limit and it scrolls within. */
grid-template-rows: minmax(0, 1fr);
place-items: center; place-items: center;
padding: 16px; padding: 16px;
} }
@@ -20,8 +20,8 @@
let current = $state(''); let current = $state('');
// The router holds the address, and the dialog is open exactly while the address names it. // The dialog is open exactly while the router's address names it. Opening, choosing a tab and
// Every change goes through the router too, so it never loses track of where the page is. // closing all navigate, so the address and the dialog cannot disagree.
const [named, tab] = $derived.by(() => { const [named, tab] = $derived.by(() => {
const [name, t] = page.url.hash.slice(1).split('/'); const [name, t] = page.url.hash.slice(1).split('/');
return [name === address, t] as const; return [name === address, t] as const;
@@ -34,10 +34,10 @@
}); });
// The address follows the chosen tab, so what is being read can be linked to. // The address follows the chosen tab, so what is being read can be linked to.
$effect(() => { $effect(() => {
if (named && current && current !== untrack(() => tab)) show(current); if (named && current) untrack(() => current !== tab && show(current));
}); });
const bare = () => `${location.pathname}${location.search}`; const bare = () => `${page.url.pathname}${page.url.search}`;
const quietly = { replaceState: true, noScroll: true, keepFocus: true }; const quietly = { replaceState: true, noScroll: true, keepFocus: true };
function show(id: string) { function show(id: string) {
goto(`${bare()}#${address}/${id}`, quietly); goto(`${bare()}#${address}/${id}`, quietly);
@@ -54,7 +54,7 @@
<svelte:window onkeydown={onKey} /> <svelte:window onkeydown={onKey} />
{#if named} {#if named}
<div class="scrim" role="presentation" onclick={(ev) => ev.target === ev.currentTarget && close()}> <div class="scrim anchored" role="presentation" onclick={(ev) => ev.target === ev.currentTarget && close()}>
<div class="panel" role="dialog" aria-modal="true" aria-labelledby="{address}-title"> <div class="panel" role="dialog" aria-modal="true" aria-labelledby="{address}-title">
<header> <header>
<h2 id="{address}-title">{title}</h2> <h2 id="{address}-title">{title}</h2>
@@ -76,10 +76,17 @@
z-index: 20; z-index: 20;
background: rgba(0, 0, 0, 0.55); background: rgba(0, 0, 0, 0.55);
display: grid; display: grid;
/* One row the window's height, so the dialog's max-height: 100% is a real limit and it scrolls within. */
grid-template-rows: minmax(0, 1fr);
place-items: center; place-items: center;
padding: 16px; padding: 16px;
} }
/* min-width 0 all the way down, so wide content (the notation's table) scrolls in place instead of widening the dialog past a phone's screen. */ /* Held near the top, not centered: a tab of another length must not move the tabs under the pointer. */
.anchored {
place-items: start center;
padding-top: max(16px, 6vh);
}
/* min-width 0 all the way down, so wide content scrolls in place instead of widening the dialog past a phone's screen. */
.panel { .panel {
width: min(100%, 44rem); width: min(100%, 44rem);
min-width: 0; min-width: 0;