fix transcendence star positioning and update uncap styles

This commit is contained in:
Justin Edmund 2025-09-25 00:26:29 -07:00
parent 6762c2dab4
commit d13412dfb9
2 changed files with 180 additions and 14 deletions

View file

@ -2,10 +2,12 @@
<script lang="ts">
import TranscendenceFragment from './TranscendenceFragment.svelte'
import { Portal } from 'bits-ui'
interface Props {
className?: string
stage?: number
type?: 'character' | 'weapon' | 'summon'
editable?: boolean
interactive?: boolean
tabIndex?: number
@ -17,6 +19,7 @@
let {
className,
stage = 0,
type = 'character',
editable = false,
interactive = false,
tabIndex,
@ -27,19 +30,100 @@
const NUM_FRAGMENTS = 5
interface PopoverPosition {
top: number
left: number
placement: 'above' | 'below'
}
let visibleStage = $state(stage)
let currentStage = $state(stage)
let immutable = $state(false)
let isPopoverOpen = $state(false)
let popoverPosition = $state<PopoverPosition | null>(null)
let starElement: HTMLDivElement
let popoverElement: HTMLDivElement
const baseLevel = $derived(type === 'character' ? 100 : 200)
const displayLevel = $derived(baseLevel + 10 * visibleStage)
function calculatePopoverPosition(): PopoverPosition | null {
if (!starElement) return null
const rect = starElement.getBoundingClientRect()
const popoverWidth = 100 // Approximate width
const popoverHeight = 120 // Approximate height
const gap = 8 // Gap between star and popover
// Calculate available space
const spaceBelow = window.innerHeight - rect.bottom
const spaceAbove = rect.top
// Determine vertical placement
const placement: 'above' | 'below' =
spaceBelow < popoverHeight && spaceAbove > spaceBelow ? 'above' : 'below'
// Calculate vertical position
let top = placement === 'below' ? rect.bottom + gap : rect.top - popoverHeight - gap
// Center horizontally on star
let left = rect.left + rect.width / 2 - popoverWidth / 2
// Adjust horizontal position if too close to edges
const edgeMargin = 8
if (left < edgeMargin) {
left = edgeMargin
} else if (left + popoverWidth > window.innerWidth - edgeMargin) {
left = window.innerWidth - popoverWidth - edgeMargin
}
return { top, left, placement }
}
$effect(() => {
visibleStage = stage
currentStage = stage
})
$effect(() => {
if (isPopoverOpen) {
// Update position when popover opens
popoverPosition = calculatePopoverPosition()
const handleClickOutside = (event: MouseEvent) => {
if (
starElement &&
!starElement.contains(event.target as Node) &&
popoverElement &&
!popoverElement.contains(event.target as Node)
) {
isPopoverOpen = false
popoverPosition = null
}
}
const updatePosition = () => {
popoverPosition = calculatePopoverPosition()
}
// Add listeners
document.addEventListener('click', handleClickOutside)
window.addEventListener('scroll', updatePosition, true)
window.addEventListener('resize', updatePosition)
return () => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('scroll', updatePosition, true)
window.removeEventListener('resize', updatePosition)
}
}
})
function handleClick() {
if (editable && onStarClick) {
onStarClick()
} else if (interactive) {
isPopoverOpen = !isPopoverOpen
}
}
@ -54,6 +138,7 @@
if (onFragmentClick) {
onFragmentClick(newStage)
}
isPopoverOpen = false
}
function handleFragmentHover(index: number) {
@ -87,25 +172,39 @@
role={editable ? 'button' : undefined}
aria-label={editable ? 'Transcendence star' : undefined}
>
<div class="fragments">
{#if interactive}
{#each Array(NUM_FRAGMENTS) as _, i}
{@const loopStage = i + 1}
<TranscendenceFragment
stage={loopStage}
visible={loopStage <= visibleStage}
{interactive}
onClick={handleFragmentClick}
onHover={handleFragmentHover}
/>
{/each}
{/if}
</div>
{#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">
{#each Array(NUM_FRAGMENTS) as _, i}
{@const loopStage = i + 1}
<TranscendenceFragment
stage={loopStage}
visible={loopStage <= visibleStage}
{interactive}
onClick={handleFragmentClick}
onHover={handleFragmentHover}
/>
{/each}
</div>
<div class="level">
<span>Level</span>
<span class="level-value" class:pending={visibleStage !== currentStage}>{displayLevel}</span>
</div>
</div>
</Portal>
{/if}
<i class="figure {className || ''}" class:interactive class:base={className?.includes('base')} />
</div>
<style lang="scss">
@use '$src/themes/spacing' as spacing;
@use '$src/themes/typography';
.star {
--size: 18px;
@ -234,4 +333,70 @@
}
}
}
.popover {
position: fixed;
z-index: 1001;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
padding: 12px;
width: auto;
min-width: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
animation: popover-appear 0.2s ease-out;
&.above {
animation: popover-appear-above 0.2s ease-out;
}
.fragments {
position: relative;
width: 48px;
height: 48px;
}
.level {
font-size: typography.$font-small;
text-align: center;
white-space: nowrap;
display: flex;
gap: 4px;
color: #333;
.level-value {
font-weight: 500;
&.pending {
color: #999;
}
}
}
}
@keyframes popover-appear {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes popover-appear-above {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View file

@ -80,6 +80,7 @@
type: 'transcendence',
props: {
stage: transcendenceStage,
type,
editable,
interactive: editable,
onFragmentClick: editable ? handleTranscendenceUpdate : undefined