fix(ui): keep toasts clear of the right hand panes

Toasts are anchored to the top right of the viewport, so whenever a right
hand pane is open they render on top of it, covering its header, its close
button and the first row of controls. Reaching a control meant waiting for
the toast to expire or dismissing it first.

Each right hand pane now publishes its rendered width to --right-pane-width
on the document element, following the same convention the sidebar already
uses for --sidebar-width, and the toast container is inset by that amount.
The three panes are the chat controls pane, the channel thread pane and the
notes panel. The width comes from a ResizeObserver on the pane element
itself, so it tracks resizer drags and window resizes, reads 0 while a pane
is collapsed, and is cleared when the pane goes away. Only the most recent
pane owns the property, so navigating between routes cannot leave a stale
inset behind.

Toast position is unchanged. With no pane open the rule resolves to the same
32px offset svelte-sonner already applies, and it is gated above 600px so
the library's full width mobile layout is untouched. If a pane is ever wide
enough that a toast cannot fit beside it, the inset stops growing so the
toast stays fully on screen instead of sliding off the left edge.
This commit is contained in:
silentoplayz 2026-08-07 17:09:35 -04:00
parent 8faaf2cd1e
commit 77aab7d49d
5 changed files with 86 additions and 1 deletions

View file

@ -918,3 +918,15 @@ html.high-contrast:not(.dark) .shimmer {
color: var(--color-gray-700);
-webkit-text-fill-color: var(--color-gray-700);
}
@media (min-width: 601px) {
[data-sonner-toaster][data-x-position='right'] {
right: max(
env(safe-area-inset-right),
min(
calc(var(--offset) + var(--right-pane-width, 0px)),
calc(100vw - var(--width) - var(--offset))
)
);
}
}

View file

@ -15,6 +15,7 @@
user
} from '$lib/stores';
import { getChannelById, getChannelMessages, sendMessage } from '$lib/apis/channels';
import { trackRightPaneWidth } from '$lib/utils/rightPane';
import Messages from './Messages.svelte';
import MessageInput from './MessageInput.svelte';
@ -266,6 +267,16 @@
let mediaQuery;
let largeScreen = false;
let threadPaneEl: HTMLElement | null = null;
let untrackPaneWidth: (() => void) | null = null;
const trackPaneWidth = (el: HTMLElement | null) => {
untrackPaneWidth?.();
untrackPaneWidth = el ? trackRightPaneWidth(el) : null;
};
$: trackPaneWidth(threadPaneEl);
onMount(() => {
if ($chatId) {
chatId.set('');
@ -292,6 +303,8 @@
updateLastReadAt(id);
_channelId.set(null);
$socket?.off('events:channel', channelEventHandler);
untrackPaneWidth?.();
untrackPaneWidth = null;
});
</script>
@ -439,7 +452,7 @@
/>
</PaneResizer>
<Pane defaultSize={50} minSize={30} class="h-full w-full">
<Pane bind:el={threadPaneEl} defaultSize={50} minSize={30} class="h-full w-full">
<div class="h-full w-full shadow-xl">
<Thread
{threadId}

View file

@ -24,6 +24,7 @@
} from '$lib/stores';
import { uploadFile } from '$lib/apis/files';
import { trackRightPaneWidth } from '$lib/utils/rightPane';
import { toast } from 'svelte-sonner';
import Controls from './Controls/Controls.svelte';
@ -62,6 +63,16 @@
let minSize = 0;
let paneReady = false;
let paneEl: HTMLElement | null = null;
let untrackPaneWidth: (() => void) | null = null;
const trackPaneWidth = (el: HTMLElement | null) => {
untrackPaneWidth?.();
untrackPaneWidth = el ? trackRightPaneWidth(el) : null;
};
$: trackPaneWidth(paneEl);
// Tab state for Controls+Files panel
let activeTab = savedTab;
// svelte-ignore reactive_declaration_module_script_dependency
@ -251,6 +262,8 @@
isDestroyed = true;
paneReady = false;
resizeObserver?.disconnect();
untrackPaneWidth?.();
untrackPaneWidth = null;
if (!largeScreen) {
showControls.set(false);
}
@ -401,6 +414,7 @@
<Pane
bind:pane
bind:el={paneEl}
defaultSize={0}
onResize={(size) => {
if ($showControls && pane.isExpanded()) {

View file

@ -3,6 +3,7 @@
import { Pane, PaneResizer } from 'paneforge';
import Drawer from '../common/Drawer.svelte';
import { trackRightPaneWidth } from '$lib/utils/rightPane';
export let show = false;
export let pane = null;
@ -14,6 +15,16 @@
let minSize = 0;
let paneEl: HTMLElement | null = null;
let untrackPaneWidth: (() => void) | null = null;
const trackPaneWidth = (el: HTMLElement | null) => {
untrackPaneWidth?.();
untrackPaneWidth = el ? trackRightPaneWidth(el) : null;
};
$: trackPaneWidth(paneEl);
const handleMediaQuery = async (e) => {
if (e.matches) {
largeScreen = true;
@ -58,6 +69,8 @@
onDestroy(() => {
mediaQuery.removeEventListener('change', handleMediaQuery);
untrackPaneWidth?.();
untrackPaneWidth = null;
});
</script>
@ -86,6 +99,7 @@
<Pane
bind:pane
bind:el={paneEl}
defaultSize={Math.max(20, minSize)}
{minSize}
onCollapse={() => {

View file

@ -0,0 +1,32 @@
const CSS_VARIABLE = '--right-pane-width';
let owner: HTMLElement | null = null;
export const trackRightPaneWidth = (el: HTMLElement) => {
owner = el;
const publish = (width: number) => {
if (owner !== el) {
return;
}
document.documentElement.style.setProperty(CSS_VARIABLE, `${width}px`);
};
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
publish(entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width);
}
});
observer.observe(el);
publish(el.offsetWidth);
return () => {
observer.disconnect();
if (owner === el) {
owner = null;
document.documentElement.style.removeProperty(CSS_VARIABLE);
}
};
};