Add scroll fade indicators to artifact lists and plan steps

- Add fade gradient to artifacts list that hides when scrolled to bottom
- Update plan area fades to use scroll position instead of always showing
- Add hover effect to artifact cards with glowing ellipse indicator
- Fix indentation in UserMessage attachment styles
This commit is contained in:
Andrew Beal 2026-07-11 14:46:39 +01:00
parent 0b54cdcc10
commit 6b5d51df39
3 changed files with 97 additions and 23 deletions

View file

@ -29,6 +29,24 @@
}
return tally;
}
let isScrolledToBottom = true;
function updateScrollFade(element: HTMLElement) {
const { scrollTop, scrollHeight, clientHeight } = element;
isScrolledToBottom = scrollHeight - scrollTop - clientHeight < 1;
}
function artifactsListScrollAction(element: HTMLElement) {
updateScrollFade(element);
const handleScroll = () => updateScrollFade(element);
element.addEventListener("scroll", handleScroll);
return {
destroy() {
element.removeEventListener("scroll", handleScroll);
}
};
}
</script>
<div class="message-container assistant">
@ -50,18 +68,24 @@
{/if}
{/each}
</div>
<div class="artifacts-list-container">
{#each message.artifacts as artifact}
<div class="artifact-card" aria-label="{artifact.filePath}">
<span class="artifact-ellipse artifact-{artifact.action}"></span>
<div class="artifacts-list-wrapper">
<div class="artifacts-list-container" use:artifactsListScrollAction>
{#each message.artifacts as artifact}
<div
class="artifact-icon"
use:setElementIcon={artifact.getIconName()}
></div>
<span class="artifact-name">{basename(artifact.filePath)}</span>
<span class="artifact-action artifact-action-{artifact.action}">{artifactActionToCopy(artifact.action)}</span>
</div>
{/each}
class="artifact-card"
aria-label="{artifact.filePath}"
>
<span class="artifact-ellipse artifact-ellipse-{artifact.action}"></span>
<div
class="artifact-icon"
use:setElementIcon={artifact.getIconName()}
></div>
<span class="artifact-name">{basename(artifact.filePath)}</span>
<span class="artifact-action artifact-action-{artifact.action}">{artifactActionToCopy(artifact.action)}</span>
</div>
{/each}
</div>
<div class="artifacts-list-fade" class:hidden={isScrolledToBottom}></div>
</div>
</div>
{/if}
@ -169,9 +193,14 @@
margin-left: var(--size-4-1);
}
.artifacts-list-container {
.artifacts-list-wrapper {
grid-row: 2;
grid-column: 1 / 3;
position: relative;
width: 100%;
}
.artifacts-list-container {
display: flex;
flex-direction: column;
overflow: scroll;
@ -184,6 +213,22 @@
display: none;
}
.artifacts-list-fade {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: var(--size-4-8);
background-image: linear-gradient(to bottom, transparent, var(--background-secondary));
pointer-events: none;
opacity: 1;
transition: opacity 0.2s ease-out;
}
.artifacts-list-fade.hidden {
opacity: 0;
}
.artifact-card {
display: grid;
grid-template-rows: auto;
@ -195,6 +240,7 @@
background-color: var(--background-secondary-alt);
border-radius: var(--size-4-2);
padding: 0 var(--size-4-1) 0 var(--size-4-2);
cursor: pointer;
}
.artifact-ellipse {
@ -207,16 +253,25 @@
align-self: center;
}
.artifact-card:hover .artifact-ellipse {
width: 12px;
height: 12px;
box-shadow: 0px 0px 4px 1px currentColor;
}
.artifact-ellipse-create {
background: var(--color-green);
color: var(--color-green);
}
.artifact-ellipse-modify {
background: var(--color-blue);
color: var(--color-blue);
}
.artifact-ellipse-delete {
background: var(--color-red);
color: var(--color-red);
}
.artifact-icon {

View file

@ -19,6 +19,8 @@
let stepElements: (HTMLDivElement | null)[] = [];
let isTransitioning = false;
let resizeObserver: ResizeObserver | null = null;
let isScrolledToTop = true;
let isScrolledToBottom = true;
$: steps = $executionPlanState.plan?.executionSteps;
$: activeStepIndex = $executionPlanState.currentStepIndex;
@ -65,6 +67,8 @@
const stepsToShow = Math.min(3, steps.length);
collapsedHeight = (stepsToShow * stepHeight) + (Math.max(0, stepsToShow - 1) * separatorHeight);
updateScrollFade();
}
onDestroy(() => {
@ -73,6 +77,15 @@
}
});
function updateScrollFade() {
if (!wrapperDiv) {
return;
}
const { scrollTop, scrollHeight, clientHeight } = wrapperDiv;
isScrolledToTop = scrollTop < 1;
isScrolledToBottom = scrollHeight - scrollTop - clientHeight < 1;
}
async function scrollToActiveStep() {
if (!wrapperDiv || !$executionPlanState.plan || activeStepIndex === -1) {
return;
@ -124,7 +137,7 @@
aria-label={expanded ? "Collapse planned steps" : "Expand planned steps"}
role="button"
tabindex=0>
<div id="chat-plan-steps-wrapper" style:height="{expanded ? expandedHeight : collapsedHeight}px" bind:this={wrapperDiv}>
<div id="chat-plan-steps-wrapper" style:height="{expanded ? expandedHeight : collapsedHeight}px" bind:this={wrapperDiv} on:scroll={updateScrollFade}>
<div id="chat-plan-steps" bind:this={contentDiv}>
{#each steps as step, index }
<div class="chat-plan-step" bind:this={stepElements[index]}>
@ -153,8 +166,8 @@
</div>
</div>
{#if steps.length > 2}
<div class="chat-plan-fade top-fade" transition:fade></div>
<div class="chat-plan-fade bottom-fade" transition:fade></div>
<div class="chat-plan-fade top-fade" class:hidden={isScrolledToTop}></div>
<div class="chat-plan-fade bottom-fade" class:hidden={isScrolledToBottom}></div>
{/if}
<div id="chat-plan-chevron"
class="transparent-button"
@ -251,6 +264,12 @@
border-radius: var(--radius-m);
pointer-events: none;
z-index: 1;
opacity: 1;
transition: opacity 0.2s ease-out;
}
.chat-plan-fade.hidden {
opacity: 0;
}
.top-fade {

View file

@ -119,16 +119,16 @@
}
.message-attachment-name {
display: inline-block;
white-space: nowrap;
width: 100%;
padding: 0;
font-size: var(--font-smaller);
display: inline-block;
white-space: nowrap;
width: 100%;
padding: 0;
font-size: var(--font-smaller);
}
.message-attachment-size {
padding: 0;
font-size: var(--font-smallest);
color: var(--text-muted);
padding: 0;
font-size: var(--font-smallest);
color: var(--text-muted);
}
</style>