move title, user, and actions into description tile

This commit is contained in:
Justin Edmund 2025-12-21 12:44:48 -08:00
parent 9fea625e7c
commit 23c4425a2a
3 changed files with 198 additions and 157 deletions

View file

@ -36,7 +36,6 @@
// Utilities
import { getLocalId } from '$lib/utils/localId'
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 WeaponGrid from '$lib/components/grids/WeaponGrid.svelte'
@ -888,41 +887,13 @@
<div class="page-wrap">
<div class="track">
<section class="party-container">
<header class="party-header">
<div class="party-info">
<h1>{party.name || '(untitled party)'}</h1>
{#if party.user}
{@const avatarSrc = getAvatarSrc(party.user.avatar?.picture)}
{@const avatarSrcSet = getAvatarSrcSet(party.user.avatar?.picture)}
<div class="creator">
<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}
<PartyInfoGrid
{party}
canEdit={canEdit()}
onOpenDescription={openDescriptionPanel}
onOpenEdit={openSettingsPanel}
>
{#snippet menu()}
<DropdownMenu.Root>
<DropdownMenu.Trigger class="party-actions-trigger" aria-label="Open actions menu">
<Icon name="ellipsis" size={14} />
@ -962,15 +933,8 @@
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu.Root>
</div>
</header>
<PartyInfoGrid
{party}
canEdit={canEdit()}
onOpenDescription={openDescriptionPanel}
onOpenEdit={openSettingsPanel}
/>
{/snippet}
</PartyInfoGrid>
<PartySegmentedControl
selectedTab={activeTab}
@ -1081,107 +1045,6 @@
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
:global(.party-actions-trigger) {
display: inline-flex;

View file

@ -1,27 +1,195 @@
<script lang="ts">
import InfoTile from './InfoTile.svelte'
import type { Snippet } from '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 {
name?: 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>
<InfoTile label="Description" clickable onclick={onOpen} class="description-tile">
{#if description}
<DescriptionRenderer content={description} truncate={true} maxLines={4} />
{:else}
<span class="empty-state">No description</span>
<div class="description-tile">
<!-- Header: Title + Actions -->
<div class="tile-header">
<h1 class="party-name">{name || '(untitled party)'}</h1>
<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}
</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">
@use '$src/themes/spacing' as *;
@use '$src/themes/layout' as *;
@use '$src/themes/effects' 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 {
flex: 1;
font-size: $font-regular;
color: var(--text-tertiary);
font-style: italic;

View file

@ -1,4 +1,5 @@
<script lang="ts">
import type { Snippet } from 'svelte'
import type { Party } from '$lib/types/api/party'
import DescriptionTile from './DescriptionTile.svelte'
import RaidTile from './RaidTile.svelte'
@ -13,9 +14,10 @@
canEdit: boolean
onOpenDescription: () => 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
const hasDescription = $derived(!!party.description)
@ -48,7 +50,15 @@
<!-- Row 1: Description + Video -->
<div class="row row-1" class:single={!showVideo}>
{#if showDescription}
<DescriptionTile description={party.description} onOpen={onOpenDescription} />
<DescriptionTile
name={party.name}
description={party.description}
user={party.user}
{canEdit}
{onOpenDescription}
{onOpenEdit}
{menu}
/>
{/if}
{#if showVideo}