6.2 KiB
6.2 KiB
Transcendence Star Popover Fix - Implementation Plan
Problem Statement
The TranscendenceStar component's popover interface has three critical issues:
- Z-index layering issue: The popover (z-index: 100) appears below weapon images and other UI elements
- Overflow clipping: The parent container
.page-wraphasoverflow-x: autowhich clips the popover - Viewport positioning: The popover can appear partially off-screen when the star is near the bottom of the viewport
Current Implementation Analysis
File Structure
- Component:
/src/lib/components/uncap/TranscendenceStar.svelte - Fragment:
/src/lib/components/uncap/TranscendenceFragment.svelte
Current Approach
- Popover is rendered as a child div with
position: absolute - Uses local state
isPopoverOpento control visibility - Z-index set to 100 (below tooltips at 1000)
- No viewport edge detection or smart positioning
Solution Architecture
1. Portal-Based Rendering
Use bits-ui Portal component to render the popover outside the DOM hierarchy, avoiding overflow clipping.
Benefits:
- Escapes any parent overflow constraints
- Maintains React-like portal behavior
- Already proven pattern in Dialog.svelte
2. Z-index Hierarchy Management
Current z-index levels in codebase:
- Tooltips: 1000
- Navigation/Side panels: 50
- Fragments: 32
- Current popover: 100
Solution: Set popover z-index to 1001 (above tooltips)
3. Smart Positioning System
Position Calculation Algorithm
interface PopoverPosition {
top: number;
left: number;
placement: 'above' | 'below';
}
function calculatePopoverPosition(
starElement: HTMLElement,
popoverWidth: number = 80,
popoverHeight: number = 100
): PopoverPosition {
const rect = starElement.getBoundingClientRect();
const viewport = {
width: window.innerWidth,
height: window.innerHeight
};
// Calculate available space
const spaceBelow = viewport.height - rect.bottom;
const spaceAbove = rect.top;
const spaceRight = viewport.width - rect.right;
const spaceLeft = rect.left;
// Determine vertical placement
const placement = spaceBelow < popoverHeight && spaceAbove > spaceBelow
? 'above'
: 'below';
// Calculate position
let top = placement === 'below'
? rect.bottom + 8 // 8px gap
: rect.top - popoverHeight - 8;
// Center horizontally on star
let left = rect.left + (rect.width / 2) - (popoverWidth / 2);
// Adjust horizontal position if too close to edges
if (left < 8) {
left = 8; // 8px from left edge
} else if (left + popoverWidth > viewport.width - 8) {
left = viewport.width - popoverWidth - 8; // 8px from right edge
}
return { top, left, placement };
}
4. Implementation Details
State Management
// New state variables
let popoverPosition = $state<PopoverPosition | null>(null);
let popoverElement: HTMLDivElement;
Position Update Effect
$effect(() => {
if (isPopoverOpen && starElement) {
const updatePosition = () => {
popoverPosition = calculatePopoverPosition(starElement);
};
// Initial position
updatePosition();
// Update on scroll/resize
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
}
});
Template Structure
{#if interactive && isPopoverOpen && popoverPosition}
<Portal>
<div
class="popover"
class:above={popoverPosition.placement === 'above'}
style="top: {popoverPosition.top}px; left: {popoverPosition.left}px"
bind:this={popoverElement}
>
<div class="fragments">
<!-- existing fragment content -->
</div>
<div class="level">
<!-- existing level display -->
</div>
</div>
</Portal>
{/if}
Style Updates
.popover {
position: fixed;
z-index: 1001;
// Remove static positioning
// top: -10px; (remove)
// left: -10px; (remove)
// Add placement variants
&.above {
// Arrow or visual indicator for above placement
}
// Smooth appearance
animation: popover-appear 0.2s ease-out;
}
@keyframes popover-appear {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
Implementation Steps
-
Install/verify bits-ui Portal availability
- Check if Portal is exported from bits-ui
- If not available, create custom portal implementation
-
Add positioning logic
- Create calculatePopoverPosition function
- Add position state management
- Add scroll/resize listeners
-
Update template
- Wrap popover in Portal component
- Apply dynamic positioning styles
- Add placement classes
-
Update styles
- Change to position: fixed
- Increase z-index to 1001
- Add animation for smooth appearance
- Handle above/below placement variants
-
Testing
- Test near all viewport edges
- Test with scrolling
- Test with window resize
- Verify z-index layering
- Confirm no overflow clipping
Alternative Approaches Considered
Floating UI Library
- Pros: Robust positioning, automatic flipping, virtual element support
- Cons: Additional dependency, may be overkill for simple use case
- Decision: Start with custom implementation, can migrate if needed
Tooltip Component Reuse
- Pros: Consistent behavior with existing tooltips
- Cons: Tooltips likely simpler, may not support interactive content
- Decision: Custom implementation for specific transcendence needs
Success Criteria
- Popover appears above all other UI elements
- No clipping by parent containers
- Smart positioning avoids viewport edges
- Smooth transitions and animations
- Click outside properly closes popover
- Position updates on scroll/resize
- Works on all screen sizes
References
- Current implementation:
/src/lib/components/uncap/TranscendenceStar.svelte - Portal example:
/src/lib/components/ui/Dialog.svelte - Original Next.js version:
/hensei-web/components/uncap/TranscendencePopover/