ui : put the discover view in the models manager

The manager dialog gains its second view: the Discover Models call to
action fades the table out, slides the title with its compass mark, and
fades the discover surface in; the arrow button returns to the table.

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-29 06:26:46 +02:00
parent a4f65c143e
commit e73cfed294
2 changed files with 140 additions and 6 deletions
@@ -1,7 +1,12 @@
<script lang="ts">
import { ArrowLeft, Compass } from '@lucide/svelte';
import ModelsDiscover from '$lib/components/app/models/discover/ModelsDiscover.svelte';
import ModelsManager from '$lib/components/app/models/ModelsManager/ModelsManager.svelte';
import { Button } from '$lib/components/ui/button';
import * as Dialog from '$lib/components/ui/dialog';
import { MODEL_ICON } from '$lib/constants';
import { uiStore } from '$lib/stores';
import { untrack } from 'svelte';
interface Props {
open?: boolean;
@@ -10,6 +15,54 @@
let { onOpenChange, open = $bindable(false) }: Props = $props();
type View = 'discover' | 'manage';
/** How long a view takes to fade out before the next one fades in. */
const VIEW_FADE_MS = 150;
// what the user asked for, and what is actually rendered: a view change fades the
// current one out, swaps, then fades the next one in
let view = $state<View>('manage');
let shownView = $state<View>('manage');
let isSwapping = $state(false);
$effect(() => {
const next = view;
if (next === shownView) return;
// nothing to fade while the dialog is closed, so the view is set before it opens
if (!open) {
untrack(() => (shownView = next));
return;
}
untrack(() => (isSwapping = true));
const timer = setTimeout(() => {
untrack(() => {
shownView = next;
isSwapping = false;
});
}, VIEW_FADE_MS);
return () => clearTimeout(timer);
});
let title = $derived(view === 'discover' ? 'Discover' : 'Models');
// the sidebar's Discover entry opens this dialog on its Discover view
$effect(() => {
if (!uiStore.discoverModelsOpen) return;
untrack(() => {
uiStore.discoverModelsOpen = false;
view = 'discover';
handleOpenChange(true);
});
});
function handleOpenChange(value: boolean) {
open = value;
onOpenChange?.(value);
@@ -22,16 +75,91 @@
onCloseAutoFocus={(event) => event.preventDefault()}
onOpenAutoFocus={(event) => event.preventDefault()}
>
<Dialog.Header class="flex flex-row items-center p-2 pr-8">
<Dialog.Title class="flex min-h-7 items-center gap-2">
<MODEL_ICON class="h-5 w-5 shrink-0" />
<Dialog.Header class="relative flex flex-row items-center justify-between p-2 pr-8">
<!--
The arrow is out of the flow and the title slides, so entering a sub-view
moves the title with a transform instead of reflowing the header. min-h
keeps the header's height fixed either way.
-->
<Button
aria-label="Back to models"
class="absolute top-1/2 left-1 h-7 w-7 -translate-y-1/2 transition-[opacity,visibility] duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] {view ===
'manage'
? 'invisible opacity-0'
: 'visible opacity-100'}"
onclick={() => (view = 'manage')}
size="icon"
variant="ghost"
>
<ArrowLeft class="h-4 w-4" />
</Button>
<span>Models</span>
<Dialog.Title
class="flex min-h-7 items-center gap-2 transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] {view ===
'manage'
? 'translate-x-0'
: 'translate-x-8'}"
>
<!-- the marks cross-fade in one grid cell, so the title never shifts -->
<span class="grid h-5 w-5 shrink-0 place-items-center">
<MODEL_ICON
class="col-start-1 row-start-1 h-5 w-5 transition-opacity duration-150 {view ===
'manage'
? 'opacity-100'
: 'opacity-0'}"
/>
<Compass
class="col-start-1 row-start-1 h-5 w-5 transition-opacity duration-150 {view ===
'discover'
? 'opacity-100'
: 'opacity-0'}"
/>
</span>
<span>{title}</span>
</Dialog.Title>
</Dialog.Header>
<div class="min-h-0 flex-1">
<ModelsManager class="h-full" />
<div class="dialog-view min-h-0 flex-1" data-visible={!isSwapping}>
{#if shownView === 'manage'}
<ModelsManager class="h-full">
{#snippet toolbarEnd()}
<Button
class="gap-1.5"
onclick={() => (view = 'discover')}
size="sm"
variant="tertiary"
>
<Compass class="h-3.5 w-3.5" />
Discover Models
</Button>
{/snippet}
</ModelsManager>
{:else}
<div class="grid h-full overflow-hidden" style="grid-template-columns: auto 1fr;">
<ModelsDiscover />
</div>
{/if}
</div>
</Dialog.Content>
</Dialog.Root>
<style>
/* a view change reads as one surface swapping, not as content teleporting */
.dialog-view {
opacity: 0;
transition: opacity 150ms cubic-bezier(0.23, 1, 0.32, 1);
}
.dialog-view[data-visible='true'] {
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.dialog-view {
transition-duration: 100ms;
}
}
</style>
+6
View File
@@ -10,6 +10,8 @@ class UiStore {
/** Set when a flow outside the chat wants the composer focused once it can take it. */
composerFocusRequested = $state(false);
discoverModelsOpen = $state(false);
/** Whether the desktop sidebar is expanded (open). */
isSidebarExpanded = $state(false);
/** Model the manager reveals when it opens, a qualified id or a raw model name. */
@@ -25,6 +27,10 @@ class UiStore {
return requested;
}
openDiscoverModels(): void {
this.discoverModelsOpen = true;
}
/** Open the models manager, optionally focused on one model. */
openModelsManager(focus?: string): void {
this.manageModelFocus = focus ?? null;