diff --git a/docs/details-sidebar-segmented-control.md b/docs/details-sidebar-segmented-control.md
new file mode 100644
index 00000000..4670a768
--- /dev/null
+++ b/docs/details-sidebar-segmented-control.md
@@ -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
+
+```
+
+#### 2. `ModificationSection.svelte`
+Generic wrapper for modification sections with consistent styling.
+
+```svelte
+
+
+{#if visible}
+
+
{title}
+
+ {@render children()}
+
+
+{/if}
+```
+
+#### 3. `AwakeningDisplay.svelte`
+Reusable awakening display component for both weapons and characters.
+
+```svelte
+
+```
+
+#### 4. `WeaponKeysList.svelte`
+Component for displaying weapon keys with proper icons and formatting.
+
+```svelte
+
+```
+
+#### 5. `StatModifierItem.svelte`
+Generic component for displaying stat modifications (rings, earrings, etc.).
+
+```svelte
+
+
+
+ {#if icon}{/if}
+ {label}
+ {value}{suffix}
+
+```
+
+#### 6. `UncapStatusDisplay.svelte`
+Dedicated component for showing current uncap/transcendence status.
+
+```svelte
+
+```
+
+### 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
+
+
+
+{#if selectedView === 'canonical'}
+
+{:else}
+
+
+
+
+
+
+
+ {#if type === 'weapon'}
+
+
+
+ {/if}
+
+
+{/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
\ No newline at end of file
diff --git a/docs/transcendence-popover-fix.md b/docs/transcendence-popover-fix.md
new file mode 100644
index 00000000..82cb370b
--- /dev/null
+++ b/docs/transcendence-popover-fix.md
@@ -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(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}
+
+
+
+
+
+
+
+
+
+
+{/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/`
\ No newline at end of file
diff --git a/src/assets/icons/perpetuity/empty.svg b/src/assets/icons/perpetuity/empty.svg
new file mode 100644
index 00000000..47ec18e1
--- /dev/null
+++ b/src/assets/icons/perpetuity/empty.svg
@@ -0,0 +1,49 @@
+
diff --git a/src/assets/icons/perpetuity/filled.svg b/src/assets/icons/perpetuity/filled.svg
new file mode 100644
index 00000000..d8df6ed8
--- /dev/null
+++ b/src/assets/icons/perpetuity/filled.svg
@@ -0,0 +1,53 @@
+
diff --git a/src/lib/api/adapters/grid.adapter.ts b/src/lib/api/adapters/grid.adapter.ts
index 72d33bfb..2f044701 100644
--- a/src/lib/api/adapters/grid.adapter.ts
+++ b/src/lib/api/adapters/grid.adapter.ts
@@ -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
- 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): Promise {
- return this.request('/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, headers?: Record): Promise {
- return this.request(`/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): Promise {
- return this.request('/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): Promise {
- return this.request('/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): Promise {
const { id, position, container, partyId } = params
- return this.request(`/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): Promise {
- return this.request('/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, headers?: Record): Promise {
- return this.request(`/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): Promise {
- return this.request('/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): Promise {
- return this.request('/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): Promise {
const { id, position, container, partyId } = params
- return this.request(`/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): Promise {
- return this.request('/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, headers?: Record): Promise {
- return this.request(`/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): Promise {
- return this.request('/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): Promise {
const { id, position, container, partyId } = params
- return this.request(`/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
}
/**
diff --git a/src/lib/api/adapters/party.adapter.ts b/src/lib/api/adapters/party.adapter.ts
index b22f65f0..bc1e25cc 100644
--- a/src/lib/api/adapters/party.adapter.ts
+++ b/src/lib/api/adapters/party.adapter.ts
@@ -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
- 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
- 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
- awakenings?: Array<{
- id: string
- level: number
- }>
- character: {
- id: string
- granblueId: string
- name: Record
- element: number
- rarity: number
- }
-}
+// GridCharacter type is imported from types/api/party
/**
* Parameters for creating a new party
diff --git a/src/lib/components/sidebar/DetailsSidebar.svelte b/src/lib/components/sidebar/DetailsSidebar.svelte
index f5e26086..70206c04 100644
--- a/src/lib/components/sidebar/DetailsSidebar.svelte
+++ b/src/lib/components/sidebar/DetailsSidebar.svelte
@@ -345,7 +345,7 @@
{#if modificationStatus.hasRings}
- {#each (char.rings || char.over_mastery || []) as ring}
+ {#each (char.over_mastery || []) as ring}
- 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('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('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)
+ }
+ }