fix: mobile modal positioning to use full screen (#607)

* fix: mobile modal positioning to use full screen

On mobile, modals were getting cut off due to Safari sticky+fixed
positioning issues. Changed from center-based positioning to inset-based
(top/left/right/bottom) to use the full available screen space.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: use svelte-portal for modal positioning

the header's backdrop-filter creates a CSS containing block, which
causes position:fixed modals to be positioned relative to the header
instead of the viewport. use svelte-portal to render modals directly
on document.body, preserving proper centering.

- add svelte-portal dependency
- apply use:portal={'body'} to LinksMenu and ProfileMenu modals
- add docs/frontend/portals.md documenting this pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

authored by zzstoatzz.io Claude Opus 4.5 and committed by GitHub 425372b9 aa17bcab

Changed files
+48 -5
docs
frontend
frontend
+40
docs/frontend/portals.md
··· 1 + # portals (rendering modals outside parent DOM) 2 + 3 + ## the problem 4 + 5 + when a modal is rendered inside an element with `backdrop-filter`, `transform`, or `filter`, the modal's `position: fixed` becomes relative to that ancestor instead of the viewport. this causes modals to be positioned incorrectly (e.g., appearing off-screen). 6 + 7 + the header uses `backdrop-filter` for the glass blur effect, so any modal rendered inside the header will not center properly on the viewport. 8 + 9 + ## the solution 10 + 11 + use `svelte-portal` to render modal content directly on `<body>`, outside the parent DOM hierarchy. 12 + 13 + ```bash 14 + bun add svelte-portal 15 + ``` 16 + 17 + ```svelte 18 + <script> 19 + import { portal } from 'svelte-portal'; 20 + </script> 21 + 22 + <div class="menu-backdrop" use:portal={'body'} onclick={close}></div> 23 + <div class="menu-popover" use:portal={'body'}> 24 + <!-- modal content --> 25 + </div> 26 + ``` 27 + 28 + the `use:portal={'body'}` action moves the element to `document.body` while preserving all svelte reactivity, bindings, and event handlers. 29 + 30 + ## when to use 31 + 32 + use portals for any fixed-position overlay (modals, dropdowns, tooltips) that might be rendered inside: 33 + - elements with `backdrop-filter` (glass effects) 34 + - elements with `transform` 35 + - elements with `filter` 36 + - `position: sticky` containers (in some browsers) 37 + 38 + ## reference 39 + 40 + - [svelte-portal on GitHub](https://github.com/romkor/svelte-portal)
frontend/bun.lockb

This is a binary file and will not be displayed.

+2 -1
frontend/package.json
··· 29 29 "vite": "^7.1.7" 30 30 }, 31 31 "dependencies": { 32 - "@atproto/api": "^0.18.7" 32 + "@atproto/api": "^0.18.7", 33 + "svelte-portal": "^2.2.1" 33 34 } 34 35 }
+3 -2
frontend/src/lib/components/LinksMenu.svelte
··· 1 1 <script lang="ts"> 2 + import { portal } from 'svelte-portal'; 2 3 import PlatformStats from './PlatformStats.svelte'; 3 4 4 5 let showMenu = $state(false); ··· 33 34 {#if showMenu} 34 35 <!-- svelte-ignore a11y_click_events_have_key_events --> 35 36 <!-- svelte-ignore a11y_no_static_element_interactions --> 36 - <div class="menu-backdrop" onclick={closeMenu}></div> 37 - <div class="menu-popover"> 37 + <div class="menu-backdrop" use:portal={'body'} onclick={closeMenu}></div> 38 + <div class="menu-popover" use:portal={'body'}> 38 39 <div class="menu-header"> 39 40 <span>links</span> 40 41 <button
+3 -2
frontend/src/lib/components/ProfileMenu.svelte
··· 1 1 <script lang="ts"> 2 + import { portal } from 'svelte-portal'; 2 3 import { onMount } from 'svelte'; 3 4 import { page } from '$app/stores'; 4 5 import { queue } from '$lib/queue.svelte'; ··· 118 119 {#if showMenu} 119 120 <!-- svelte-ignore a11y_click_events_have_key_events --> 120 121 <!-- svelte-ignore a11y_no_static_element_interactions --> 121 - <div class="menu-backdrop" onclick={closeMenu}></div> 122 - <div class="menu-popover"> 122 + <div class="menu-backdrop" use:portal={'body'} onclick={closeMenu}></div> 123 + <div class="menu-popover" use:portal={'body'}> 123 124 <div class="menu-header"> 124 125 <span>{showSettings ? 'settings' : 'menu'}</span> 125 126 <button class="close-btn" onclick={closeMenu} aria-label="close">