add perpetuity ring toggle and fix grid api response handling
This commit is contained in:
parent
a10659b347
commit
3666b8db86
11 changed files with 1175 additions and 397 deletions
414
docs/details-sidebar-segmented-control.md
Normal file
414
docs/details-sidebar-segmented-control.md
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
# DetailsSidebar Segmented Control Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Add a segmented control to the DetailsSidebar component that allows users to switch between viewing the canonical (base) item data and the user's customized version with their modifications.
|
||||
|
||||
## User Requirements
|
||||
|
||||
The sidebar should display two distinct views:
|
||||
|
||||
1. **Canonical Data View** - Shows the base item statistics and properties as they exist in the game database
|
||||
2. **User Version View** - Shows the user's specific customizations and modifications to the item
|
||||
|
||||
## Data Structure Analysis
|
||||
|
||||
### Current Grid Item Structure
|
||||
|
||||
Each grid item (GridCharacter, GridWeapon, GridSummon) contains:
|
||||
- The base object data (`character`, `weapon`, or `summon`)
|
||||
- User-specific modifications stored at the grid item level
|
||||
- Instance-specific properties like position, uncap level, etc.
|
||||
|
||||
### User Version Data by Type
|
||||
|
||||
#### Weapons (GridWeapon)
|
||||
- `uncapLevel` - Current uncap level (0-6)
|
||||
- `transcendenceStep` - Transcendence stage (0-5)
|
||||
- `awakening` - Object containing:
|
||||
- `type` - Awakening type with name and slug
|
||||
- `level` - Awakening level
|
||||
- `weaponKeys` - Array of weapon keys:
|
||||
- Opus pendulums (series 2)
|
||||
- Draconic telumas (series 3, 34)
|
||||
- Ultima gauph keys (series 17)
|
||||
- Revans emblems (series 22)
|
||||
- `ax` - Array of AX skills containing:
|
||||
- `modifier` - Skill ID
|
||||
- `strength` - Skill strength value
|
||||
- `element` - Instance element for null-element weapons
|
||||
|
||||
#### Characters (GridCharacter)
|
||||
- `uncapLevel` - Current uncap level (0-5 or 0-6)
|
||||
- `transcendenceStep` - Transcendence stage (0-5)
|
||||
- `awakening` - Awakening type and level
|
||||
- `rings` - Array of over mastery rings:
|
||||
- `modifier` - Ring stat type
|
||||
- `strength` - Ring stat value
|
||||
- `earring` - Aetherial mastery object:
|
||||
- `modifier` - Earring stat type
|
||||
- `strength` - Earring stat value
|
||||
- `aetherial_mastery` - Alternative property name for earring
|
||||
- `perpetuity` - Boolean for permanent mastery status
|
||||
|
||||
#### Summons (GridSummon)
|
||||
- `uncapLevel` - Current uncap level (0-5)
|
||||
- `transcendenceStep` - Transcendence stage (0-5)
|
||||
- `quick_summon` - Boolean for quick summon status
|
||||
- `friend` - Boolean for friend summon
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Reusable Components to Create
|
||||
|
||||
#### 1. `DetailsSidebarSegmentedControl.svelte`
|
||||
A specialized segmented control for the details sidebar that can be reused across different detail views.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
hasModifications: boolean
|
||||
selectedView: 'canonical' | 'user'
|
||||
onViewChange: (view: 'canonical' | 'user') => void
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 2. `ModificationSection.svelte`
|
||||
Generic wrapper for modification sections with consistent styling.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
title: string
|
||||
visible?: boolean
|
||||
children: Snippet
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if visible}
|
||||
<div class="modification-section">
|
||||
<h3>{title}</h3>
|
||||
<div class="modification-content">
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
```
|
||||
|
||||
#### 3. `AwakeningDisplay.svelte`
|
||||
Reusable awakening display component for both weapons and characters.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { getAwakeningImage } from '$lib/utils/modifiers'
|
||||
|
||||
interface Props {
|
||||
awakening?: { type: Awakening; level: number }
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
showLevel?: boolean
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 4. `WeaponKeysList.svelte`
|
||||
Component for displaying weapon keys with proper icons and formatting.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { getWeaponKeyImages } from '$lib/utils/modifiers'
|
||||
|
||||
interface Props {
|
||||
weaponKeys?: WeaponKey[]
|
||||
weaponData: { element?: number; proficiency?: number; series?: number; name?: LocalizedString }
|
||||
layout?: 'list' | 'grid'
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
#### 5. `StatModifierItem.svelte`
|
||||
Generic component for displaying stat modifications (rings, earrings, etc.).
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label: string
|
||||
value: string | number
|
||||
suffix?: string
|
||||
icon?: string
|
||||
variant?: 'default' | 'enhanced' | 'max'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="stat-modifier" class:variant>
|
||||
{#if icon}<img src={icon} alt="" />{/if}
|
||||
<span class="label">{label}</span>
|
||||
<span class="value">{value}{suffix}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 6. `UncapStatusDisplay.svelte`
|
||||
Dedicated component for showing current uncap/transcendence status.
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
uncapLevel?: number
|
||||
transcendenceStep?: number
|
||||
maxUncap: number
|
||||
showIndicator?: boolean
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### Data Processing Utilities
|
||||
|
||||
#### `modificationDetector.ts`
|
||||
Utility to detect what modifications exist on a grid item.
|
||||
|
||||
```typescript
|
||||
export interface ModificationStatus {
|
||||
hasModifications: boolean
|
||||
hasAwakening: boolean
|
||||
hasWeaponKeys: boolean
|
||||
hasAxSkills: boolean
|
||||
hasRings: boolean
|
||||
hasEarring: boolean
|
||||
hasPerpetuity: boolean
|
||||
hasTranscendence: boolean
|
||||
}
|
||||
|
||||
export function detectModifications(
|
||||
type: 'character' | 'weapon' | 'summon',
|
||||
item: GridCharacter | GridWeapon | GridSummon
|
||||
): ModificationStatus {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
#### `modificationFormatters.ts`
|
||||
Centralized formatters for modification display.
|
||||
|
||||
```typescript
|
||||
export function formatRingStat(modifier: number, strength: number): string
|
||||
export function formatEarringStat(modifier: number, strength: number): string
|
||||
export function formatAxSkill(ax: SimpleAxSkill): string
|
||||
export function getWeaponKeyTitle(series?: number): string
|
||||
```
|
||||
|
||||
### Component Composition Pattern
|
||||
|
||||
The main DetailsSidebar will compose these smaller components:
|
||||
|
||||
```svelte
|
||||
<!-- DetailsSidebar.svelte -->
|
||||
<DetailsSidebarSegmentedControl {hasModifications} bind:selectedView />
|
||||
|
||||
{#if selectedView === 'canonical'}
|
||||
<!-- Existing canonical view -->
|
||||
{:else}
|
||||
<!-- User version composed of reusable components -->
|
||||
<UncapStatusDisplay {type} {uncapLevel} {transcendenceStep} />
|
||||
|
||||
<ModificationSection title="Awakening" visible={item.awakening}>
|
||||
<AwakeningDisplay awakening={item.awakening} size="medium" showLevel />
|
||||
</ModificationSection>
|
||||
|
||||
{#if type === 'weapon'}
|
||||
<ModificationSection title={getWeaponKeyTitle(item.weapon?.series)} visible={item.weaponKeys?.length}>
|
||||
<WeaponKeysList {weaponKeys} weaponData={item.weapon} />
|
||||
</ModificationSection>
|
||||
{/if}
|
||||
|
||||
<!-- etc... -->
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Styling Guidelines
|
||||
|
||||
### IMPORTANT: Use Existing Theme System
|
||||
|
||||
**DO NOT create new style variables or custom styles.** All necessary styling is already defined in the theme files:
|
||||
|
||||
- `_colors.scss` - All color variables and element-specific colors
|
||||
- `_typography.scss` - Font sizes, weights, and text styling
|
||||
- `_spacing.scss` - Spacing units and gaps
|
||||
- `_layout.scss` - Border radius, corners, and layout constants
|
||||
- `_effects.scss` - Shadows, transitions, and visual effects
|
||||
- `_mixins.scss` - Reusable style mixins
|
||||
- `_rep.scss` - Representation/aspect ratio utilities
|
||||
|
||||
### Component Styling Example
|
||||
|
||||
```scss
|
||||
// Import only what's needed from themes
|
||||
@use '$src/themes/colors' as colors;
|
||||
@use '$src/themes/typography' as typography;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/layout' as layout;
|
||||
@use '$src/themes/effects' as effects;
|
||||
|
||||
.modification-section {
|
||||
// Use existing spacing variables
|
||||
margin-bottom: spacing.$unit-3x;
|
||||
padding: spacing.$unit-2x;
|
||||
|
||||
h3 {
|
||||
// Use existing typography
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-secondary, colors.$grey-40);
|
||||
margin-bottom: spacing.$unit-1-5x;
|
||||
}
|
||||
}
|
||||
|
||||
.stat-modifier {
|
||||
// Use existing layout patterns
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: spacing.$unit;
|
||||
background: colors.$grey-90;
|
||||
border-radius: layout.$item-corner-small;
|
||||
|
||||
.label {
|
||||
font-size: typography.$font-small;
|
||||
color: var(--text-secondary, colors.$grey-50);
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: typography.$font-regular;
|
||||
font-weight: typography.$medium;
|
||||
color: var(--text-primary, colors.$grey-10);
|
||||
}
|
||||
|
||||
// Use existing effect patterns for enhanced state
|
||||
&.enhanced {
|
||||
background: colors.$grey-85;
|
||||
box-shadow: effects.$hover-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
.awakening-display {
|
||||
// Use consistent spacing
|
||||
display: flex;
|
||||
gap: spacing.$unit-2x;
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
// Use standard sizing
|
||||
width: spacing.$unit-6x;
|
||||
height: spacing.$unit-6x;
|
||||
border-radius: layout.$item-corner-small;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Theme Variables Reference
|
||||
|
||||
#### Colors
|
||||
- Text: `var(--text-primary)`, `var(--text-secondary)`, `var(--text-tertiary)`
|
||||
- Backgrounds: `var(--card-bg)`, `colors.$grey-90`, `colors.$grey-85`
|
||||
- Element colors: `var(--wind-item-detail-bg)`, etc.
|
||||
- State colors: `var(--color-success)`, `var(--color-error)`
|
||||
|
||||
#### Typography
|
||||
- Sizes: `$font-tiny`, `$font-small`, `$font-regular`, `$font-medium`, `$font-large`
|
||||
- Weights: `$normal: 400`, `$medium: 500`, `$bold: 600`
|
||||
|
||||
#### Spacing
|
||||
- Base unit: `$unit: 8px`
|
||||
- Multipliers: `$unit-half`, `$unit-2x`, `$unit-3x`, `$unit-4x`, etc.
|
||||
- Fractions: `$unit-fourth`, `$unit-third`
|
||||
|
||||
#### Layout
|
||||
- Corners: `$item-corner`, `$item-corner-small`, `$modal-corner`
|
||||
- Breakpoints: Use mixins from `_mixins.scss`
|
||||
|
||||
#### Effects
|
||||
- Shadows: `$hover-shadow`, `$focus-shadow`
|
||||
- Transitions: `$duration-zoom`, `$duration-color-fade`
|
||||
- Transforms: `$scale-wide`, `$scale-tall`
|
||||
|
||||
## Benefits of Componentization
|
||||
|
||||
### Maintainability
|
||||
- Each component has a single responsibility
|
||||
- Changes to display logic are isolated
|
||||
- Easier to test individual components
|
||||
- Consistent styling through shared theme system
|
||||
|
||||
### Reusability
|
||||
- `AwakeningDisplay` can be used in hovercards, modals, and sidebars
|
||||
- `StatModifierItem` works for any stat modification
|
||||
- `ModificationSection` provides consistent section layout
|
||||
|
||||
### Type Safety
|
||||
- Each component has clearly defined props
|
||||
- TypeScript interfaces ensure correct data flow
|
||||
- Compile-time checking prevents runtime errors
|
||||
|
||||
### Performance
|
||||
- Components can be memoized if needed
|
||||
- Smaller components = smaller re-render boundaries
|
||||
- Derived states prevent unnecessary recalculation
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests for Components
|
||||
Each reusable component should have tests for:
|
||||
- Rendering with different prop combinations
|
||||
- Conditional visibility
|
||||
- Event handling
|
||||
- Edge cases (missing data, invalid values)
|
||||
|
||||
### Integration Tests
|
||||
Test the complete DetailsSidebar with:
|
||||
- View switching
|
||||
- Data flow between components
|
||||
- Correct component composition
|
||||
|
||||
### Visual Regression Tests
|
||||
Use Storybook to document and test visual states:
|
||||
- Different modification combinations
|
||||
- Various item types
|
||||
- Empty states
|
||||
- Loading states
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Phase 1: Infrastructure
|
||||
- [ ] Set up modificationDetector utility
|
||||
- [ ] Set up modificationFormatters utility
|
||||
- [ ] Create ModificationSection wrapper component
|
||||
|
||||
### Phase 2: Display Components
|
||||
- [ ] Create AwakeningDisplay component
|
||||
- [ ] Create WeaponKeysList component
|
||||
- [ ] Create StatModifierItem component
|
||||
- [ ] Create UncapStatusDisplay component
|
||||
- [ ] Create DetailsSidebarSegmentedControl
|
||||
|
||||
### Phase 3: Integration
|
||||
- [ ] Update DetailsSidebar to use new components
|
||||
- [ ] Wire up view switching logic
|
||||
- [ ] Implement canonical view with existing code
|
||||
- [ ] Implement user version view with new components
|
||||
|
||||
### Phase 4: Polish
|
||||
- [ ] Add loading states
|
||||
- [ ] Add empty states
|
||||
- [ ] Optimize performance
|
||||
- [ ] Add accessibility attributes
|
||||
- [ ] Documentation and examples
|
||||
|
||||
## Notes
|
||||
|
||||
- Components should accept `class` prop for custom styling
|
||||
- All components should handle missing/null data gracefully
|
||||
- Consider using slots/snippets for maximum flexibility
|
||||
- Keep components pure - no direct API calls
|
||||
- Use consistent prop naming across components
|
||||
- **Always use existing theme variables - never create custom styles**
|
||||
- Import only the theme modules you need to minimize bundle size
|
||||
- Use CSS custom properties (var()) for dynamic theming support
|
||||
234
docs/transcendence-popover-fix.md
Normal file
234
docs/transcendence-popover-fix.md
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
# 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/`
|
||||
49
src/assets/icons/perpetuity/empty.svg
Normal file
49
src/assets/icons/perpetuity/empty.svg
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<svg viewBox="0 0 45 46" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1460_2996)">
|
||||
<rect x="4" y="2" width="37" height="38" rx="2" fill="white"/>
|
||||
<rect x="5" y="3" width="35" height="36" rx="1" fill="url(#paint0_linear_1460_2996)"/>
|
||||
<g filter="url(#filter1_i_1460_2996)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4885 18.3682C27.4659 19.17 29.6429 21.7269 29.6429 24.7572C29.6429 28.4299 26.445 31.4072 22.5001 31.4072C18.5552 31.4072 15.3572 28.4299 15.3572 24.7572C15.3572 21.7288 17.5316 19.1732 20.5061 18.3697L18.6119 16.002C15.0204 17.4432 12.5 20.8206 12.5 24.7572C12.5 30.0039 16.9772 34.2572 22.5 34.2572C28.0228 34.2572 32.5 30.0039 32.5 24.7572C32.5 20.8187 29.9772 17.4401 26.3831 16L24.4885 18.3682Z" fill="#999999"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_i_1460_2996)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7158 18.0239L16.9691 12.0905C16.6896 11.7412 16.6762 11.2487 16.9363 10.8846L18.6976 8.41876C18.8853 8.15597 19.1884 8 19.5113 8H25.4821C25.805 8 26.1081 8.15597 26.2958 8.41876L28.0571 10.8846C28.3172 11.2487 28.3038 11.7412 28.0243 12.0905L23.2776 18.0239C22.8772 18.5243 22.1162 18.5243 21.7158 18.0239ZM23 9.25V15.7873C23 16.0236 23.2976 16.128 23.4452 15.9435L26.8751 11.6562C26.9481 11.5649 26.9481 11.4351 26.8751 11.3438L25.0751 9.09383C25.0276 9.03452 24.9558 9 24.8798 9H23.25C23.1119 9 23 9.11193 23 9.25Z" fill="#999999"/>
|
||||
</g>
|
||||
<rect x="5.5" y="3.5" width="34" height="35" rx="0.5" stroke="black" stroke-opacity="0.09"/>
|
||||
<rect x="3.5" y="1.5" width="38" height="39" rx="2.5" stroke="black" stroke-opacity="0.1"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1460_2996" x="0" y="0" width="45" height="46" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1460_2996"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1460_2996" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_i_1460_2996" x="12.5" y="16" width="20" height="19.2572" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1460_2996"/>
|
||||
</filter>
|
||||
<filter id="filter2_i_1460_2996" x="16.75" y="8" width="11.4934" height="11.3992" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.12 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_1460_2996"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1460_2996" x1="22.5" y1="3" x2="22.5" y2="39" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0618126" stop-color="#C1C1C1"/>
|
||||
<stop offset="0.756271" stop-color="#B0B0B0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.8 KiB |
53
src/assets/icons/perpetuity/filled.svg
Normal file
53
src/assets/icons/perpetuity/filled.svg
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<svg viewBox="0 0 45 46" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_1460_2995)">
|
||||
<rect x="4" y="2" width="37" height="38" rx="2" fill="white"/>
|
||||
<rect x="5" y="3" width="35" height="36" rx="1" fill="url(#paint0_linear_1460_2995)"/>
|
||||
<g filter="url(#filter1_d_1460_2995)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.4885 18.3682C27.4659 19.17 29.6429 21.7269 29.6429 24.7572C29.6429 28.4299 26.445 31.4072 22.5001 31.4072C18.5552 31.4072 15.3572 28.4299 15.3572 24.7572C15.3572 21.7288 17.5316 19.1732 20.5061 18.3697L18.6119 16.002C15.0204 17.4432 12.5 20.8206 12.5 24.7572C12.5 30.0039 16.9772 34.2572 22.5 34.2572C28.0228 34.2572 32.5 30.0039 32.5 24.7572C32.5 20.8187 29.9772 17.4401 26.3831 16L24.4885 18.3682Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_d_1460_2995)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M21.7158 18.0239L16.9691 12.0905C16.6896 11.7412 16.6762 11.2487 16.9363 10.8846L18.6976 8.41876C18.8853 8.15597 19.1884 8 19.5113 8H25.4821C25.805 8 26.1081 8.15597 26.2958 8.41876L28.0571 10.8846C28.3172 11.2487 28.3038 11.7412 28.0243 12.0905L23.2776 18.0239C22.8772 18.5243 22.1162 18.5243 21.7158 18.0239ZM23 9.25V15.7873C23 16.0236 23.2976 16.128 23.4452 15.9435L26.8751 11.6562C26.9481 11.5649 26.9481 11.4351 26.8751 11.3438L25.0751 9.09383C25.0276 9.03452 24.9558 9 24.8798 9H23.25C23.1119 9 23 9.11193 23 9.25Z" fill="white"/>
|
||||
</g>
|
||||
<rect x="5.5" y="3.5" width="34" height="35" rx="0.5" stroke="url(#paint1_linear_1460_2995)" stroke-opacity="0.5"/>
|
||||
<rect x="3.5" y="1.5" width="38" height="39" rx="2.5" stroke="black" stroke-opacity="0.1"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_1460_2995" x="0" y="0" width="45" height="46" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="1.5"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1460_2995"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1460_2995" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_d_1460_2995" x="10.5" y="15" width="24" height="22.2572" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.22 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1460_2995"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1460_2995" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_d_1460_2995" x="14.75" y="7" width="15.4934" height="14.3992" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.22 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1460_2995"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1460_2995" result="shape"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear_1460_2995" x1="22.5" y1="3" x2="22.5" y2="39" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0618126" stop-color="#6CD6FF"/>
|
||||
<stop offset="0.756271" stop-color="#ACB3FE"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_1460_2995" x1="22.5" y1="3" x2="22.5" y2="39" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#8DEFFE"/>
|
||||
<stop offset="1" stop-color="#E8EDFF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
|
|
@ -9,65 +9,10 @@
|
|||
|
||||
import { BaseAdapter } from './base.adapter'
|
||||
import type { AdapterOptions } from './types'
|
||||
import type { GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
|
||||
import { DEFAULT_ADAPTER_CONFIG } from './config'
|
||||
|
||||
/**
|
||||
* Common grid item structure
|
||||
*/
|
||||
interface BaseGridItem {
|
||||
id: string
|
||||
partyId: string
|
||||
position: number
|
||||
uncapLevel?: number
|
||||
transcendenceStage?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid weapon specific fields
|
||||
*/
|
||||
export interface GridWeapon extends BaseGridItem {
|
||||
weaponId: string
|
||||
mainhand?: boolean
|
||||
element?: number
|
||||
weaponKeys?: Array<{
|
||||
id: string
|
||||
slot: number
|
||||
}>
|
||||
axModifier1?: string
|
||||
axModifier2?: string
|
||||
axStrength1?: number
|
||||
axStrength2?: number
|
||||
awakeningId?: string
|
||||
awakeningLevel?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid character specific fields
|
||||
*/
|
||||
export interface GridCharacter extends BaseGridItem {
|
||||
characterId: string
|
||||
perpetualModifiers?: Record<string, any>
|
||||
awakeningId?: string
|
||||
awakeningLevel?: number
|
||||
rings?: Array<{
|
||||
modifier: string
|
||||
strength: number
|
||||
}>
|
||||
earring?: {
|
||||
modifier: string
|
||||
strength: number
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid summon specific fields
|
||||
*/
|
||||
export interface GridSummon extends BaseGridItem {
|
||||
summonId: string
|
||||
main?: boolean
|
||||
friend?: boolean
|
||||
quickSummon?: boolean
|
||||
}
|
||||
// GridWeapon, GridCharacter, and GridSummon types are imported from types/api/party
|
||||
|
||||
/**
|
||||
* Parameters for creating grid items
|
||||
|
|
@ -78,7 +23,7 @@ export interface CreateGridWeaponParams {
|
|||
position: number
|
||||
mainhand?: boolean
|
||||
uncapLevel?: number
|
||||
transcendenceStage?: number
|
||||
transcendenceStep?: number
|
||||
}
|
||||
|
||||
export interface CreateGridCharacterParams {
|
||||
|
|
@ -86,7 +31,7 @@ export interface CreateGridCharacterParams {
|
|||
characterId: string
|
||||
position: number
|
||||
uncapLevel?: number
|
||||
transcendenceStage?: number
|
||||
transcendenceStep?: number
|
||||
}
|
||||
|
||||
export interface CreateGridSummonParams {
|
||||
|
|
@ -97,7 +42,7 @@ export interface CreateGridSummonParams {
|
|||
friend?: boolean
|
||||
quickSummon?: boolean
|
||||
uncapLevel?: number
|
||||
transcendenceStage?: number
|
||||
transcendenceStep?: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -151,22 +96,24 @@ export class GridAdapter extends BaseAdapter {
|
|||
* Creates a new grid weapon instance
|
||||
*/
|
||||
async createWeapon(params: CreateGridWeaponParams, headers?: Record<string, string>): Promise<GridWeapon> {
|
||||
return this.request<GridWeapon>('/grid_weapons', {
|
||||
const response = await this.request<{ gridWeapon: GridWeapon }>('/grid_weapons', {
|
||||
method: 'POST',
|
||||
body: { weapon: params },
|
||||
headers
|
||||
})
|
||||
return response.gridWeapon
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a grid weapon instance
|
||||
*/
|
||||
async updateWeapon(id: string, params: Partial<GridWeapon>, headers?: Record<string, string>): Promise<GridWeapon> {
|
||||
return this.request<GridWeapon>(`/grid_weapons/${id}`, {
|
||||
const response = await this.request<{ gridWeapon: GridWeapon }>(`/grid_weapons/${id}`, {
|
||||
method: 'PUT',
|
||||
body: { weapon: params },
|
||||
headers
|
||||
})
|
||||
return response.gridWeapon
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -192,7 +139,7 @@ export class GridAdapter extends BaseAdapter {
|
|||
* Updates weapon uncap level
|
||||
*/
|
||||
async updateWeaponUncap(params: UpdateUncapParams, headers?: Record<string, string>): Promise<GridWeapon> {
|
||||
return this.request<GridWeapon>('/grid_weapons/update_uncap', {
|
||||
const response = await this.request<{ gridWeapon: GridWeapon }>('/grid_weapons/update_uncap', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
weapon: {
|
||||
|
|
@ -204,17 +151,19 @@ export class GridAdapter extends BaseAdapter {
|
|||
},
|
||||
headers
|
||||
})
|
||||
return response.gridWeapon
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves weapon conflicts
|
||||
*/
|
||||
async resolveWeaponConflict(params: ResolveConflictParams, headers?: Record<string, string>): Promise<GridWeapon> {
|
||||
return this.request<GridWeapon>('/grid_weapons/resolve', {
|
||||
const response = await this.request<{ gridWeapon: GridWeapon }>('/grid_weapons/resolve', {
|
||||
method: 'POST',
|
||||
body: { resolve: params },
|
||||
headers
|
||||
})
|
||||
return response.gridWeapon
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -222,11 +171,12 @@ export class GridAdapter extends BaseAdapter {
|
|||
*/
|
||||
async updateWeaponPosition(params: UpdatePositionParams, headers?: Record<string, string>): Promise<GridWeapon> {
|
||||
const { id, position, container, partyId } = params
|
||||
return this.request<GridWeapon>(`/parties/${partyId}/grid_weapons/${id}/position`, {
|
||||
const response = await this.request<{ gridWeapon: GridWeapon }>(`/parties/${partyId}/grid_weapons/${id}/position`, {
|
||||
method: 'PUT',
|
||||
body: { position, container },
|
||||
headers
|
||||
})
|
||||
return response.gridWeapon
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -250,22 +200,24 @@ export class GridAdapter extends BaseAdapter {
|
|||
* Creates a new grid character instance
|
||||
*/
|
||||
async createCharacter(params: CreateGridCharacterParams, headers?: Record<string, string>): Promise<GridCharacter> {
|
||||
return this.request<GridCharacter>('/grid_characters', {
|
||||
const response = await this.request<{ gridCharacter: GridCharacter }>('/grid_characters', {
|
||||
method: 'POST',
|
||||
body: { character: params },
|
||||
headers
|
||||
})
|
||||
return response.gridCharacter
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a grid character instance
|
||||
*/
|
||||
async updateCharacter(id: string, params: Partial<GridCharacter>, headers?: Record<string, string>): Promise<GridCharacter> {
|
||||
return this.request<GridCharacter>(`/grid_characters/${id}`, {
|
||||
const response = await this.request<{ gridCharacter: GridCharacter }>(`/grid_characters/${id}`, {
|
||||
method: 'PUT',
|
||||
body: { character: params },
|
||||
headers
|
||||
})
|
||||
return response.gridCharacter
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,7 +243,7 @@ export class GridAdapter extends BaseAdapter {
|
|||
* Updates character uncap level
|
||||
*/
|
||||
async updateCharacterUncap(params: UpdateUncapParams, headers?: Record<string, string>): Promise<GridCharacter> {
|
||||
return this.request<GridCharacter>('/grid_characters/update_uncap', {
|
||||
const response = await this.request<{ gridCharacter: GridCharacter }>('/grid_characters/update_uncap', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
character: {
|
||||
|
|
@ -303,17 +255,19 @@ export class GridAdapter extends BaseAdapter {
|
|||
},
|
||||
headers
|
||||
})
|
||||
return response.gridCharacter
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves character conflicts
|
||||
*/
|
||||
async resolveCharacterConflict(params: ResolveConflictParams, headers?: Record<string, string>): Promise<GridCharacter> {
|
||||
return this.request<GridCharacter>('/grid_characters/resolve', {
|
||||
const response = await this.request<{ gridCharacter: GridCharacter }>('/grid_characters/resolve', {
|
||||
method: 'POST',
|
||||
body: { resolve: params },
|
||||
headers
|
||||
})
|
||||
return response.gridCharacter
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -321,11 +275,12 @@ export class GridAdapter extends BaseAdapter {
|
|||
*/
|
||||
async updateCharacterPosition(params: UpdatePositionParams, headers?: Record<string, string>): Promise<GridCharacter> {
|
||||
const { id, position, container, partyId } = params
|
||||
return this.request<GridCharacter>(`/parties/${partyId}/grid_characters/${id}/position`, {
|
||||
const response = await this.request<{ gridCharacter: GridCharacter }>(`/parties/${partyId}/grid_characters/${id}/position`, {
|
||||
method: 'PUT',
|
||||
body: { position, container },
|
||||
headers
|
||||
})
|
||||
return response.gridCharacter
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -349,22 +304,24 @@ export class GridAdapter extends BaseAdapter {
|
|||
* Creates a new grid summon instance
|
||||
*/
|
||||
async createSummon(params: CreateGridSummonParams, headers?: Record<string, string>): Promise<GridSummon> {
|
||||
return this.request<GridSummon>('/grid_summons', {
|
||||
const response = await this.request<{ gridSummon: GridSummon }>('/grid_summons', {
|
||||
method: 'POST',
|
||||
body: { summon: params },
|
||||
headers
|
||||
})
|
||||
return response.gridSummon
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a grid summon instance
|
||||
*/
|
||||
async updateSummon(id: string, params: Partial<GridSummon>, headers?: Record<string, string>): Promise<GridSummon> {
|
||||
return this.request<GridSummon>(`/grid_summons/${id}`, {
|
||||
const response = await this.request<{ gridSummon: GridSummon }>(`/grid_summons/${id}`, {
|
||||
method: 'PUT',
|
||||
body: { summon: params },
|
||||
headers
|
||||
})
|
||||
return response.gridSummon
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -390,7 +347,7 @@ export class GridAdapter extends BaseAdapter {
|
|||
* Updates summon uncap level
|
||||
*/
|
||||
async updateSummonUncap(params: UpdateUncapParams, headers?: Record<string, string>): Promise<GridSummon> {
|
||||
return this.request<GridSummon>('/grid_summons/update_uncap', {
|
||||
const response = await this.request<{ gridSummon: GridSummon }>('/grid_summons/update_uncap', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
summon: {
|
||||
|
|
@ -402,6 +359,7 @@ export class GridAdapter extends BaseAdapter {
|
|||
},
|
||||
headers
|
||||
})
|
||||
return response.gridSummon
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -424,11 +382,12 @@ export class GridAdapter extends BaseAdapter {
|
|||
*/
|
||||
async updateSummonPosition(params: UpdatePositionParams, headers?: Record<string, string>): Promise<GridSummon> {
|
||||
const { id, position, container, partyId } = params
|
||||
return this.request<GridSummon>(`/parties/${partyId}/grid_summons/${id}/position`, {
|
||||
const response = await this.request<{ gridSummon: GridSummon }>(`/parties/${partyId}/grid_summons/${id}/position`, {
|
||||
method: 'PUT',
|
||||
body: { position, container },
|
||||
headers
|
||||
})
|
||||
return response.gridSummon
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import { BaseAdapter } from './base.adapter'
|
||||
import type { RequestOptions, AdapterOptions, PaginatedResponse } from './types'
|
||||
import { DEFAULT_ADAPTER_CONFIG } from './config'
|
||||
import type { Party, GridWeapon, GridCharacter, GridSummon } from '$lib/types/api/party'
|
||||
|
||||
/**
|
||||
* Party data structure
|
||||
|
|
@ -58,66 +59,11 @@ export interface Party {
|
|||
updatedAt: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Grid weapon structure
|
||||
*/
|
||||
export interface GridWeapon {
|
||||
id: string
|
||||
position: number
|
||||
mainhand: boolean
|
||||
uncapLevel: number
|
||||
transcendenceStage: number
|
||||
weaponKeys: Array<{
|
||||
id: string
|
||||
slot: number
|
||||
}>
|
||||
weapon: {
|
||||
id: string
|
||||
granblueId: string
|
||||
name: Record<string, string>
|
||||
element: number
|
||||
rarity: number
|
||||
}
|
||||
}
|
||||
// GridWeapon type is imported from types/api/party
|
||||
|
||||
/**
|
||||
* Grid summon structure
|
||||
*/
|
||||
export interface GridSummon {
|
||||
id: string
|
||||
position: number
|
||||
quickSummon: boolean
|
||||
transcendenceStage: number
|
||||
summon: {
|
||||
id: string
|
||||
granblueId: string
|
||||
name: Record<string, string>
|
||||
element: number
|
||||
rarity: number
|
||||
}
|
||||
}
|
||||
// GridSummon type is imported from types/api/party
|
||||
|
||||
/**
|
||||
* Grid character structure
|
||||
*/
|
||||
export interface GridCharacter {
|
||||
id: string
|
||||
position: number
|
||||
uncapLevel: number
|
||||
transcendenceStage: number
|
||||
perpetualModifiers?: Record<string, any>
|
||||
awakenings?: Array<{
|
||||
id: string
|
||||
level: number
|
||||
}>
|
||||
character: {
|
||||
id: string
|
||||
granblueId: string
|
||||
name: Record<string, string>
|
||||
element: number
|
||||
rarity: number
|
||||
}
|
||||
}
|
||||
// GridCharacter type is imported from types/api/party
|
||||
|
||||
/**
|
||||
* Parameters for creating a new party
|
||||
|
|
|
|||
|
|
@ -345,7 +345,7 @@
|
|||
|
||||
{#if modificationStatus.hasRings}
|
||||
<ModificationSection title="Over Mastery Rings" visible={true}>
|
||||
{#each (char.rings || char.over_mastery || []) as ring}
|
||||
{#each (char.over_mastery || []) as ring}
|
||||
<StatModifierItem
|
||||
label={formatRingStat(ring.modifier, ring.strength).split('+')[0].trim()}
|
||||
value={`+${ring.strength}`}
|
||||
|
|
@ -356,7 +356,7 @@
|
|||
{/if}
|
||||
|
||||
{#if modificationStatus.hasEarring}
|
||||
{@const earring = char.earring || char.aetherial_mastery}
|
||||
{@const earring = char.aetherial_mastery}
|
||||
{#if earring}
|
||||
<ModificationSection title="Aetherial Mastery" visible={true}>
|
||||
<StatModifierItem
|
||||
|
|
|
|||
|
|
@ -1,280 +1,406 @@
|
|||
<script lang="ts">
|
||||
import type { GridCharacter } from '$lib/types/api/party'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import { getCharacterImageWithPose } from '$lib/utils/images'
|
||||
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
|
||||
import type { GridCharacter } from '$lib/types/api/party'
|
||||
import type { Party } from '$lib/types/api/party'
|
||||
import { getContext } from 'svelte'
|
||||
import Icon from '$lib/components/Icon.svelte'
|
||||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte'
|
||||
import { ContextMenu as ContextMenuBase } from 'bits-ui'
|
||||
import UncapIndicator from '$lib/components/uncap/UncapIndicator.svelte'
|
||||
import { getCharacterImageWithPose } from '$lib/utils/images'
|
||||
import { openDetailsSidebar } from '$lib/features/details/openDetailsSidebar.svelte'
|
||||
import perpetuityFilled from '$src/assets/icons/perpetuity/filled.svg'
|
||||
import perpetuityEmpty from '$src/assets/icons/perpetuity/empty.svg'
|
||||
|
||||
interface Props {
|
||||
item?: GridCharacter
|
||||
position: number
|
||||
mainWeaponElement?: number | null
|
||||
partyElement?: number | null
|
||||
}
|
||||
interface Props {
|
||||
item?: GridCharacter
|
||||
position: number
|
||||
mainWeaponElement?: number | null
|
||||
partyElement?: number | null
|
||||
}
|
||||
|
||||
let { item, position, mainWeaponElement, partyElement }: Props = $props()
|
||||
let { item, position, mainWeaponElement, partyElement }: Props = $props()
|
||||
|
||||
type PartyCtx = {
|
||||
getParty: () => Party
|
||||
updateParty: (p: Party) => void
|
||||
canEdit: () => boolean
|
||||
getEditKey: () => string | null
|
||||
services: { gridService: any; partyService: any }
|
||||
openPicker?: (opts: { type: 'character' | 'weapon' | 'summon'; position: number; item?: any }) => void
|
||||
}
|
||||
const ctx = getContext<PartyCtx>('party')
|
||||
type PartyCtx = {
|
||||
getParty: () => Party
|
||||
updateParty: (p: Party) => void
|
||||
canEdit: () => boolean
|
||||
getEditKey: () => string | null
|
||||
services: { gridService: any; partyService: any }
|
||||
openPicker?: (opts: {
|
||||
type: 'character' | 'weapon' | 'summon'
|
||||
position: number
|
||||
item?: any
|
||||
}) => void
|
||||
}
|
||||
const ctx = getContext<PartyCtx>('party')
|
||||
|
||||
function displayName(input: any): string {
|
||||
if (!input) return '—'
|
||||
const maybe = input.name ?? input
|
||||
if (typeof maybe === 'string') return maybe
|
||||
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
|
||||
return '—'
|
||||
}
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived.by(() => {
|
||||
// If no item or no character with granblueId, return placeholder
|
||||
if (!item || !item.character?.granblueId) {
|
||||
return getCharacterImageWithPose(null, 'main', 0, 0)
|
||||
}
|
||||
function displayName(input: any): string {
|
||||
if (!input) return '—'
|
||||
const maybe = input.name ?? input
|
||||
if (typeof maybe === 'string') return maybe
|
||||
if (maybe && typeof maybe === 'object') return maybe.en || maybe.ja || '—'
|
||||
return '—'
|
||||
}
|
||||
// Use $derived to ensure consistent computation between server and client
|
||||
let imageUrl = $derived.by(() => {
|
||||
// If no item or no character with granblueId, return placeholder
|
||||
if (!item || !item.character?.granblueId) {
|
||||
return getCharacterImageWithPose(null, 'main', 0, 0)
|
||||
}
|
||||
|
||||
return getCharacterImageWithPose(
|
||||
item.character.granblueId,
|
||||
'main',
|
||||
item?.uncapLevel ?? 0,
|
||||
item?.transcendenceStep ?? 0,
|
||||
mainWeaponElement,
|
||||
partyElement
|
||||
)
|
||||
})
|
||||
return getCharacterImageWithPose(
|
||||
item.character.granblueId,
|
||||
'main',
|
||||
item?.uncapLevel ?? 0,
|
||||
item?.transcendenceStep ?? 0,
|
||||
mainWeaponElement,
|
||||
partyElement
|
||||
)
|
||||
})
|
||||
|
||||
async function remove() {
|
||||
if (!item?.id) return
|
||||
try {
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.removeCharacter(party.id, item.id as any, editKey || undefined)
|
||||
if (updated) {
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error removing character:', err)
|
||||
}
|
||||
}
|
||||
async function remove() {
|
||||
if (!item?.id) return
|
||||
try {
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.removeCharacter(
|
||||
party.id,
|
||||
item.id as any,
|
||||
editKey || undefined
|
||||
)
|
||||
if (updated) {
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error removing character:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function viewDetails() {
|
||||
if (!item) return
|
||||
openDetailsSidebar({
|
||||
type: 'character',
|
||||
item
|
||||
})
|
||||
}
|
||||
function viewDetails() {
|
||||
if (!item) return
|
||||
openDetailsSidebar({
|
||||
type: 'character',
|
||||
item
|
||||
})
|
||||
}
|
||||
|
||||
function replace() {
|
||||
if (ctx?.openPicker) {
|
||||
ctx.openPicker({ type: 'character', position, item })
|
||||
}
|
||||
}
|
||||
function replace() {
|
||||
if (ctx?.openPicker) {
|
||||
ctx.openPicker({ type: 'character', position, item })
|
||||
}
|
||||
}
|
||||
|
||||
async function togglePerpetuity(e: Event) {
|
||||
e.stopPropagation()
|
||||
if (!item?.id || !ctx?.canEdit()) return
|
||||
|
||||
try {
|
||||
const party = ctx.getParty()
|
||||
const editKey = ctx.getEditKey()
|
||||
// Update the character on the server
|
||||
const updatedCharacter = await ctx.services.gridService.updateCharacter(
|
||||
party.id,
|
||||
item.id,
|
||||
{ perpetuity: !item.perpetuity },
|
||||
editKey || undefined
|
||||
)
|
||||
|
||||
if (updatedCharacter) {
|
||||
// The API returns 'object' but we need 'character'
|
||||
const fixedCharacter = {
|
||||
...updatedCharacter,
|
||||
character: updatedCharacter.object,
|
||||
object: undefined
|
||||
}
|
||||
|
||||
// Update the party locally with the new character data
|
||||
const updatedParty = {
|
||||
...party,
|
||||
characters: party.characters.map((char) =>
|
||||
char.id === fixedCharacter.id ? fixedCharacter : char
|
||||
)
|
||||
}
|
||||
ctx.updateParty(updatedParty)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error toggling perpetuity:', err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="unit" class:empty={!item}>
|
||||
{#if item}
|
||||
<ContextMenu>
|
||||
{#snippet children()}
|
||||
{#key item?.id ?? position}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
onclick={() => viewDetails()}
|
||||
>
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!item?.character?.granblueId}
|
||||
alt={displayName(item?.character)}
|
||||
src={imageUrl}
|
||||
/>
|
||||
{#if ctx?.canEdit() && item?.id}
|
||||
<div class="actions">
|
||||
<button class="remove" title="Remove" onclick={(e) => { e.stopPropagation(); remove() }}>×</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
{/snippet}
|
||||
{#if item}
|
||||
<ContextMenu>
|
||||
{#snippet children()}
|
||||
{#key item?.id ?? position}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
onclick={() => viewDetails()}
|
||||
>
|
||||
{#if ctx?.canEdit()}
|
||||
<button
|
||||
class="perpetuity"
|
||||
class:active={item.perpetuity}
|
||||
onclick={togglePerpetuity}
|
||||
title={item.perpetuity ? 'Remove Perpetuity Ring' : 'Add Perpetuity Ring'}
|
||||
>
|
||||
<img class="perpetuity-icon filled" src={perpetuityFilled} alt="Perpetuity Ring" />
|
||||
<img
|
||||
class="perpetuity-icon empty"
|
||||
src={perpetuityEmpty}
|
||||
alt="Add Perpetuity Ring"
|
||||
/>
|
||||
</button>
|
||||
{:else if item.perpetuity}
|
||||
<img
|
||||
class="perpetuity static"
|
||||
src={perpetuityFilled}
|
||||
alt="Perpetuity Ring"
|
||||
title="Perpetuity Ring"
|
||||
/>
|
||||
{/if}
|
||||
<img
|
||||
class="image"
|
||||
class:placeholder={!item?.character?.granblueId}
|
||||
alt={displayName(item?.character)}
|
||||
src={imageUrl}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/snippet}
|
||||
|
||||
{#snippet menu()}
|
||||
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
|
||||
View Details
|
||||
</ContextMenuBase.Item>
|
||||
{#if ctx?.canEdit()}
|
||||
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
|
||||
Replace
|
||||
</ContextMenuBase.Item>
|
||||
<ContextMenuBase.Separator class="context-menu-separator" />
|
||||
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
|
||||
Remove
|
||||
</ContextMenuBase.Item>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ContextMenu>
|
||||
{:else}
|
||||
{#key `empty-${position}`}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
onclick={() => ctx?.canEdit() && ctx?.openPicker && ctx.openPicker({ type: 'character', position, item })}
|
||||
>
|
||||
<img
|
||||
class="image placeholder"
|
||||
alt=""
|
||||
src="/images/placeholders/placeholder-weapon-grid.png"
|
||||
/>
|
||||
{#if ctx?.canEdit()}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if item}
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
uncapLevel={item.uncapLevel}
|
||||
transcendenceStage={item.transcendenceStep}
|
||||
special={item.character?.special}
|
||||
flb={item.character?.uncap?.flb}
|
||||
ulb={item.character?.uncap?.ulb}
|
||||
transcendence={!item.character?.special && item.character?.uncap?.ulb}
|
||||
editable={ctx?.canEdit()}
|
||||
updateUncap={async (level) => {
|
||||
if (!item?.id || !ctx) return
|
||||
try {
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.updateCharacterUncap(item.id, level, undefined, editKey || undefined)
|
||||
if (updated) {
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update character uncap:', err)
|
||||
// TODO: Show user-friendly error notification
|
||||
}
|
||||
}}
|
||||
updateTranscendence={async (stage) => {
|
||||
if (!item?.id || !ctx) return
|
||||
try {
|
||||
const editKey = ctx.getEditKey()
|
||||
// When setting transcendence > 0, also set uncap to max (6)
|
||||
const maxUncap = stage > 0 ? 6 : undefined
|
||||
const updated = await ctx.services.gridService.updateCharacterUncap(item.id, maxUncap, stage, editKey || undefined)
|
||||
if (updated) {
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update character transcendence:', err)
|
||||
// TODO: Show user-friendly error notification
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="name">{item ? displayName(item?.character) : ''}</div>
|
||||
{#snippet menu()}
|
||||
<ContextMenuBase.Item class="context-menu-item" onclick={viewDetails}>
|
||||
View Details
|
||||
</ContextMenuBase.Item>
|
||||
{#if ctx?.canEdit()}
|
||||
<ContextMenuBase.Item class="context-menu-item" onclick={replace}>
|
||||
Replace
|
||||
</ContextMenuBase.Item>
|
||||
<ContextMenuBase.Separator class="context-menu-separator" />
|
||||
<ContextMenuBase.Item class="context-menu-item danger" onclick={remove}>
|
||||
Remove
|
||||
</ContextMenuBase.Item>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ContextMenu>
|
||||
{:else}
|
||||
{#key `empty-${position}`}
|
||||
<div
|
||||
class="frame character cell"
|
||||
class:editable={ctx?.canEdit()}
|
||||
onclick={() =>
|
||||
ctx?.canEdit() &&
|
||||
ctx?.openPicker &&
|
||||
ctx.openPicker({ type: 'character', position, item })}
|
||||
>
|
||||
<img
|
||||
class="image placeholder"
|
||||
alt=""
|
||||
src="/images/placeholders/placeholder-weapon-grid.png"
|
||||
/>
|
||||
{#if ctx?.canEdit()}
|
||||
<span class="icon">
|
||||
<Icon name="plus" size={24} />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
{#if item}
|
||||
<UncapIndicator
|
||||
type="character"
|
||||
uncapLevel={item.uncapLevel}
|
||||
transcendenceStage={item.transcendenceStep}
|
||||
special={item.character?.special}
|
||||
flb={item.character?.uncap?.flb}
|
||||
ulb={item.character?.uncap?.ulb}
|
||||
transcendence={!item.character?.special && item.character?.uncap?.ulb}
|
||||
editable={ctx?.canEdit()}
|
||||
updateUncap={async (level) => {
|
||||
if (!item?.id || !ctx) return
|
||||
try {
|
||||
const editKey = ctx.getEditKey()
|
||||
const updated = await ctx.services.gridService.updateCharacterUncap(
|
||||
item.id,
|
||||
level,
|
||||
undefined,
|
||||
editKey || undefined
|
||||
)
|
||||
if (updated) {
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update character uncap:', err)
|
||||
// TODO: Show user-friendly error notification
|
||||
}
|
||||
}}
|
||||
updateTranscendence={async (stage) => {
|
||||
if (!item?.id || !ctx) return
|
||||
try {
|
||||
const editKey = ctx.getEditKey()
|
||||
// When setting transcendence > 0, also set uncap to max (6)
|
||||
const maxUncap = stage > 0 ? 6 : undefined
|
||||
const updated = await ctx.services.gridService.updateCharacterUncap(
|
||||
item.id,
|
||||
maxUncap,
|
||||
stage,
|
||||
editKey || undefined
|
||||
)
|
||||
if (updated) {
|
||||
ctx.updateParty(updated)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to update character transcendence:', err)
|
||||
// TODO: Show user-friendly error notification
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="name">{item ? displayName(item?.character) : ''}</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/rep' as rep;
|
||||
@use '$src/themes/colors' as *;
|
||||
@use '$src/themes/spacing' as spacing;
|
||||
@use '$src/themes/typography' as *;
|
||||
@use '$src/themes/spacing' as *;
|
||||
@use '$src/themes/rep' as rep;
|
||||
|
||||
.unit {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
.unit {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
|
||||
&.empty .name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
&.empty .name {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg, #f5f5f5);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
.frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
border-radius: 8px;
|
||||
background: var(--card-bg, #f5f5f5);
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.95;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
opacity: 0.95;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.frame.character.cell {
|
||||
@include rep.aspect(rep.$char-cell-w, rep.$char-cell-h);
|
||||
}
|
||||
.frame.character.cell {
|
||||
@include rep.aspect(rep.$char-cell-w, rep.$char-cell-h);
|
||||
}
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
z-index: 2;
|
||||
.image {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
z-index: 2;
|
||||
border-radius: 8px;
|
||||
|
||||
&.placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
&.placeholder {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: var(--icon-secondary, #999);
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
.icon {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
color: var(--icon-secondary, #999);
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.frame.editable:hover .icon {
|
||||
color: var(--icon-secondary-hover, #666);
|
||||
}
|
||||
.frame.editable:hover .icon {
|
||||
color: var(--icon-secondary-hover, #666);
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: $font-small;
|
||||
text-align: center;
|
||||
color: $grey-50;
|
||||
}
|
||||
.name {
|
||||
font-size: $font-small;
|
||||
text-align: center;
|
||||
color: $grey-50;
|
||||
}
|
||||
|
||||
.actions {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
z-index: 3;
|
||||
}
|
||||
.perpetuity {
|
||||
position: absolute;
|
||||
z-index: 20;
|
||||
top: calc(spacing.$unit * -1);
|
||||
right: spacing.$unit-3x;
|
||||
width: spacing.$unit-4x;
|
||||
height: spacing.$unit-4x;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
.remove {
|
||||
background: rgba(0,0,0,.6);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
}
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
&.static {
|
||||
cursor: default;
|
||||
&:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.perpetuity-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
|
||||
&.filled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.perpetuity.active {
|
||||
.perpetuity-icon.filled {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.perpetuity-icon.empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.perpetuity-icon.filled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.perpetuity-icon.empty {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.perpetuity:not(.active):hover {
|
||||
.perpetuity-icon.filled {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.perpetuity-icon.empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export class GridService {
|
|||
position,
|
||||
mainhand: options?.mainhand,
|
||||
uncapLevel: getDefaultMaxUncapLevel('weapon'),
|
||||
transcendenceStage: 0
|
||||
transcendenceStep: 0
|
||||
}, this.buildHeaders(editKey))
|
||||
|
||||
console.log('[GridService] Created grid weapon:', gridWeapon)
|
||||
|
|
@ -125,7 +125,7 @@ export class GridService {
|
|||
await gridAdapter.updateWeapon(gridWeaponId, {
|
||||
position: updates.position,
|
||||
uncapLevel: updates.uncapLevel,
|
||||
transcendenceStage: updates.transcendenceStep,
|
||||
transcendenceStep: updates.transcendenceStep,
|
||||
element: updates.element
|
||||
}, this.buildHeaders(editKey))
|
||||
|
||||
|
|
@ -360,7 +360,7 @@ export class GridService {
|
|||
characterId,
|
||||
position,
|
||||
uncapLevel: getDefaultMaxUncapLevel('character'),
|
||||
transcendenceStage: 0
|
||||
transcendenceStep: 0
|
||||
}, this.buildHeaders(editKey))
|
||||
|
||||
console.log('[GridService] Created grid character:', gridCharacter)
|
||||
|
|
@ -435,12 +435,12 @@ export class GridService {
|
|||
},
|
||||
editKey?: string,
|
||||
options?: { shortcode?: string }
|
||||
): Promise<Party | null> {
|
||||
await gridAdapter.updateCharacter(gridCharacterId, {
|
||||
): Promise<GridCharacter | null> {
|
||||
const updated = await gridAdapter.updateCharacter(gridCharacterId, {
|
||||
position: updates.position,
|
||||
uncapLevel: updates.uncapLevel,
|
||||
transcendenceStage: updates.transcendenceStep,
|
||||
perpetualModifiers: updates.perpetuity ? {} : undefined
|
||||
transcendenceStep: updates.transcendenceStep,
|
||||
perpetuity: updates.perpetuity
|
||||
}, this.buildHeaders(editKey))
|
||||
|
||||
// Clear party cache if shortcode provided
|
||||
|
|
@ -448,8 +448,8 @@ export class GridService {
|
|||
partyAdapter.clearPartyCache(options.shortcode)
|
||||
}
|
||||
|
||||
// Don't fetch - let caller handle refresh
|
||||
return null
|
||||
// Return the updated character
|
||||
return updated
|
||||
}
|
||||
|
||||
async moveCharacter(
|
||||
|
|
|
|||
|
|
@ -44,8 +44,6 @@ export interface GridCharacter {
|
|||
transcendenceStep?: number
|
||||
character: Character // Named properly, not "object"
|
||||
awakening?: Awakening
|
||||
rings?: Array<{ modifier: number; strength: number }>
|
||||
earring?: { modifier: number; strength: number }
|
||||
aetherial_mastery?: { modifier: number; strength: number }
|
||||
over_mastery?: Array<{ modifier: number; strength: number }>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,8 @@ export function detectModifications(
|
|||
const char = item as GridCharacter
|
||||
|
||||
status.hasAwakening = !!char.awakening
|
||||
status.hasRings = !!(char.rings && char.rings.length > 0) ||
|
||||
!!(char.over_mastery && char.over_mastery.length > 0)
|
||||
status.hasEarring = !!char.earring || !!char.aetherial_mastery
|
||||
status.hasRings = !!(char.over_mastery && char.over_mastery.length > 0)
|
||||
status.hasEarring = !!char.aetherial_mastery
|
||||
status.hasPerpetuity = !!char.perpetuity
|
||||
status.hasTranscendence = !!(char.transcendenceStep && char.transcendenceStep > 0)
|
||||
status.hasUncapLevel = char.uncapLevel !== undefined && char.uncapLevel !== null
|
||||
|
|
|
|||
Loading…
Reference in a new issue