show first two paragraphs in description tile with fade gradient
This commit is contained in:
parent
e0ec59c147
commit
c93f65153c
3 changed files with 86 additions and 3 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte'
|
import type { Snippet } from 'svelte'
|
||||||
import DescriptionRenderer from '$lib/components/DescriptionRenderer.svelte'
|
import type { JSONContent } from '@tiptap/core'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
import Button from '$lib/components/ui/Button.svelte'
|
||||||
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
||||||
|
|
||||||
|
|
@ -33,6 +33,49 @@
|
||||||
|
|
||||||
const avatarSrc = $derived(getAvatarSrc(user?.avatar?.picture))
|
const avatarSrc = $derived(getAvatarSrc(user?.avatar?.picture))
|
||||||
const avatarSrcSet = $derived(getAvatarSrcSet(user?.avatar?.picture))
|
const avatarSrcSet = $derived(getAvatarSrcSet(user?.avatar?.picture))
|
||||||
|
|
||||||
|
/** Extract plain text from first two non-empty paragraphs of TipTap JSON content */
|
||||||
|
function getPreviewParagraphs(content?: string): string[] {
|
||||||
|
if (!content) return []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(content) as JSONContent
|
||||||
|
if (json.type !== 'doc' || !json.content?.length) return []
|
||||||
|
|
||||||
|
// Extract text from an inline node (text or mention)
|
||||||
|
const getNodeText = (node: JSONContent): string => {
|
||||||
|
if (node.type === 'text') return node.text ?? ''
|
||||||
|
if (node.type === 'mention') {
|
||||||
|
// EntityMention stores name in attrs.id
|
||||||
|
const id = node.attrs?.id as { name?: { en?: string }; granblue_en?: string } | undefined
|
||||||
|
return id?.name?.en ?? id?.granblue_en ?? ''
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract text from a block
|
||||||
|
const getBlockText = (block: JSONContent): string =>
|
||||||
|
block.content?.map(getNodeText).join('') ?? ''
|
||||||
|
|
||||||
|
// Find first two non-empty paragraphs or headings
|
||||||
|
const paragraphs: string[] = []
|
||||||
|
for (const node of json.content) {
|
||||||
|
if (node.type !== 'paragraph' && node.type !== 'heading') continue
|
||||||
|
const text = getBlockText(node).trim()
|
||||||
|
if (text) {
|
||||||
|
paragraphs.push(text)
|
||||||
|
if (paragraphs.length >= 2) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return paragraphs
|
||||||
|
} catch {
|
||||||
|
// Plain text fallback - return first two non-empty lines
|
||||||
|
return content.split('\n').map((l) => l.trim()).filter(Boolean).slice(0, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const previewParagraphs = $derived(getPreviewParagraphs(description))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="description-tile">
|
<div class="description-tile">
|
||||||
|
|
@ -74,8 +117,12 @@
|
||||||
|
|
||||||
<!-- Description content (clickable) -->
|
<!-- Description content (clickable) -->
|
||||||
<button type="button" class="description-content" onclick={onOpenDescription}>
|
<button type="button" class="description-content" onclick={onOpenDescription}>
|
||||||
{#if description}
|
{#if previewParagraphs.length}
|
||||||
<DescriptionRenderer content={description} truncate={true} maxLines={3} />
|
<div class="preview-text">
|
||||||
|
{#each previewParagraphs as paragraph}
|
||||||
|
<p>{paragraph}</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="empty-state">No description</span>
|
<span class="empty-state">No description</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -96,6 +143,20 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $unit;
|
gap: $unit;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 96px;
|
||||||
|
background: linear-gradient(to bottom, transparent, var(--card-bg));
|
||||||
|
pointer-events: none;
|
||||||
|
border-radius: 0 0 $card-corner $card-corner;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tile-header-container {
|
.tile-header-container {
|
||||||
|
|
@ -179,10 +240,17 @@
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: $item-corner;
|
border-radius: $item-corner;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-start;
|
||||||
|
display: flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
|
min-width: 0;
|
||||||
|
width: calc(100% + #{$unit * 2});
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
@include smooth-transition($duration-quick, background-color);
|
@include smooth-transition($duration-quick, background-color);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
@ -190,6 +258,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preview-text {
|
||||||
|
font-size: $font-regular;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0 0 $unit-half 0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: $font-regular;
|
font-size: $font-regular;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue