hensei-web/docs/transcendence-popover-fix.md

234 lines
No EOL
6.2 KiB
Markdown

# Transcendence Star Popover Fix - Implementation Plan
## Problem Statement
The TranscendenceStar component's popover interface has three critical issues:
1. **Z-index layering issue**: The popover (z-index: 100) appears below weapon images and other UI elements
2. **Overflow clipping**: The parent container `.page-wrap` has `overflow-x: auto` which clips the popover
3. **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 `isPopoverOpen` to 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
```typescript
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
```typescript
// New state variables
let popoverPosition = $state<PopoverPosition | null>(null);
let popoverElement: HTMLDivElement;
```
#### Position Update Effect
```typescript
$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
```svelte
{#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
```scss
.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
1. **Install/verify bits-ui Portal availability**
- Check if Portal is exported from bits-ui
- If not available, create custom portal implementation
2. **Add positioning logic**
- Create calculatePopoverPosition function
- Add position state management
- Add scroll/resize listeners
3. **Update template**
- Wrap popover in Portal component
- Apply dynamic positioning styles
- Add placement classes
4. **Update styles**
- Change to position: fixed
- Increase z-index to 1001
- Add animation for smooth appearance
- Handle above/below placement variants
5. **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/`