explorer: Clamp long node descriptions

Collapse the node description to three lines with a Show more toggle
when it overflows, and let the sidebar scroll on wide screens so the
node details below it stay reachable.
This commit is contained in:
Brandon Oxendine
2026-10-29 11:23:58 +01:00
committed by Rūdolfs Ošiņš
parent 8e5089cbdc
commit 86fcd93a68
2 changed files with 74 additions and 2 deletions
+1
View File
@@ -19,6 +19,7 @@
grid-area: left;
background-color: var(--color-surface-base);
height: calc(100vh - var(--global-header-height));
overflow-y: auto;
}
.center {
grid-area: center;
+73 -2
View File
@@ -1,6 +1,8 @@
<script lang="ts">
import type { BaseUrl, Node, NodeStats } from "@http-client";
import { tick } from "svelte";
import dompurify from "dompurify";
import { markdown, sanitizeConfig } from "@app/lib/markdown";
@@ -23,6 +25,33 @@
export let stats: NodeStats;
export let node: Node;
let descriptionContent: HTMLDivElement | undefined = undefined;
let descriptionExpanded = false;
let descriptionTruncated = false;
// Clamp to three lines, but only when that hides at least two: a Show more
// that reveals a single line costs as much space as the line itself.
function measureDescription() {
if (descriptionContent && !descriptionExpanded) {
const lineHeight = parseFloat(
getComputedStyle(descriptionContent).lineHeight,
);
descriptionTruncated = descriptionContent.offsetHeight > 4.5 * lineHeight;
}
}
function observeSize(element: HTMLElement) {
const observer = new ResizeObserver(measureDescription);
observer.observe(element);
return { destroy: () => observer.disconnect() };
}
$: {
descriptionExpanded = false;
void node.description;
void tick().then(measureDescription);
}
function render(content: string): string {
return dompurify.sanitize(
markdown({ linkify: true, emojis: true }).parse(content) as string,
@@ -75,6 +104,34 @@
word-break: break-word;
}
.description.clamped {
max-height: 3.75rem;
overflow: hidden;
mask-image: linear-gradient(to bottom, black 50%, transparent);
}
.description :global(p:first-child) {
margin-top: 0;
}
.description :global(p:last-child) {
margin-bottom: 0;
}
.description-toggle {
margin-top: 0.25rem;
padding: 0;
border: 0;
background: none;
cursor: pointer;
font: var(--txt-body-m-semibold);
color: var(--color-text-secondary);
}
.description-toggle:hover {
color: var(--color-text-primary);
}
.sidebar-item {
display: flex;
align-items: center;
@@ -138,8 +195,22 @@
</div>
{#if node.description}
<div class="description txt-body-m-regular">
{@html render(node.description)}
<div>
<div
class="description txt-body-m-regular"
class:clamped={descriptionTruncated && !descriptionExpanded}>
<div bind:this={descriptionContent} use:observeSize>
{@html render(node.description)}
</div>
</div>
{#if descriptionTruncated}
<button
class="description-toggle"
aria-expanded={descriptionExpanded}
on:click={() => (descriptionExpanded = !descriptionExpanded)}>
{descriptionExpanded ? "Show less" : "Show more"}
</button>
{/if}
</div>
{:else}
<div