This commit is contained in:
Timothy Jaeryang Baek 2026-07-14 16:57:35 -04:00
parent 6772b1cb4f
commit 79d3e34eea
4 changed files with 91 additions and 23 deletions

View file

@ -84,7 +84,7 @@
import GlobeAlt from '../icons/GlobeAlt.svelte';
import Photo from '../icons/Photo.svelte';
import Wrench from '../icons/Wrench.svelte';
import Keyframes from '../icons/Keyframes.svelte';
import Cube from '../icons/Cube.svelte';
import Sparkles from '../icons/Sparkles.svelte';
import InputVariablesModal from './MessageInput/InputVariablesModal.svelte';
@ -1233,7 +1233,7 @@
/>
{#if loaded}
<div class="w-full ">
<div class="w-full">
<div class=" mx-auto inset-x-0 bg-transparent flex justify-center">
<div
class="flex flex-col px-3 {($settings?.widescreenMode ?? null)
@ -1852,7 +1852,7 @@
showSkills = !showSkills;
}}
>
<Keyframes className="size-4" strokeWidth="1.75" />
<Cube className="size-4" strokeWidth="1.75" />
<span class="text-sm">
{(selectedSkillIds ?? []).length}

View file

@ -2,7 +2,7 @@
import { getContext, onDestroy } from 'svelte';
import { getSkillItems } from '$lib/apis/skills';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Keyframes from '$lib/components/icons/Keyframes.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
const i18n = getContext('i18n');
@ -96,7 +96,7 @@
>
<div class="flex w-full min-w-0 items-center text-black dark:text-gray-100">
<div class="flex items-center justify-center size-5 mr-2 shrink-0">
<Keyframes className="size-4" />
<Cube className="size-4" />
</div>
<div class="truncate min-w-0 flex-1">
{skill.name}

View file

@ -27,7 +27,7 @@
import Switch from '$lib/components/common/Switch.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import Wrench from '$lib/components/icons/Wrench.svelte';
import Keyframes from '$lib/components/icons/Keyframes.svelte';
import Cube from '$lib/components/icons/Cube.svelte';
import Sparkles from '$lib/components/icons/Sparkles.svelte';
import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
import Photo from '$lib/components/icons/Photo.svelte';
@ -176,7 +176,7 @@
tab = 'skills';
}}
>
<Keyframes className="size-3.5" strokeWidth="1.75" />
<Cube className="size-3.5" strokeWidth="1.75" />
<div class="flex items-center w-full justify-between">
<div class=" line-clamp-1">
@ -534,7 +534,7 @@
<div class="flex flex-1 gap-2 items-center">
<Tooltip content={skills[skillId]?.name ?? ''} placement="top">
<div class="shrink-0">
<Keyframes className="size-3.5" strokeWidth="1.75" />
<Cube className="size-3.5" strokeWidth="1.75" />
</div>
</Tooltip>
<Tooltip content={skills[skillId]?.description ?? ''} placement="top-start">

View file

@ -74,7 +74,10 @@
let show = false;
let triggerElement: HTMLElement | null = null;
let contentElement: HTMLElement | null = null;
let dropdownPosition = { top: 0, left: 0 };
let dropdownPosition = { top: 0, left: 0, maxHeight: undefined as number | undefined };
let isSmallViewport = false;
let positionFrame: number | undefined;
let settleTimers: number[] = [];
const portal = (node: HTMLElement) => {
document.body.appendChild(node);
@ -85,31 +88,79 @@
};
};
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 updateViewportSize = () => {
isSmallViewport = (window.visualViewport?.width ?? window.innerWidth) < 640;
};
const updatePosition = () => {
if (!show || !triggerElement) return;
const rect = triggerElement.getBoundingClientRect();
const contentRect = contentElement?.getBoundingClientRect();
const contentWidth = contentRect?.width ?? 0;
const contentHeight = contentRect?.height ?? 0;
const spaceBelow = window.innerHeight - rect.bottom - 8;
const spaceAbove = rect.top - 8;
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 ? window.innerWidth - contentWidth - 8 : preferredLeft;
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:
resolvedPlacement === 'top' && contentHeight
? rect.top - contentHeight - 2
: rect.bottom + 2,
left: Math.max(8, Math.min(preferredLeft, maxLeft))
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 = () => {
updateViewportSize();
for (const timer of settleTimers) window.clearTimeout(timer);
settleTimers = [];
schedulePositionUpdate();
for (const delay of [50, 150, 300]) {
settleTimers.push(window.setTimeout(schedulePositionUpdate, delay));
}
};
const toggleOpen = async () => {
show = !show;
if (show) {
@ -495,7 +546,7 @@
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
};
onMount(async () => {
onMount(() => {
if (items) {
tags = items
.filter((item) => includeHidden || !(item.model?.info?.meta?.hidden ?? false))
@ -504,6 +555,19 @@
// Remove duplicates and sort
tags = Array.from(new Set(tags)).sort((a, b) => a.localeCompare(b));
}
updateViewportSize();
window.addEventListener('scroll', schedulePositionUpdate, 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', schedulePositionUpdate, true);
window.visualViewport?.removeEventListener('resize', scheduleSettledPositionUpdates);
window.visualViewport?.removeEventListener('scroll', schedulePositionUpdate);
};
});
$: if (show && !selectionOnly) {
@ -585,11 +649,12 @@
let listScrollTop = 0;
let listContainer;
$: listViewportHeight = isSmallViewport ? 120 : 288;
$: visibleStart = Math.max(0, Math.floor(listScrollTop / ITEM_HEIGHT) - OVERSCAN);
$: visibleEnd = Math.min(
filteredItems.length,
Math.ceil((listScrollTop + 288) / ITEM_HEIGHT) + OVERSCAN
Math.ceil((listScrollTop + listViewportHeight) / ITEM_HEIGHT) + OVERSCAN
);
</script>
@ -607,7 +672,7 @@
<svelte:window
on:pointerdown={handlePointerDown}
on:keydown={handleKeydown}
on:resize={updatePosition}
on:resize={scheduleSettledPositionUpdates}
/>
<div class="relative w-full">
@ -649,7 +714,9 @@
<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: {dropdownPosition.top}px; left: {dropdownPosition.left}px; {dropdownPosition.maxHeight
? `max-height: ${dropdownPosition.maxHeight}px;`
: ''}"
>
<div
class="z-40 {className} 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"
@ -698,7 +765,7 @@
<button
type="button"
class="flex size-[1.375rem] shrink-0 items-center justify-center rounded-lg transition-colors duration-100 {compareEnabled
? 'bg-gray-100/60 text-gray-700 hover:bg-gray-100/60 dark:bg-gray-800/40 dark:text-gray-300 dark:hover:bg-gray-800/40'
? 'bg-gray-50 text-gray-700 hover:bg-gray-50 dark:bg-gray-800/60 dark:text-gray-200 dark:hover:bg-gray-800/60'
: 'text-gray-500 hover:bg-gray-50/40 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800/40 dark:hover:text-gray-200'}"
aria-label={$i18n.t('Compare')}
aria-pressed={compareEnabled}
@ -758,7 +825,8 @@
{:else}
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="max-h-72 overflow-y-auto"
class="overflow-y-auto"
style="max-height: {listViewportHeight}px;"
role="listbox"
aria-label={$i18n.t('Available models')}
bind:this={listContainer}