fix transcendence star positioning and update uncap styles
This commit is contained in:
parent
6762c2dab4
commit
d13412dfb9
2 changed files with 180 additions and 14 deletions
|
|
@ -2,10 +2,12 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TranscendenceFragment from './TranscendenceFragment.svelte'
|
import TranscendenceFragment from './TranscendenceFragment.svelte'
|
||||||
|
import { Portal } from 'bits-ui'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string
|
className?: string
|
||||||
stage?: number
|
stage?: number
|
||||||
|
type?: 'character' | 'weapon' | 'summon'
|
||||||
editable?: boolean
|
editable?: boolean
|
||||||
interactive?: boolean
|
interactive?: boolean
|
||||||
tabIndex?: number
|
tabIndex?: number
|
||||||
|
|
@ -17,6 +19,7 @@
|
||||||
let {
|
let {
|
||||||
className,
|
className,
|
||||||
stage = 0,
|
stage = 0,
|
||||||
|
type = 'character',
|
||||||
editable = false,
|
editable = false,
|
||||||
interactive = false,
|
interactive = false,
|
||||||
tabIndex,
|
tabIndex,
|
||||||
|
|
@ -27,19 +30,100 @@
|
||||||
|
|
||||||
const NUM_FRAGMENTS = 5
|
const NUM_FRAGMENTS = 5
|
||||||
|
|
||||||
|
interface PopoverPosition {
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
placement: 'above' | 'below'
|
||||||
|
}
|
||||||
|
|
||||||
let visibleStage = $state(stage)
|
let visibleStage = $state(stage)
|
||||||
let currentStage = $state(stage)
|
let currentStage = $state(stage)
|
||||||
let immutable = $state(false)
|
let immutable = $state(false)
|
||||||
|
let isPopoverOpen = $state(false)
|
||||||
|
let popoverPosition = $state<PopoverPosition | null>(null)
|
||||||
let starElement: HTMLDivElement
|
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(() => {
|
$effect(() => {
|
||||||
visibleStage = stage
|
visibleStage = stage
|
||||||
currentStage = 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() {
|
function handleClick() {
|
||||||
if (editable && onStarClick) {
|
if (editable && onStarClick) {
|
||||||
onStarClick()
|
onStarClick()
|
||||||
|
} else if (interactive) {
|
||||||
|
isPopoverOpen = !isPopoverOpen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +138,7 @@
|
||||||
if (onFragmentClick) {
|
if (onFragmentClick) {
|
||||||
onFragmentClick(newStage)
|
onFragmentClick(newStage)
|
||||||
}
|
}
|
||||||
|
isPopoverOpen = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFragmentHover(index: number) {
|
function handleFragmentHover(index: number) {
|
||||||
|
|
@ -87,25 +172,39 @@
|
||||||
role={editable ? 'button' : undefined}
|
role={editable ? 'button' : undefined}
|
||||||
aria-label={editable ? 'Transcendence star' : undefined}
|
aria-label={editable ? 'Transcendence star' : undefined}
|
||||||
>
|
>
|
||||||
<div class="fragments">
|
{#if interactive && isPopoverOpen && popoverPosition}
|
||||||
{#if interactive}
|
<Portal>
|
||||||
{#each Array(NUM_FRAGMENTS) as _, i}
|
<div
|
||||||
{@const loopStage = i + 1}
|
class="popover"
|
||||||
<TranscendenceFragment
|
class:above={popoverPosition.placement === 'above'}
|
||||||
stage={loopStage}
|
style="top: {popoverPosition.top}px; left: {popoverPosition.left}px"
|
||||||
visible={loopStage <= visibleStage}
|
bind:this={popoverElement}
|
||||||
{interactive}
|
>
|
||||||
onClick={handleFragmentClick}
|
<div class="fragments">
|
||||||
onHover={handleFragmentHover}
|
{#each Array(NUM_FRAGMENTS) as _, i}
|
||||||
/>
|
{@const loopStage = i + 1}
|
||||||
{/each}
|
<TranscendenceFragment
|
||||||
{/if}
|
stage={loopStage}
|
||||||
</div>
|
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')} />
|
<i class="figure {className || ''}" class:interactive class:base={className?.includes('base')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$src/themes/spacing' as spacing;
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/typography';
|
||||||
|
|
||||||
.star {
|
.star {
|
||||||
--size: 18px;
|
--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>
|
</style>
|
||||||
|
|
@ -80,6 +80,7 @@
|
||||||
type: 'transcendence',
|
type: 'transcendence',
|
||||||
props: {
|
props: {
|
||||||
stage: transcendenceStage,
|
stage: transcendenceStage,
|
||||||
|
type,
|
||||||
editable,
|
editable,
|
||||||
interactive: editable,
|
interactive: editable,
|
||||||
onFragmentClick: editable ? handleTranscendenceUpdate : undefined
|
onFragmentClick: editable ? handleTranscendenceUpdate : undefined
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue