This commit is contained in:
Timothy Jaeryang Baek 2026-09-12 17:43:01 -04:00
parent 08578557de
commit 5bb321de06

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { marked } from 'marked';
import Fuse from 'fuse.js';
import { autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom';
import dayjs from '$lib/dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
@ -87,104 +88,41 @@
let show = false;
let triggerElement: HTMLElement | null = null;
let contentElement: HTMLElement | null = null;
let panelElement: HTMLElement | null = null;
let dropdownPosition = { top: 0, left: 0, maxHeight: undefined as number | undefined };
let positionFrame: number | undefined;
let settleTimers: number[] = [];
const portal = (node: HTMLElement) => {
document.body.appendChild(node);
const panel = node.firstElementChild as HTMLElement;
const cleanup = autoUpdate(triggerElement!, node, () => {
// Let flip measure the full list after content or viewport changes.
panel.style.maxHeight = '';
computePosition(triggerElement!, node, {
strategy: 'fixed',
placement: `${placement === 'auto' ? 'bottom' : placement}-${align}`,
middleware: [
offset(2),
placement === 'auto' && flip({ padding: 8, crossAxis: false }),
shift({ padding: 8 }),
size({
padding: 8,
apply({ availableHeight, availableWidth }) {
panel.style.maxHeight = `${Math.max(0, availableHeight)}px`;
panel.style.maxWidth = `${Math.max(0, availableWidth)}px`;
}
})
]
}).then(({ x, y }) => {
node.style.left = `${x}px`;
node.style.top = `${y}px`;
});
});
return {
destroy() {
cleanup();
node.remove();
}
};
};
const measureContent = () => {
if (!contentElement) return { width: 0, height: 0 };
const previousMaxHeight = panelElement?.style.maxHeight;
if (panelElement) panelElement.style.maxHeight = '';
const rect = contentElement.getBoundingClientRect();
if (panelElement && previousMaxHeight !== undefined) {
panelElement.style.maxHeight = previousMaxHeight;
}
return { width: rect.width, height: rect.height };
};
const visualViewportRect = () => {
const viewport = window.visualViewport;
return {
left: viewport?.offsetLeft ?? 0,
top: viewport?.offsetTop ?? 0,
width: viewport?.width ?? window.innerWidth,
height: viewport?.height ?? window.innerHeight
};
};
const updatePosition = () => {
if (!show || !triggerElement) return;
const rect = triggerElement.getBoundingClientRect();
const { width: contentWidth, height: contentHeight } = measureContent();
const viewport = visualViewportRect();
const viewportRight = viewport.left + viewport.width;
const viewportBottom = viewport.top + viewport.height;
const pad = 8;
const gap = 2;
const spaceBelow = viewportBottom - rect.bottom - gap - pad;
const spaceAbove = rect.top - viewport.top - gap - pad;
const preferredLeft = align === 'end' && contentWidth ? rect.right - contentWidth : rect.left;
const maxLeft = contentWidth ? viewportRight - contentWidth - pad : preferredLeft;
const resolvedPlacement =
placement === 'auto'
? contentHeight && spaceBelow < contentHeight && spaceAbove > spaceBelow
? 'top'
: 'bottom'
: placement;
const availableHeight = resolvedPlacement === 'top' ? spaceAbove : spaceBelow;
const constrainedHeight =
contentHeight && availableHeight >= 0
? Math.min(contentHeight, availableHeight)
: contentHeight;
const top =
resolvedPlacement === 'top' && contentHeight
? rect.top - constrainedHeight - gap
: rect.bottom + gap;
dropdownPosition = {
top: Math.max(viewport.top + pad, Math.min(top, viewportBottom - pad - constrainedHeight)),
left: Math.max(viewport.left + pad, Math.min(preferredLeft, maxLeft)),
maxHeight:
contentHeight && availableHeight >= 0 && contentHeight > availableHeight
? Math.max(0, availableHeight)
: undefined
};
};
const schedulePositionUpdate = () => {
if (positionFrame != null) cancelAnimationFrame(positionFrame);
positionFrame = requestAnimationFrame(() => {
positionFrame = undefined;
updatePosition();
});
};
const scheduleSettledPositionUpdates = () => {
for (const timer of settleTimers) window.clearTimeout(timer);
settleTimers = [];
schedulePositionUpdate();
for (const delay of [50, 150, 300]) {
settleTimers.push(window.setTimeout(schedulePositionUpdate, delay));
}
};
const handleScroll = (event: Event) => {
if (event.target instanceof Node && contentElement?.contains(event.target)) return;
schedulePositionUpdate();
};
const focusSearchInput = () => {
if (!$mobile) {
document.getElementById('model-search-input')?.focus();
@ -206,9 +144,7 @@
setProviderDownloadConnections();
}
resetView();
updatePosition();
await tick();
updatePosition();
for (const delay of [0, 50, 150]) {
window.setTimeout(focusSearchInput, delay);
}
@ -478,7 +414,6 @@
await tick();
const item = document.querySelector(`[data-arrow-selected="true"]`);
item?.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
schedulePositionUpdate();
};
const setCompareEnabled = (enabled: boolean) => {
@ -824,18 +759,6 @@
// Remove duplicates and sort
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
}
window.addEventListener('scroll', handleScroll, true);
window.visualViewport?.addEventListener('resize', scheduleSettledPositionUpdates);
window.visualViewport?.addEventListener('scroll', schedulePositionUpdate);
return () => {
if (positionFrame != null) cancelAnimationFrame(positionFrame);
for (const timer of settleTimers) window.clearTimeout(timer);
window.removeEventListener('scroll', handleScroll, true);
window.visualViewport?.removeEventListener('resize', scheduleSettledPositionUpdates);
window.visualViewport?.removeEventListener('scroll', schedulePositionUpdate);
};
});
const cancelModelPullHandler = async (model: string) => {
@ -978,11 +901,7 @@
}}
/>
<svelte:window
on:pointerdown={handlePointerDown}
on:keydown={handleKeydown}
on:resize={scheduleSettledPositionUpdates}
/>
<svelte:window on:pointerdown={handlePointerDown} on:keydown={handleKeydown} />
<div class="relative w-full">
<button
@ -1023,13 +942,11 @@
<div
use:portal
bind:this={contentElement}
style="position: fixed; z-index: 9999; top: {dropdownPosition.top}px; left: {dropdownPosition.left}px;"
style="position: fixed; z-index: 9999; top: 0; left: 0; width: max-content;"
>
<div
bind:this={panelElement}
class="z-40 {className ??
'w-[20rem]'} max-w-[calc(100vw-1rem)] justify-start rounded-xl border border-gray-100 bg-white p-0.5 shadow-lg outline-hidden dark:border-gray-800 dark:bg-gray-850 dark:text-white flex flex-col overflow-hidden"
style={dropdownPosition.maxHeight ? `max-height: ${dropdownPosition.maxHeight}px;` : ''}
transition:flyAndScale
>
<slot>