Add sync UI to Party and edit sidebars
- Party.svelte: Pass authUserId to openSearchSidebar, link collection items when adding to party via collectionId - DetailsSidebar: Show sync banner for out-of-sync items, add sync functionality for characters, weapons, and summons - EditCharacterSidebar/EditWeaponSidebar: Add sync banner and button for items linked to collection - party.ts types: Add collectionId and outOfSync fields to grid types
This commit is contained in:
parent
bf2bf8663f
commit
f5d0bbe7da
5 changed files with 303 additions and 15 deletions
|
|
@ -42,7 +42,7 @@
|
||||||
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
|
import CharacterGrid from '$lib/components/grids/CharacterGrid.svelte'
|
||||||
import { openSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte'
|
import { openSearchSidebar } from '$lib/features/search/openSearchSidebar.svelte'
|
||||||
import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte'
|
import PartySegmentedControl from '$lib/components/party/PartySegmentedControl.svelte'
|
||||||
import type { SearchResult } from '$lib/api/adapters'
|
import type { AddItemResult } from '$lib/types/api/search'
|
||||||
import { GridType } from '$lib/types/enums'
|
import { GridType } from '$lib/types/enums'
|
||||||
import Dialog from '$lib/components/ui/Dialog.svelte'
|
import Dialog from '$lib/components/ui/Dialog.svelte'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
|
@ -522,7 +522,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle adding items from the search sidebar
|
// Handle adding items from the search sidebar
|
||||||
async function handleAddItems(items: SearchResult[]) {
|
async function handleAddItems(items: AddItemResult[]) {
|
||||||
if (items.length === 0 || !canEdit()) return
|
if (items.length === 0 || !canEdit()) return
|
||||||
|
|
||||||
const item = items[0]
|
const item = items[0]
|
||||||
|
|
@ -535,7 +535,7 @@
|
||||||
let targetSlot = selectedSlot
|
let targetSlot = selectedSlot
|
||||||
|
|
||||||
// Call appropriate create mutation based on current tab
|
// Call appropriate create mutation based on current tab
|
||||||
// Use granblueId (camelCase) as that's what the SearchResult type uses
|
// Use granblueId (camelCase) as that's what the AddItemResult type uses
|
||||||
const itemId = item.granblueId
|
const itemId = item.granblueId
|
||||||
let result: unknown
|
let result: unknown
|
||||||
|
|
||||||
|
|
@ -544,7 +544,9 @@
|
||||||
partyId: party.id,
|
partyId: party.id,
|
||||||
weaponId: itemId,
|
weaponId: itemId,
|
||||||
position: targetSlot,
|
position: targetSlot,
|
||||||
mainhand: targetSlot === -1
|
mainhand: targetSlot === -1,
|
||||||
|
// Link to collection if item was selected from collection
|
||||||
|
collectionWeaponId: item.collectionId
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if the result is a conflict response
|
// Check if the result is a conflict response
|
||||||
|
|
@ -559,13 +561,17 @@
|
||||||
summonId: itemId,
|
summonId: itemId,
|
||||||
position: targetSlot,
|
position: targetSlot,
|
||||||
main: targetSlot === -1,
|
main: targetSlot === -1,
|
||||||
friend: targetSlot === 6
|
friend: targetSlot === 6,
|
||||||
|
// Link to collection if item was selected from collection
|
||||||
|
collectionSummonId: item.collectionId
|
||||||
})
|
})
|
||||||
} else if (activeTab === GridType.Character) {
|
} else if (activeTab === GridType.Character) {
|
||||||
result = await createCharacter.mutateAsync({
|
result = await createCharacter.mutateAsync({
|
||||||
partyId: party.id,
|
partyId: party.id,
|
||||||
characterId: itemId,
|
characterId: itemId,
|
||||||
position: targetSlot
|
position: targetSlot,
|
||||||
|
// Link to collection if item was selected from collection
|
||||||
|
collectionCharacterId: item.collectionId
|
||||||
})
|
})
|
||||||
|
|
||||||
// Check if the result is a conflict response
|
// Check if the result is a conflict response
|
||||||
|
|
@ -777,10 +783,12 @@
|
||||||
: GridType.Character
|
: GridType.Character
|
||||||
|
|
||||||
// Open the search sidebar with the appropriate type
|
// Open the search sidebar with the appropriate type
|
||||||
|
// Pass authUserId to enable collection mode toggle
|
||||||
openSearchSidebar({
|
openSearchSidebar({
|
||||||
type: opts.type,
|
type: opts.type,
|
||||||
onAddItems: handleAddItems,
|
onAddItems: handleAddItems,
|
||||||
canAddMore: true
|
canAddMore: true,
|
||||||
|
authUserId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,12 @@
|
||||||
import StatsSection from './details/StatsSection.svelte'
|
import StatsSection from './details/StatsSection.svelte'
|
||||||
import SkillsSection from './details/SkillsSection.svelte'
|
import SkillsSection from './details/SkillsSection.svelte'
|
||||||
import TeamView from './details/TeamView.svelte'
|
import TeamView from './details/TeamView.svelte'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import {
|
||||||
|
useSyncGridCharacter,
|
||||||
|
useSyncGridWeapon,
|
||||||
|
useSyncGridSummon
|
||||||
|
} from '$lib/api/mutations/grid.mutations'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: 'character' | 'weapon' | 'summon'
|
type: 'character' | 'weapon' | 'summon'
|
||||||
|
|
@ -17,6 +23,11 @@
|
||||||
|
|
||||||
let { type, item: initialItem }: Props = $props()
|
let { type, item: initialItem }: Props = $props()
|
||||||
|
|
||||||
|
// Sync mutations
|
||||||
|
const syncCharacterMutation = useSyncGridCharacter()
|
||||||
|
const syncWeaponMutation = useSyncGridWeapon()
|
||||||
|
const syncSummonMutation = useSyncGridSummon()
|
||||||
|
|
||||||
// Derive item from partyStore for reactivity, fall back to prop if not in store
|
// Derive item from partyStore for reactivity, fall back to prop if not in store
|
||||||
// This ensures the sidebar updates when party data changes (e.g., uncap level)
|
// This ensures the sidebar updates when party data changes (e.g., uncap level)
|
||||||
let item = $derived.by(() => {
|
let item = $derived.by(() => {
|
||||||
|
|
@ -40,14 +51,21 @@
|
||||||
// Track selected view - updated reactively based on modifiability
|
// Track selected view - updated reactively based on modifiability
|
||||||
let selectedView = $state<'canonical' | 'user'>('user')
|
let selectedView = $state<'canonical' | 'user'>('user')
|
||||||
|
|
||||||
// Update view when switching to items with different modifiability
|
// Track the item ID to detect when switching to a different item
|
||||||
|
let currentItemId = $state<string | undefined>(undefined)
|
||||||
|
|
||||||
|
// Update view when switching to a different item
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!showSegmentedControl) {
|
const itemId = item && 'id' in item ? item.id : undefined
|
||||||
// Force canonical view for non-modifiable items
|
if (itemId !== currentItemId) {
|
||||||
selectedView = 'canonical'
|
currentItemId = itemId
|
||||||
} else if (showSegmentedControl && selectedView === 'canonical') {
|
if (!showSegmentedControl) {
|
||||||
// Switch to user view when selecting a modifiable item
|
// Force canonical view for non-modifiable items
|
||||||
selectedView = 'user'
|
selectedView = 'canonical'
|
||||||
|
} else {
|
||||||
|
// Default to user view for modifiable items
|
||||||
|
selectedView = 'user'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -81,11 +99,58 @@
|
||||||
? ((item as GridWeapon).transcendenceStep ?? null)
|
? ((item as GridWeapon).transcendenceStep ?? null)
|
||||||
: ((item as GridSummon).transcendenceStep ?? null)
|
: ((item as GridSummon).transcendenceStep ?? null)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Sync status - check if linked to collection and out of sync
|
||||||
|
const isLinkedToCollection = $derived.by(() => {
|
||||||
|
if (type === 'character') return !!(item as GridCharacter).collectionCharacterId
|
||||||
|
if (type === 'weapon') return !!(item as GridWeapon).collectionWeaponId
|
||||||
|
if (type === 'summon') return !!(item as GridSummon).collectionSummonId
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const isOutOfSync = $derived.by(() => {
|
||||||
|
if (type === 'character') return (item as GridCharacter).outOfSync ?? false
|
||||||
|
if (type === 'weapon') return (item as GridWeapon).outOfSync ?? false
|
||||||
|
if (type === 'summon') return (item as GridSummon).outOfSync ?? false
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSyncing = $derived(
|
||||||
|
syncCharacterMutation.isPending ||
|
||||||
|
syncWeaponMutation.isPending ||
|
||||||
|
syncSummonMutation.isPending
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handle sync from collection
|
||||||
|
async function handleSync() {
|
||||||
|
const itemId = item && 'id' in item ? item.id : undefined
|
||||||
|
if (!itemId || !isLinkedToCollection) return
|
||||||
|
|
||||||
|
if (type === 'character') {
|
||||||
|
await syncCharacterMutation.mutateAsync({ id: itemId, partyShortcode: '' })
|
||||||
|
} else if (type === 'weapon') {
|
||||||
|
await syncWeaponMutation.mutateAsync({ id: itemId, partyShortcode: '' })
|
||||||
|
} else if (type === 'summon') {
|
||||||
|
await syncSummonMutation.mutateAsync({ id: itemId, partyShortcode: '' })
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="details-sidebar">
|
<div class="details-sidebar">
|
||||||
<ItemHeader {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} />
|
<ItemHeader {type} {item} {itemData} {gridUncapLevel} {gridTranscendence} />
|
||||||
|
|
||||||
|
{#if isLinkedToCollection && isOutOfSync}
|
||||||
|
<div class="sync-banner">
|
||||||
|
<div class="sync-message">
|
||||||
|
<Icon name="refresh-cw" size={14} />
|
||||||
|
<span>Out of sync with collection</span>
|
||||||
|
</div>
|
||||||
|
<button class="sync-button" onclick={handleSync} disabled={isSyncing}>
|
||||||
|
{isSyncing ? 'Syncing...' : 'Sync'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<DetailsSidebarSegmentedControl
|
<DetailsSidebarSegmentedControl
|
||||||
hasModifications={showSegmentedControl}
|
hasModifications={showSegmentedControl}
|
||||||
bind:selectedView
|
bind:selectedView
|
||||||
|
|
@ -116,6 +181,50 @@
|
||||||
gap: spacing.$unit-2x;
|
gap: spacing.$unit-2x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
|
background: var(--warning-bg, rgba(255, 193, 7, 0.15));
|
||||||
|
border: 1px solid var(--warning-border, rgba(255, 193, 7, 0.3));
|
||||||
|
border-radius: spacing.$unit;
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--warning-text, #b59100);
|
||||||
|
|
||||||
|
:global(svg) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-button {
|
||||||
|
padding: spacing.$unit-half spacing.$unit;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--button-bg);
|
||||||
|
border: 1px solid var(--button-border);
|
||||||
|
border-radius: spacing.$unit-half;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--button-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.canonical-view {
|
.canonical-view {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@
|
||||||
type CharacterEditValues,
|
type CharacterEditValues,
|
||||||
type CharacterEditUpdates
|
type CharacterEditUpdates
|
||||||
} from './CharacterEditPane.svelte'
|
} from './CharacterEditPane.svelte'
|
||||||
|
import { useSyncGridCharacter } from '$lib/api/mutations/grid.mutations'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
character: GridCharacter
|
character: GridCharacter
|
||||||
|
|
@ -20,12 +22,29 @@
|
||||||
|
|
||||||
let { character, onSave, onCancel }: Props = $props()
|
let { character, onSave, onCancel }: Props = $props()
|
||||||
|
|
||||||
|
// Sync mutation
|
||||||
|
const syncMutation = useSyncGridCharacter()
|
||||||
|
|
||||||
// Character data shortcut
|
// Character data shortcut
|
||||||
const characterData = $derived(character.character)
|
const characterData = $derived(character.character)
|
||||||
|
|
||||||
// Perpetuity is only available for non-MC characters (position > 0)
|
// Perpetuity is only available for non-MC characters (position > 0)
|
||||||
const canHavePerpetuity = $derived(character.position > 0)
|
const canHavePerpetuity = $derived(character.position > 0)
|
||||||
|
|
||||||
|
// Sync status
|
||||||
|
const isLinkedToCollection = $derived(!!character.collectionCharacterId)
|
||||||
|
const isOutOfSync = $derived(character.outOfSync ?? false)
|
||||||
|
const isSyncing = $derived(syncMutation.isPending)
|
||||||
|
|
||||||
|
// Handle sync from collection
|
||||||
|
async function handleSync() {
|
||||||
|
if (!character.id || !isLinkedToCollection) return
|
||||||
|
await syncMutation.mutateAsync({
|
||||||
|
id: character.id,
|
||||||
|
partyShortcode: '' // Will be handled by cache invalidation
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Convert GridCharacter data to CharacterEditPane format
|
// Convert GridCharacter data to CharacterEditPane format
|
||||||
const currentValues = $derived<CharacterEditValues>({
|
const currentValues = $derived<CharacterEditValues>({
|
||||||
uncapLevel: character.uncapLevel ?? 0,
|
uncapLevel: character.uncapLevel ?? 0,
|
||||||
|
|
@ -66,17 +85,34 @@
|
||||||
gridTranscendence={character.transcendenceStep}
|
gridTranscendence={character.transcendenceStep}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if isLinkedToCollection && isOutOfSync}
|
||||||
|
<div class="sync-banner">
|
||||||
|
<div class="sync-message">
|
||||||
|
<Icon name="refresh-cw" size={14} />
|
||||||
|
<span>Out of sync with collection</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="sync-button"
|
||||||
|
onclick={handleSync}
|
||||||
|
disabled={isSyncing}
|
||||||
|
>
|
||||||
|
{isSyncing ? 'Syncing...' : 'Sync'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<CharacterEditPane
|
<CharacterEditPane
|
||||||
{characterData}
|
{characterData}
|
||||||
{currentValues}
|
{currentValues}
|
||||||
showPerpetuity={canHavePerpetuity}
|
showPerpetuity={canHavePerpetuity}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onCancel={handleCancel}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$src/themes/spacing' as spacing;
|
@use '$src/themes/spacing' as spacing;
|
||||||
|
@use '$src/themes/colors' as colors;
|
||||||
|
@use '$src/themes/typography' as typography;
|
||||||
|
|
||||||
.character-edit-sidebar {
|
.character-edit-sidebar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -84,4 +120,48 @@
|
||||||
height: 100%;
|
height: 100%;
|
||||||
gap: spacing.$unit-4x;
|
gap: spacing.$unit-4x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
|
background: var(--warning-bg, rgba(255, 193, 7, 0.15));
|
||||||
|
border: 1px solid var(--warning-border, rgba(255, 193, 7, 0.3));
|
||||||
|
border-radius: spacing.$unit;
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--warning-text, #b59100);
|
||||||
|
|
||||||
|
:global(svg) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-button {
|
||||||
|
padding: spacing.$unit-half spacing.$unit;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--button-bg);
|
||||||
|
border: 1px solid var(--button-border);
|
||||||
|
border-radius: spacing.$unit-half;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--button-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,10 @@
|
||||||
import AwakeningSelect from './edit/AwakeningSelect.svelte'
|
import AwakeningSelect from './edit/AwakeningSelect.svelte'
|
||||||
import AxSkillSelect from './edit/AxSkillSelect.svelte'
|
import AxSkillSelect from './edit/AxSkillSelect.svelte'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
import { getElementIcon } from '$lib/utils/images'
|
import { getElementIcon } from '$lib/utils/images'
|
||||||
import { seriesHasWeaponKeys, getSeriesSlug } from '$lib/utils/weaponSeries'
|
import { seriesHasWeaponKeys, getSeriesSlug } from '$lib/utils/weaponSeries'
|
||||||
|
import { useSyncGridWeapon } from '$lib/api/mutations/grid.mutations'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
weapon: GridWeapon
|
weapon: GridWeapon
|
||||||
|
|
@ -20,6 +22,23 @@
|
||||||
|
|
||||||
let { weapon, onSave, onCancel }: Props = $props()
|
let { weapon, onSave, onCancel }: Props = $props()
|
||||||
|
|
||||||
|
// Sync mutation
|
||||||
|
const syncMutation = useSyncGridWeapon()
|
||||||
|
|
||||||
|
// Sync status
|
||||||
|
const isLinkedToCollection = $derived(!!weapon.collectionWeaponId)
|
||||||
|
const isOutOfSync = $derived(weapon.outOfSync ?? false)
|
||||||
|
const isSyncing = $derived(syncMutation.isPending)
|
||||||
|
|
||||||
|
// Handle sync from collection
|
||||||
|
async function handleSync() {
|
||||||
|
if (!weapon.id || !isLinkedToCollection) return
|
||||||
|
await syncMutation.mutateAsync({
|
||||||
|
id: weapon.id,
|
||||||
|
partyShortcode: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Local state for edits
|
// Local state for edits
|
||||||
let element = $state(weapon.element ?? weapon.weapon?.element ?? 0)
|
let element = $state(weapon.element ?? weapon.weapon?.element ?? 0)
|
||||||
|
|
||||||
|
|
@ -210,6 +229,22 @@
|
||||||
gridTranscendence={weapon.transcendenceStep}
|
gridTranscendence={weapon.transcendenceStep}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{#if isLinkedToCollection && isOutOfSync}
|
||||||
|
<div class="sync-banner">
|
||||||
|
<div class="sync-message">
|
||||||
|
<Icon name="refresh-cw" size={14} />
|
||||||
|
<span>Out of sync with collection</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="sync-button"
|
||||||
|
onclick={handleSync}
|
||||||
|
disabled={isSyncing}
|
||||||
|
>
|
||||||
|
{isSyncing ? 'Syncing...' : 'Sync'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="edit-sections">
|
<div class="edit-sections">
|
||||||
{#if canChangeElement}
|
{#if canChangeElement}
|
||||||
<DetailsSection title="Element">
|
<DetailsSection title="Element">
|
||||||
|
|
@ -312,6 +347,50 @@
|
||||||
gap: spacing.$unit-4x;
|
gap: spacing.$unit-4x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sync-banner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: spacing.$unit spacing.$unit-2x;
|
||||||
|
background: var(--warning-bg, rgba(255, 193, 7, 0.15));
|
||||||
|
border: 1px solid var(--warning-border, rgba(255, 193, 7, 0.3));
|
||||||
|
border-radius: spacing.$unit;
|
||||||
|
gap: spacing.$unit-2x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--warning-text, #b59100);
|
||||||
|
|
||||||
|
:global(svg) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-button {
|
||||||
|
padding: spacing.$unit-half spacing.$unit;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--button-bg);
|
||||||
|
border: 1px solid var(--button-border);
|
||||||
|
border-radius: spacing.$unit-half;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.15s ease;
|
||||||
|
|
||||||
|
&:hover:not(:disabled) {
|
||||||
|
background: var(--button-bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.weapon-title {
|
.weapon-title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,10 @@ export interface GridWeapon {
|
||||||
type?: Awakening
|
type?: Awakening
|
||||||
level?: number
|
level?: number
|
||||||
}
|
}
|
||||||
|
/** Reference to the source collection weapon if linked */
|
||||||
|
collectionWeaponId?: string
|
||||||
|
/** Whether the grid item is out of sync with its collection source */
|
||||||
|
outOfSync?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// GridCharacter from GridCharacterBlueprint
|
// GridCharacter from GridCharacterBlueprint
|
||||||
|
|
@ -52,6 +56,10 @@ export interface GridCharacter {
|
||||||
overMastery?: Array<{ modifier: number; strength: number }>
|
overMastery?: Array<{ modifier: number; strength: number }>
|
||||||
/** Equipped artifact (can be grid or collection artifact) */
|
/** Equipped artifact (can be grid or collection artifact) */
|
||||||
artifact?: GridArtifact | CollectionArtifact
|
artifact?: GridArtifact | CollectionArtifact
|
||||||
|
/** Reference to the source collection character if linked */
|
||||||
|
collectionCharacterId?: string
|
||||||
|
/** Whether the grid item is out of sync with its collection source */
|
||||||
|
outOfSync?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// GridSummon from GridSummonBlueprint
|
// GridSummon from GridSummonBlueprint
|
||||||
|
|
@ -64,6 +72,10 @@ export interface GridSummon {
|
||||||
uncapLevel?: number
|
uncapLevel?: number
|
||||||
transcendenceStep?: number
|
transcendenceStep?: number
|
||||||
summon: Summon // Named properly, not "object"
|
summon: Summon // Named properly, not "object"
|
||||||
|
/** Reference to the source collection summon if linked */
|
||||||
|
collectionSummonId?: string
|
||||||
|
/** Whether the grid item is out of sync with its collection source */
|
||||||
|
outOfSync?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// JobSkillList for party job skills
|
// JobSkillList for party job skills
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue