move title, user, and actions into description tile
This commit is contained in:
parent
9fea625e7c
commit
23c4425a2a
3 changed files with 198 additions and 157 deletions
|
|
@ -36,7 +36,6 @@
|
||||||
// Utilities
|
// Utilities
|
||||||
import { getLocalId } from '$lib/utils/localId'
|
import { getLocalId } from '$lib/utils/localId'
|
||||||
import { getEditKey, storeEditKey, computeEditability } from '$lib/utils/editKeys'
|
import { getEditKey, storeEditKey, computeEditability } from '$lib/utils/editKeys'
|
||||||
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
|
||||||
|
|
||||||
import { createDragDropContext, type DragOperation } from '$lib/composables/drag-drop.svelte'
|
import { createDragDropContext, type DragOperation } from '$lib/composables/drag-drop.svelte'
|
||||||
import WeaponGrid from '$lib/components/grids/WeaponGrid.svelte'
|
import WeaponGrid from '$lib/components/grids/WeaponGrid.svelte'
|
||||||
|
|
@ -888,41 +887,13 @@
|
||||||
<div class="page-wrap">
|
<div class="page-wrap">
|
||||||
<div class="track">
|
<div class="track">
|
||||||
<section class="party-container">
|
<section class="party-container">
|
||||||
<header class="party-header">
|
<PartyInfoGrid
|
||||||
<div class="party-info">
|
{party}
|
||||||
<h1>{party.name || '(untitled party)'}</h1>
|
canEdit={canEdit()}
|
||||||
{#if party.user}
|
onOpenDescription={openDescriptionPanel}
|
||||||
{@const avatarSrc = getAvatarSrc(party.user.avatar?.picture)}
|
onOpenEdit={openSettingsPanel}
|
||||||
{@const avatarSrcSet = getAvatarSrcSet(party.user.avatar?.picture)}
|
>
|
||||||
<div class="creator">
|
{#snippet menu()}
|
||||||
<a href="/{party.user.username}" class="creator-link">
|
|
||||||
<div class="avatar-wrapper {party.user.avatar?.element || ''}">
|
|
||||||
{#if party.user.avatar?.picture}
|
|
||||||
<img
|
|
||||||
class="avatar"
|
|
||||||
alt={`Avatar of ${party.user.username}`}
|
|
||||||
src={avatarSrc}
|
|
||||||
srcset={avatarSrcSet}
|
|
||||||
width="32"
|
|
||||||
height="32"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="avatar-placeholder" aria-hidden="true"></div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<span class="username">{party.user.username}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="party-actions">
|
|
||||||
{#if canEdit()}
|
|
||||||
<Button variant="secondary" size="small" onclick={openSettingsPanel} disabled={loading}>
|
|
||||||
Edit
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<DropdownMenu.Root>
|
<DropdownMenu.Root>
|
||||||
<DropdownMenu.Trigger class="party-actions-trigger" aria-label="Open actions menu">
|
<DropdownMenu.Trigger class="party-actions-trigger" aria-label="Open actions menu">
|
||||||
<Icon name="ellipsis" size={14} />
|
<Icon name="ellipsis" size={14} />
|
||||||
|
|
@ -962,15 +933,8 @@
|
||||||
</DropdownMenu.Content>
|
</DropdownMenu.Content>
|
||||||
</DropdownMenu.Portal>
|
</DropdownMenu.Portal>
|
||||||
</DropdownMenu.Root>
|
</DropdownMenu.Root>
|
||||||
</div>
|
{/snippet}
|
||||||
</header>
|
</PartyInfoGrid>
|
||||||
|
|
||||||
<PartyInfoGrid
|
|
||||||
{party}
|
|
||||||
canEdit={canEdit()}
|
|
||||||
onOpenDescription={openDescriptionPanel}
|
|
||||||
onOpenEdit={openSettingsPanel}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PartySegmentedControl
|
<PartySegmentedControl
|
||||||
selectedTab={activeTab}
|
selectedTab={activeTab}
|
||||||
|
|
@ -1081,107 +1045,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.party-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: start;
|
|
||||||
vertical-align: middle;
|
|
||||||
align-items: center;
|
|
||||||
padding: $unit-2x 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-info {
|
|
||||||
flex-grow: 1;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0 0 $unit-fourth 0;
|
|
||||||
font-size: $font-xlarge;
|
|
||||||
font-weight: $bold;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.creator {
|
|
||||||
margin-top: $unit-half;
|
|
||||||
|
|
||||||
&-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: $unit-three-quarter;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
@include smooth-transition($duration-standard, color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--text-tertiary-hover);
|
|
||||||
|
|
||||||
.avatar-wrapper {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-wrapper {
|
|
||||||
width: $unit-4x;
|
|
||||||
height: $unit-4x;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--card-bg);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
@include smooth-transition($duration-zoom, transform);
|
|
||||||
|
|
||||||
&.wind {
|
|
||||||
background: var(--wind-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.fire {
|
|
||||||
background: var(--fire-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.water {
|
|
||||||
background: var(--water-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.earth {
|
|
||||||
background: var(--earth-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.light {
|
|
||||||
background: var(--light-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dark {
|
|
||||||
background: var(--dark-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: $unit-4x + $unit-half;
|
|
||||||
height: $unit-4x + $unit-half;
|
|
||||||
border-radius: 50%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-placeholder {
|
|
||||||
width: $unit-4x + $unit-half;
|
|
||||||
height: $unit-4x + $unit-half;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--placeholder-bg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.username {
|
|
||||||
font-size: $font-regular;
|
|
||||||
font-weight: $medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
.party-actions {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit-half;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Style the dropdown trigger button to match Button ghost small
|
// Style the dropdown trigger button to match Button ghost small
|
||||||
:global(.party-actions-trigger) {
|
:global(.party-actions-trigger) {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,195 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import InfoTile from './InfoTile.svelte'
|
import type { Snippet } from 'svelte'
|
||||||
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
||||||
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
|
import Icon from '$lib/components/Icon.svelte'
|
||||||
|
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
onOpen: () => void
|
user?: {
|
||||||
|
username?: string
|
||||||
|
avatar?: {
|
||||||
|
picture?: string | null
|
||||||
|
element?: string | null
|
||||||
|
} | null
|
||||||
|
} | null
|
||||||
|
canEdit?: boolean
|
||||||
|
onOpenDescription: () => void
|
||||||
|
onOpenEdit?: () => void
|
||||||
|
/** Slot for the dropdown menu */
|
||||||
|
menu?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
let { description, onOpen }: Props = $props()
|
let { name, description, user, canEdit = false, onOpenDescription, onOpenEdit, menu }: Props =
|
||||||
|
$props()
|
||||||
|
|
||||||
|
const avatarSrc = $derived(getAvatarSrc(user?.avatar?.picture))
|
||||||
|
const avatarSrcSet = $derived(getAvatarSrcSet(user?.avatar?.picture))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InfoTile label="Description" clickable onclick={onOpen} class="description-tile">
|
<div class="description-tile">
|
||||||
{#if description}
|
<!-- Header: Title + Actions -->
|
||||||
<DescriptionRenderer content={description} truncate={true} maxLines={4} />
|
<div class="tile-header">
|
||||||
{:else}
|
<h1 class="party-name">{name || '(untitled party)'}</h1>
|
||||||
<span class="empty-state">No description</span>
|
<div class="actions">
|
||||||
|
{#if canEdit}
|
||||||
|
<Button variant="secondary" size="small" onclick={onOpenEdit}>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if menu}
|
||||||
|
{@render menu()}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Creator info -->
|
||||||
|
{#if user}
|
||||||
|
<a href="/{user.username}" class="creator-link">
|
||||||
|
<div class="avatar-wrapper {user.avatar?.element || ''}">
|
||||||
|
{#if user.avatar?.picture}
|
||||||
|
<img
|
||||||
|
class="avatar"
|
||||||
|
alt={`Avatar of ${user.username}`}
|
||||||
|
src={avatarSrc}
|
||||||
|
srcset={avatarSrcSet}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="avatar-placeholder" aria-hidden="true"></div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="username">{user.username}</span>
|
||||||
|
</a>
|
||||||
{/if}
|
{/if}
|
||||||
</InfoTile>
|
|
||||||
|
<!-- Description content (clickable) -->
|
||||||
|
<button type="button" class="description-content" onclick={onOpenDescription}>
|
||||||
|
{#if description}
|
||||||
|
<DescriptionRenderer content={description} truncate={true} maxLines={3} />
|
||||||
|
{:else}
|
||||||
|
<span class="empty-state">No description</span>
|
||||||
|
{/if}
|
||||||
|
<Icon name="chevron-right" size={16} class="chevron" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@use '$src/themes/spacing' as *;
|
||||||
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/effects' as *;
|
||||||
@use '$src/themes/typography' as *;
|
@use '$src/themes/typography' as *;
|
||||||
|
|
||||||
|
.description-tile {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 0.5px solid var(--button-bg);
|
||||||
|
border-radius: $card-corner;
|
||||||
|
padding: $unit-2x;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.party-name {
|
||||||
|
font-size: $font-large;
|
||||||
|
font-weight: $bold;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.creator-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
.username {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--button-bg);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--button-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: $font-small;
|
||||||
|
font-weight: $medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: $unit;
|
||||||
|
padding: $unit;
|
||||||
|
margin: 0 (-$unit);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: $item-corner;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
@include smooth-transition($duration-quick, background-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.chevron) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
|
flex: 1;
|
||||||
font-size: $font-regular;
|
font-size: $font-regular;
|
||||||
color: var(--text-tertiary);
|
color: var(--text-tertiary);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { Snippet } from 'svelte'
|
||||||
import type { Party } from '$lib/types/api/party'
|
import type { Party } from '$lib/types/api/party'
|
||||||
import DescriptionTile from './DescriptionTile.svelte'
|
import DescriptionTile from './DescriptionTile.svelte'
|
||||||
import RaidTile from './RaidTile.svelte'
|
import RaidTile from './RaidTile.svelte'
|
||||||
|
|
@ -13,9 +14,10 @@
|
||||||
canEdit: boolean
|
canEdit: boolean
|
||||||
onOpenDescription: () => void
|
onOpenDescription: () => void
|
||||||
onOpenEdit?: () => void
|
onOpenEdit?: () => void
|
||||||
|
menu?: Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
let { party, canEdit, onOpenDescription, onOpenEdit }: Props = $props()
|
let { party, canEdit, onOpenDescription, onOpenEdit, menu }: Props = $props()
|
||||||
|
|
||||||
// Check if data exists for each tile
|
// Check if data exists for each tile
|
||||||
const hasDescription = $derived(!!party.description)
|
const hasDescription = $derived(!!party.description)
|
||||||
|
|
@ -48,7 +50,15 @@
|
||||||
<!-- Row 1: Description + Video -->
|
<!-- Row 1: Description + Video -->
|
||||||
<div class="row row-1" class:single={!showVideo}>
|
<div class="row row-1" class:single={!showVideo}>
|
||||||
{#if showDescription}
|
{#if showDescription}
|
||||||
<DescriptionTile description={party.description} onOpen={onOpenDescription} />
|
<DescriptionTile
|
||||||
|
name={party.name}
|
||||||
|
description={party.description}
|
||||||
|
user={party.user}
|
||||||
|
{canEdit}
|
||||||
|
{onOpenDescription}
|
||||||
|
{onOpenEdit}
|
||||||
|
{menu}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showVideo}
|
{#if showVideo}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue