817 lines
22 KiB
Svelte
817 lines
22 KiB
Svelte
<svelte:options runes={true} />
|
|
|
|
<script lang="ts">
|
|
import { page } from '$app/stores'
|
|
import { goto } from '$app/navigation'
|
|
import { createQuery, useQueryClient } from '@tanstack/svelte-query'
|
|
import { crewQueries } from '$lib/api/queries/crew.queries'
|
|
import {
|
|
useRemoveMember,
|
|
useUpdateMembership,
|
|
useDeletePhantom,
|
|
useDeclinePhantomClaim
|
|
} from '$lib/api/mutations/crew.mutations'
|
|
import { crewAdapter } from '$lib/api/adapters/crew.adapter'
|
|
import { crewStore } from '$lib/stores/crew.store.svelte'
|
|
import Button from '$lib/components/ui/Button.svelte'
|
|
import Dialog from '$lib/components/ui/Dialog.svelte'
|
|
import DropdownMenu from '$lib/components/ui/DropdownMenu.svelte'
|
|
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
|
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
|
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
|
import SettingsRow from '$lib/components/ui/SettingsRow.svelte'
|
|
import Switch from '$lib/components/ui/switch/Switch.svelte'
|
|
import CrewHeader from '$lib/components/crew/CrewHeader.svelte'
|
|
import MemberRow from '$lib/components/crew/MemberRow.svelte'
|
|
import PhantomRow from '$lib/components/crew/PhantomRow.svelte'
|
|
import ScoutUserModal from '$lib/components/crew/ScoutUserModal.svelte'
|
|
import BulkPhantomModal from '$lib/components/crew/BulkPhantomModal.svelte'
|
|
import AssignPhantomModal from '$lib/components/crew/AssignPhantomModal.svelte'
|
|
import ConfirmClaimModal from '$lib/components/crew/ConfirmClaimModal.svelte'
|
|
import { DropdownMenu as DropdownMenuBase } from 'bits-ui'
|
|
import type {
|
|
MemberFilter,
|
|
CrewMembership,
|
|
PhantomPlayer,
|
|
CrewInvitation
|
|
} from '$lib/types/api/crew'
|
|
import type { PageData } from './$types'
|
|
|
|
interface Props {
|
|
data: PageData
|
|
}
|
|
|
|
let { data }: Props = $props()
|
|
|
|
const queryClient = useQueryClient()
|
|
|
|
// Get filter from URL
|
|
const filter = $derived<MemberFilter>(
|
|
($page.url.searchParams.get('filter') as MemberFilter) || 'all'
|
|
)
|
|
|
|
// Query for members based on filter
|
|
const membersQuery = createQuery(() => crewQueries.members(filter))
|
|
|
|
// Query for active members (needed for roster cap check regardless of current filter)
|
|
const activeQuery = createQuery(() => ({
|
|
...crewQueries.members('active'),
|
|
enabled: filter !== 'active' // Only fetch separately if not already viewing active
|
|
}))
|
|
|
|
// Query for pending invitations (officers only)
|
|
const invitationsQuery = createQuery(() => ({
|
|
...crewQueries.crewInvitations(crewStore.crew?.id ?? ''),
|
|
enabled: crewStore.isOfficer && !!crewStore.crew?.id
|
|
}))
|
|
|
|
// Calculate total active roster size (members + phantoms)
|
|
const activeRosterSize = $derived.by(() => {
|
|
// Use active filter data if viewing active, otherwise use dedicated query
|
|
const activeMembers =
|
|
filter === 'active' ? (membersQuery.data?.members ?? []) : (activeQuery.data?.members ?? [])
|
|
const activePhantoms =
|
|
filter === 'active' ? (membersQuery.data?.phantoms ?? []) : (activeQuery.data?.phantoms ?? [])
|
|
|
|
const activeMemberCount = activeMembers.filter((m) => !m.retired).length
|
|
const activePhantomCount = activePhantoms.filter((p) => !p.retired).length
|
|
return activeMemberCount + activePhantomCount
|
|
})
|
|
|
|
const isRosterFull = $derived(activeRosterSize >= 30)
|
|
|
|
// Mutations
|
|
const removeMemberMutation = useRemoveMember()
|
|
const updateMembershipMutation = useUpdateMembership()
|
|
const deletePhantomMutation = useDeletePhantom()
|
|
const declinePhantomClaimMutation = useDeclinePhantomClaim()
|
|
|
|
// Filter options - Pending only shown to officers
|
|
const filterOptions = $derived.by(() => {
|
|
const options: { value: MemberFilter; label: string }[] = [
|
|
{ value: 'all', label: 'All' },
|
|
{ value: 'active', label: 'Active' },
|
|
{ value: 'phantom', label: 'Phantoms' }
|
|
]
|
|
if (crewStore.isOfficer) {
|
|
options.push({ value: 'pending', label: 'Pending' })
|
|
}
|
|
options.push({ value: 'retired', label: 'Retired' })
|
|
return options
|
|
})
|
|
|
|
// Change filter
|
|
function handleFilterChange(newFilter: MemberFilter) {
|
|
const url = new URL($page.url)
|
|
if (newFilter === 'all') {
|
|
url.searchParams.delete('filter')
|
|
} else {
|
|
url.searchParams.set('filter', newFilter)
|
|
}
|
|
goto(url.toString(), { replaceState: true })
|
|
}
|
|
|
|
// Dialog state for member actions
|
|
let confirmDialogOpen = $state(false)
|
|
let confirmAction = $state<'remove' | 'promote' | 'demote' | null>(null)
|
|
let selectedMember = $state<CrewMembership | null>(null)
|
|
|
|
// Dialog state for editing member/phantom
|
|
let editDialogOpen = $state(false)
|
|
let editingMember = $state<CrewMembership | null>(null)
|
|
let editingPhantom = $state<PhantomPlayer | null>(null)
|
|
let editJoinDate = $state('')
|
|
let editRetired = $state(false)
|
|
|
|
// Dialog state for scout modal
|
|
let scoutModalOpen = $state(false)
|
|
|
|
// Dialog state for phantom creation
|
|
let bulkPhantomDialogOpen = $state(false)
|
|
|
|
// Dialog state for phantom deletion confirmation
|
|
let deletePhantomDialogOpen = $state(false)
|
|
let phantomToDelete = $state<PhantomPlayer | null>(null)
|
|
|
|
// Dialog state for phantom assignment
|
|
let assignPhantomDialogOpen = $state(false)
|
|
let phantomToAssign = $state<PhantomPlayer | null>(null)
|
|
|
|
// Dialog state for confirm claim
|
|
let confirmClaimDialogOpen = $state(false)
|
|
let phantomToClaim = $state<PhantomPlayer | null>(null)
|
|
|
|
// Member actions
|
|
function openRemoveDialog(member: CrewMembership) {
|
|
selectedMember = member
|
|
confirmAction = 'remove'
|
|
confirmDialogOpen = true
|
|
}
|
|
|
|
function openPromoteDialog(member: CrewMembership) {
|
|
selectedMember = member
|
|
confirmAction = 'promote'
|
|
confirmDialogOpen = true
|
|
}
|
|
|
|
function openDemoteDialog(member: CrewMembership) {
|
|
selectedMember = member
|
|
confirmAction = 'demote'
|
|
confirmDialogOpen = true
|
|
}
|
|
|
|
async function handleConfirmAction() {
|
|
if (!selectedMember || !crewStore.crew) return
|
|
|
|
try {
|
|
if (confirmAction === 'remove') {
|
|
await removeMemberMutation.mutateAsync({
|
|
crewId: crewStore.crew.id,
|
|
membershipId: selectedMember.id
|
|
})
|
|
} else if (confirmAction === 'promote') {
|
|
await updateMembershipMutation.mutateAsync({
|
|
crewId: crewStore.crew.id,
|
|
membershipId: selectedMember.id,
|
|
input: { role: 'vice_captain' }
|
|
})
|
|
} else if (confirmAction === 'demote') {
|
|
await updateMembershipMutation.mutateAsync({
|
|
crewId: crewStore.crew.id,
|
|
membershipId: selectedMember.id,
|
|
input: { role: 'member' }
|
|
})
|
|
}
|
|
} catch (error) {
|
|
console.error('Action failed:', error)
|
|
}
|
|
|
|
confirmDialogOpen = false
|
|
selectedMember = null
|
|
confirmAction = null
|
|
}
|
|
|
|
// Member/phantom editing
|
|
function openEditMemberDialog(member: CrewMembership) {
|
|
editingMember = member
|
|
editingPhantom = null
|
|
// Format date for input
|
|
editJoinDate = member.joinedAt ? (member.joinedAt.split('T')[0] ?? '') : ''
|
|
editRetired = member.retired
|
|
editDialogOpen = true
|
|
}
|
|
|
|
function openEditPhantomDialog(phantom: PhantomPlayer) {
|
|
editingPhantom = phantom
|
|
editingMember = null
|
|
editJoinDate = phantom.joinedAt ? (phantom.joinedAt.split('T')[0] ?? '') : ''
|
|
editRetired = phantom.retired
|
|
editDialogOpen = true
|
|
}
|
|
|
|
async function handleSaveEdit() {
|
|
if (!crewStore.crew) return
|
|
|
|
try {
|
|
if (editingMember) {
|
|
await updateMembershipMutation.mutateAsync({
|
|
crewId: crewStore.crew.id,
|
|
membershipId: editingMember.id,
|
|
input: { joinedAt: editJoinDate, retired: editRetired }
|
|
})
|
|
} else if (editingPhantom) {
|
|
// Call the phantom update directly through the adapter
|
|
await crewAdapter.updatePhantom(crewStore.crew.id, editingPhantom.id, {
|
|
joinedAt: editJoinDate,
|
|
retired: editRetired
|
|
})
|
|
// Invalidate members query
|
|
membersQuery.refetch()
|
|
}
|
|
// Invalidate GW event queries since membersDuringEvent depends on join dates
|
|
queryClient.invalidateQueries({ queryKey: ['crew', 'gw'] })
|
|
} catch (error) {
|
|
console.error('Failed to update:', error)
|
|
}
|
|
|
|
editDialogOpen = false
|
|
editingMember = null
|
|
editingPhantom = null
|
|
editJoinDate = ''
|
|
editRetired = false
|
|
}
|
|
|
|
function openDeletePhantomDialog(phantom: PhantomPlayer) {
|
|
phantomToDelete = phantom
|
|
deletePhantomDialogOpen = true
|
|
}
|
|
|
|
async function handleConfirmDeletePhantom() {
|
|
if (!crewStore.crew || !phantomToDelete) return
|
|
|
|
try {
|
|
await deletePhantomMutation.mutateAsync({
|
|
crewId: crewStore.crew.id,
|
|
phantomId: phantomToDelete.id
|
|
})
|
|
} catch (error) {
|
|
console.error('Failed to delete phantom:', error)
|
|
}
|
|
|
|
deletePhantomDialogOpen = false
|
|
phantomToDelete = null
|
|
}
|
|
|
|
// Phantom assignment
|
|
function openAssignPhantomDialog(phantom: PhantomPlayer) {
|
|
phantomToAssign = phantom
|
|
assignPhantomDialogOpen = true
|
|
}
|
|
|
|
// Confirm claim (opens modal)
|
|
function openConfirmClaimDialog(phantom: PhantomPlayer) {
|
|
phantomToClaim = phantom
|
|
confirmClaimDialogOpen = true
|
|
}
|
|
|
|
// Decline claim (direct action, no confirmation needed)
|
|
async function handleDeclineClaim(phantom: PhantomPlayer) {
|
|
if (!crewStore.crew) return
|
|
|
|
try {
|
|
await declinePhantomClaimMutation.mutateAsync({
|
|
crewId: crewStore.crew.id,
|
|
phantomId: phantom.id
|
|
})
|
|
} catch (error) {
|
|
console.error('Failed to decline phantom claim:', error)
|
|
}
|
|
}
|
|
|
|
// Format date
|
|
function formatDate(dateString: string): string {
|
|
return new Date(dateString).toLocaleDateString(undefined, {
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric'
|
|
})
|
|
}
|
|
|
|
// Check if invitation is expired
|
|
function isInvitationExpired(expiresAt: string): boolean {
|
|
return new Date(expiresAt) < new Date()
|
|
}
|
|
|
|
// Get pending (non-expired) invitations count
|
|
const pendingInvitationsCount = $derived(
|
|
invitationsQuery.data?.filter((inv) => !isInvitationExpired(inv.expiresAt)).length ?? 0
|
|
)
|
|
|
|
// Get phantoms with pending claims (assigned but not confirmed)
|
|
const pendingClaimPhantoms = $derived(
|
|
membersQuery.data?.phantoms?.filter((p) => p.claimedBy && !p.claimConfirmed) ?? []
|
|
)
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>Crew Members / granblue.team</title>
|
|
</svelte:head>
|
|
|
|
<div class="page">
|
|
<div class="card">
|
|
<CrewHeader title="Members" backHref="/crew">
|
|
{#snippet belowTitle()}
|
|
<div class="filter-tabs">
|
|
{#each filterOptions as option}
|
|
<button
|
|
class="filter-tab"
|
|
class:active={filter === option.value}
|
|
onclick={() => handleFilterChange(option.value)}
|
|
>
|
|
{option.label}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
{/snippet}
|
|
{#snippet actions()}
|
|
{#if crewStore.isOfficer}
|
|
<Button
|
|
variant="secondary"
|
|
size="small"
|
|
disabled={isRosterFull}
|
|
onclick={() => (scoutModalOpen = true)}
|
|
>
|
|
Scout
|
|
</Button>
|
|
<DropdownMenu>
|
|
{#snippet trigger({ props })}
|
|
<Button variant="secondary" size="small" iconOnly icon="ellipsis" {...props} />
|
|
{/snippet}
|
|
{#snippet menu()}
|
|
<DropdownMenuBase.Item
|
|
class="dropdown-menu-item"
|
|
onclick={() => (bulkPhantomDialogOpen = true)}
|
|
>
|
|
Add phantoms...
|
|
</DropdownMenuBase.Item>
|
|
{/snippet}
|
|
</DropdownMenu>
|
|
{/if}
|
|
{/snippet}
|
|
</CrewHeader>
|
|
|
|
<!-- Pending Invitations (shown when filter is 'pending') -->
|
|
{#if filter === 'pending'}
|
|
{#if invitationsQuery.isLoading}
|
|
<div class="loading-state">
|
|
<p>Loading...</p>
|
|
</div>
|
|
{:else if invitationsQuery.data && invitationsQuery.data.length > 0}
|
|
<ul class="member-list">
|
|
{#each invitationsQuery.data as invitation}
|
|
{@const expired = isInvitationExpired(invitation.expiresAt)}
|
|
<li class="invitation-row" class:expired>
|
|
<div class="invitation-info">
|
|
<span class="invited-user">{invitation.user?.username ?? 'Unknown'}</span>
|
|
{#if invitation.invitedBy}
|
|
<span class="invited-by">
|
|
Invited by {invitation.invitedBy.username}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
<div class="invitation-status">
|
|
{#if expired}
|
|
<span class="status-badge expired">Expired</span>
|
|
{:else}
|
|
<span class="expires-text">Expires {formatDate(invitation.expiresAt)}</span>
|
|
{/if}
|
|
</div>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
{:else}
|
|
<div class="empty-state">
|
|
<p>No pending invitations.</p>
|
|
</div>
|
|
{/if}
|
|
{:else if membersQuery.isLoading}
|
|
<div class="loading-state">
|
|
<p>Loading...</p>
|
|
</div>
|
|
{:else if membersQuery.isError}
|
|
<div class="error-state">
|
|
<p>Failed to load members</p>
|
|
</div>
|
|
{:else}
|
|
<!-- Regular members -->
|
|
{#if membersQuery.data?.members && membersQuery.data.members.length > 0}
|
|
<ul class="member-list">
|
|
{#each membersQuery.data.members as member}
|
|
<MemberRow
|
|
{member}
|
|
onEdit={() => openEditMemberDialog(member)}
|
|
onPromote={() => openPromoteDialog(member)}
|
|
onDemote={() => openDemoteDialog(member)}
|
|
onRemove={() => openRemoveDialog(member)}
|
|
/>
|
|
{/each}
|
|
</ul>
|
|
{:else if filter !== 'phantom'}
|
|
<p class="empty-state">No members found</p>
|
|
{/if}
|
|
|
|
<!-- Pending Claims Section (officers only) -->
|
|
{#if crewStore.isOfficer && pendingClaimPhantoms.length > 0 && (filter === 'all' || filter === 'phantom')}
|
|
<div class="section-divider pending-claims">
|
|
<span>Pending Claims ({pendingClaimPhantoms.length})</span>
|
|
</div>
|
|
<ul class="member-list">
|
|
{#each pendingClaimPhantoms as phantom}
|
|
<PhantomRow
|
|
{phantom}
|
|
currentUserId={crewStore.membership?.user?.id}
|
|
onEdit={() => openEditPhantomDialog(phantom)}
|
|
onDelete={() => openDeletePhantomDialog(phantom)}
|
|
onAssign={() => openAssignPhantomDialog(phantom)}
|
|
onAccept={() => openConfirmClaimDialog(phantom)}
|
|
onDecline={() => handleDeclineClaim(phantom)}
|
|
/>
|
|
{/each}
|
|
</ul>
|
|
{/if}
|
|
|
|
<!-- Phantom players -->
|
|
{#if membersQuery.data?.phantoms && membersQuery.data.phantoms.length > 0}
|
|
{#if filter === 'all' && membersQuery.data.members.length > 0}
|
|
<div class="section-divider">
|
|
<span>Phantom Players</span>
|
|
</div>
|
|
{/if}
|
|
<ul class="member-list">
|
|
{#each membersQuery.data.phantoms as phantom}
|
|
<PhantomRow
|
|
{phantom}
|
|
currentUserId={crewStore.membership?.user?.id}
|
|
onEdit={() => openEditPhantomDialog(phantom)}
|
|
onDelete={() => openDeletePhantomDialog(phantom)}
|
|
onAssign={() => openAssignPhantomDialog(phantom)}
|
|
onAccept={() => openConfirmClaimDialog(phantom)}
|
|
onDecline={() => handleDeclineClaim(phantom)}
|
|
/>
|
|
{/each}
|
|
</ul>
|
|
{:else if filter === 'phantom'}
|
|
<div class="empty-state">
|
|
<p>No phantom players.</p>
|
|
{#if crewStore.isOfficer}
|
|
<Button variant="secondary" size="small" onclick={() => (bulkPhantomDialogOpen = true)}>
|
|
Add phantoms...
|
|
</Button>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Confirm Action Dialog -->
|
|
<Dialog bind:open={confirmDialogOpen}>
|
|
{#snippet children()}
|
|
<ModalHeader
|
|
title={confirmAction === 'remove'
|
|
? 'Remove Member'
|
|
: confirmAction === 'promote'
|
|
? 'Promote to Vice Captain'
|
|
: 'Demote to Member'}
|
|
/>
|
|
|
|
<ModalBody>
|
|
<p class="confirm-message">
|
|
{#if confirmAction === 'remove'}
|
|
Are you sure you want to remove {selectedMember?.user?.username ?? 'this member'} from the
|
|
crew?
|
|
{:else if confirmAction === 'promote'}
|
|
Promote {selectedMember?.user?.username ?? 'this member'} to Vice Captain?
|
|
{:else if confirmAction === 'demote'}
|
|
Demote {selectedMember?.user?.username ?? 'this member'} from Vice Captain to Member?
|
|
{/if}
|
|
</p>
|
|
</ModalBody>
|
|
|
|
<ModalFooter
|
|
onCancel={() => (confirmDialogOpen = false)}
|
|
primaryAction={{
|
|
label:
|
|
confirmAction === 'remove'
|
|
? 'Remove'
|
|
: confirmAction === 'promote'
|
|
? 'Promote'
|
|
: 'Demote',
|
|
onclick: handleConfirmAction,
|
|
destructive: confirmAction === 'remove'
|
|
}}
|
|
/>
|
|
{/snippet}
|
|
</Dialog>
|
|
|
|
<!-- Edit Member/Phantom Dialog -->
|
|
<Dialog bind:open={editDialogOpen}>
|
|
{#snippet children()}
|
|
<ModalHeader title="Edit player" />
|
|
|
|
<ModalBody>
|
|
<div class="modal-form">
|
|
<div class="form-fields">
|
|
<div class="form-field">
|
|
<label for="joinDate">Join date</label>
|
|
<input id="joinDate" type="date" bind:value={editJoinDate} class="date-input" />
|
|
</div>
|
|
<p class="help-text">
|
|
This date is used to determine which events a member was active for when adding
|
|
historical GW scores.
|
|
</p>
|
|
<SettingsRow title="Retired" subtitle="This player is no longer a part of the crew">
|
|
{#snippet control()}
|
|
<Switch bind:checked={editRetired} name="retired" />
|
|
{/snippet}
|
|
</SettingsRow>
|
|
</div>
|
|
</div>
|
|
</ModalBody>
|
|
|
|
<ModalFooter
|
|
onCancel={() => (editDialogOpen = false)}
|
|
primaryAction={{
|
|
label: 'Save',
|
|
onclick: handleSaveEdit,
|
|
disabled: !editJoinDate
|
|
}}
|
|
/>
|
|
{/snippet}
|
|
</Dialog>
|
|
|
|
<!-- Delete Phantom Confirmation Dialog -->
|
|
<Dialog bind:open={deletePhantomDialogOpen}>
|
|
{#snippet children()}
|
|
<ModalHeader title="Delete Phantom Player" />
|
|
|
|
<ModalBody>
|
|
<p class="confirm-message">
|
|
Are you sure you want to delete {phantomToDelete?.name ?? 'this phantom player'}? This
|
|
action cannot be undone.
|
|
</p>
|
|
</ModalBody>
|
|
|
|
<ModalFooter
|
|
onCancel={() => (deletePhantomDialogOpen = false)}
|
|
primaryAction={{
|
|
label: 'Delete',
|
|
onclick: handleConfirmDeletePhantom,
|
|
destructive: true
|
|
}}
|
|
/>
|
|
{/snippet}
|
|
</Dialog>
|
|
|
|
<!-- Scout User Modal -->
|
|
{#if crewStore.crew?.id}
|
|
<ScoutUserModal bind:open={scoutModalOpen} crewId={crewStore.crew.id} />
|
|
<BulkPhantomModal bind:open={bulkPhantomDialogOpen} crewId={crewStore.crew.id} />
|
|
<AssignPhantomModal
|
|
bind:open={assignPhantomDialogOpen}
|
|
crewId={crewStore.crew.id}
|
|
phantom={phantomToAssign}
|
|
/>
|
|
<ConfirmClaimModal
|
|
bind:open={confirmClaimDialogOpen}
|
|
crewId={crewStore.crew.id}
|
|
phantom={phantomToClaim}
|
|
/>
|
|
{/if}
|
|
|
|
<style lang="scss">
|
|
@use '$src/themes/colors' as colors;
|
|
@use '$src/themes/effects' as effects;
|
|
@use '$src/themes/spacing' as spacing;
|
|
@use '$src/themes/typography' as typography;
|
|
@use '$src/themes/layout' as layout;
|
|
|
|
.page {
|
|
margin: 0 auto;
|
|
max-width: var(--main-max-width);
|
|
}
|
|
|
|
.card {
|
|
background: var(--card-bg);
|
|
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
|
border-radius: layout.$page-corner;
|
|
box-shadow: effects.$page-elevation;
|
|
overflow: hidden;
|
|
|
|
:global(.header-info) {
|
|
gap: spacing.$unit-2x;
|
|
}
|
|
}
|
|
|
|
.filter-tabs {
|
|
display: flex;
|
|
gap: spacing.$unit-half;
|
|
}
|
|
|
|
.filter-tab {
|
|
padding: 4px spacing.$unit;
|
|
background: none;
|
|
border: none;
|
|
border-radius: layout.$item-corner-small;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: typography.$font-small;
|
|
transition:
|
|
color 0.15s,
|
|
background-color 0.15s;
|
|
|
|
&:hover {
|
|
color: var(--text-primary);
|
|
background: rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
&.active {
|
|
color: var(--text-primary);
|
|
background: rgba(0, 0, 0, 0.06);
|
|
font-weight: typography.$medium;
|
|
}
|
|
}
|
|
|
|
.loading-state,
|
|
.error-state {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
padding: spacing.$unit-4x;
|
|
color: var(--text-secondary);
|
|
font-size: typography.$font-small;
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: spacing.$unit-2x;
|
|
text-align: center;
|
|
color: var(--text-secondary);
|
|
padding: spacing.$unit-4x;
|
|
font-size: typography.$font-small;
|
|
|
|
p {
|
|
margin: 0;
|
|
}
|
|
}
|
|
|
|
.member-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: spacing.$unit;
|
|
}
|
|
|
|
.section-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: spacing.$unit spacing.$unit-2x;
|
|
background: rgba(0, 0, 0, 0.02);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
|
|
|
&.pending-claims {
|
|
background: var(--color-yellow-light, #fef9c3);
|
|
border-bottom-color: var(--color-yellow-dark, #854d0e);
|
|
|
|
span {
|
|
color: var(--color-yellow-dark, #854d0e);
|
|
}
|
|
}
|
|
|
|
span {
|
|
font-size: typography.$font-small;
|
|
font-weight: typography.$medium;
|
|
color: var(--text-secondary);
|
|
}
|
|
}
|
|
|
|
// Confirm dialog styles
|
|
.confirm-message {
|
|
color: var(--text-primary);
|
|
line-height: 1.5;
|
|
margin: 0;
|
|
}
|
|
|
|
// Modal form styles
|
|
.modal-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: spacing.$unit-3x;
|
|
}
|
|
|
|
.form-fields {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: spacing.$unit-3x;
|
|
}
|
|
|
|
.form-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: spacing.$unit-half;
|
|
|
|
label {
|
|
font-size: typography.$font-small;
|
|
font-weight: typography.$medium;
|
|
color: var(--text-primary);
|
|
}
|
|
}
|
|
|
|
:global(fieldset) {
|
|
border: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.date-input {
|
|
padding: spacing.$unit spacing.$unit-2x;
|
|
border: none;
|
|
border-radius: layout.$input-corner;
|
|
font-size: typography.$font-regular;
|
|
font-family: inherit;
|
|
background: var(--input-bound-bg);
|
|
color: var(--text-primary);
|
|
width: 100%;
|
|
|
|
&:hover {
|
|
background: var(--input-bound-bg-hover);
|
|
}
|
|
|
|
&:focus {
|
|
outline: none;
|
|
background: var(--input-bound-bg-hover);
|
|
}
|
|
}
|
|
|
|
.help-text {
|
|
font-size: typography.$font-small;
|
|
color: var(--text-secondary);
|
|
margin: 0;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
// Invitation row styles
|
|
.invitation-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: spacing.$unit spacing.$unit spacing.$unit spacing.$unit-2x;
|
|
border-radius: layout.$item-corner;
|
|
transition: background-color 0.15s;
|
|
|
|
&:hover {
|
|
background: rgba(0, 0, 0, 0.03);
|
|
}
|
|
|
|
&.expired {
|
|
opacity: 0.5;
|
|
}
|
|
}
|
|
|
|
.invitation-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.invited-user {
|
|
font-size: typography.$font-small;
|
|
font-weight: typography.$medium;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.invited-by {
|
|
font-size: typography.$font-tiny;
|
|
color: var(--text-tertiary);
|
|
}
|
|
|
|
.invitation-status {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.expires-text {
|
|
font-size: typography.$font-tiny;
|
|
color: var(--text-tertiary);
|
|
}
|
|
|
|
.status-badge.expired {
|
|
font-size: typography.$font-tiny;
|
|
color: colors.$error;
|
|
background: colors.$error--bg--light;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: typography.$medium;
|
|
}
|
|
</style>
|