ui : polish the shell

The secondary button gets its own look back and the chat add button its
own light surface. Pressable elements get a pointer cursor again, the
font rendering smooths on the app shell, the root layout resolves its
props probe through the providers' api url, and the agent skills stay
out of prettier's way.

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-28 17:12:27 +02:00
parent db0eb67ef1
commit a69de44bc3
5 changed files with 54 additions and 29 deletions
+4
View File
@@ -19,3 +19,7 @@ test-results
# Vendored third party sources, kept byte identical to upstream
src/lib/vendors/
# Agent skills installed from upstream, kept as installed
.agents/
skills-lock.json
+28 -1
View File
@@ -6,6 +6,33 @@
@custom-variant dark (&:is(.dark *));
/* shadcn components key their orientation styles off these, toggle-group among them */
@custom-variant data-horizontal (&[data-orientation='horizontal']);
@custom-variant data-vertical (&[data-orientation='vertical']);
/*
* Tailwind v4 dropped the pointer cursor from buttons. Every control a user can
* press gets it back, and anything disabled keeps the cursor its own class asks for.
*/
@layer base {
button:not(:disabled),
[role='button']:not([data-disabled]),
[role='checkbox']:not([data-disabled]),
[role='combobox']:not([data-disabled]),
[role='menuitem']:not([data-disabled]),
[role='menuitemcheckbox']:not([data-disabled]),
[role='menuitemradio']:not([data-disabled]),
[role='option']:not([data-disabled]),
[role='radio']:not([data-disabled]),
[role='switch']:not([data-disabled]),
[role='tab']:not([data-disabled]),
label[for],
select,
summary {
cursor: pointer;
}
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
@@ -140,7 +167,7 @@
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground antialiased;
scrollbar-width: thin;
scrollbar-gutter: stable;
}
@@ -3,7 +3,7 @@
import { page } from '$app/state';
import { ChatForm } from '$lib/components/app';
import { useDraftMessages } from '$lib/hooks/use-draft-messages.svelte';
import { deviceStore } from '$lib/stores';
import { deviceStore, uiStore } from '$lib/stores';
import { onMount } from 'svelte';
interface Props {
@@ -33,6 +33,15 @@
}: Props = $props();
let chatFormRef: ChatForm | undefined = $state(undefined);
// a dialog that closed onto the chat asks for the composer to take focus
$effect(() => {
if (!uiStore.consumeComposerFocus()) return;
const frame = requestAnimationFrame(() => chatFormRef?.focus());
return () => cancelAnimationFrame(frame);
});
let formWrapperEl: HTMLDivElement | undefined = $state();
let chatId = $derived(page.params.id as string | undefined);
@@ -27,7 +27,7 @@
outline:
'shadow-sm hover:text-accent-foreground hover:bg-muted-foreground/10 backdrop-blur-sm dark:border-input border',
secondary:
'bg-background dark:bg-muted-foreground/15 dark:text-secondary-foreground shadow-sm text-foreground hover:bg-muted-foreground/20 dark:hover:bg-muted-foreground/25',
'bg-muted/30 dark:bg-muted-foreground/15 dark:text-secondary-foreground shadow-sm border-muted border text-foreground hover:bg-muted dark:hover:bg-muted-foreground/25',
tertiary:
'bg-muted/60 dark:bg-muted/75 shadow-sm border border-border/30 text-foreground hover:bg-muted/80 dark:border-border/20 dark:hover:bg-muted'
}
+11 -26
View File
@@ -2,7 +2,6 @@
import '../app.css';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { base } from '$app/paths';
import { page } from '$app/state';
import { SidebarNavigation } from '$lib/components/app';
import { PwaMetaTags, PwaRefreshAlert } from '$lib/components/pwa';
@@ -31,6 +30,7 @@
versionStore
} from '$lib/stores';
import { initStores } from '$lib/stores/init';
import { apiUrl } from '$lib/utils/api-base';
import { ModeWatcher } from 'mode-watcher';
import { untrack } from 'svelte';
import { onMount } from 'svelte';
@@ -147,6 +147,11 @@
});
function checkApiKey() {
// the stored key authenticates the llama.cpp server serving this UI
if (!serverStore.capabilities.props) {
return;
}
const apiKey = settingsStore.config.apiKey;
// Without a stored key there is nothing to re-validate here; the keyless
@@ -167,7 +172,7 @@
[HEADERS.AUTHORIZATION]: `${HEADERS.BEARER}${apiKey.trim()}`
};
fetch(`${base}/props`, { headers })
fetch(apiUrl('/props'), { headers })
.then((response) => {
if (response.status === 401 || response.status === 403) {
window.location.reload();
@@ -230,37 +235,17 @@
});
}
// Fetch router models when in router mode (for status and modalities)
// Wait for models to be loaded first, run only once
let routerModelsFetched = false;
$effect(() => {
const isRouter = serverStore.isRouterMode;
const modelsCount = modelsStore.models.length;
// Only fetch router models once when we have models loaded and in router mode
if (isRouter && modelsCount > 0 && !routerModelsFetched) {
routerModelsFetched = true;
untrack(() => {
modelsStore.fetchRouterModels();
});
}
});
// Live model status and load progress via the /models/sse feed (router mode)
// Live model status and load progress via the /models/sse feed (router mode).
// The feed is kept for the session: switching to an external backend and back
// must not tear it down and reconnect on every tab switch.
$effect(() => {
if (!browser) return;
if (!serverStore.isRouterMode) return;
if (!serverStore.localIsRouter) return;
untrack(() => {
modelsStore.status.subscribe();
});
return () => {
modelsStore.status.unsubscribe();
};
});
// Background MCP server health checks on app load.