improve mobile

This commit is contained in:
Thomas G. Lopes 2025-06-19 17:18:04 +01:00
parent c2cc437061
commit b468afdc8c
10 changed files with 78 additions and 31 deletions

View file

@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "concurrently -n \"convex,vite\" -c \"blue.bold,green.bold\" \"convex dev\" \"vite dev\"",
"dev:host": "concurrently -n \"convex,vite\" -c \"blue.bold,green.bold\" \"convex dev\" \"vite dev --host\"",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",

View file

@ -54,7 +54,7 @@
}
.dark {
--background: oklch(0.2409 0.0201 307.5346);
--background: oklch(0.2409 0.0201 267.5346);
--foreground: oklch(0.8398 0.0387 309.5391);
--card: oklch(0.2803 0.0232 307.5413);
--card-foreground: oklch(0.8456 0.0302 341.4597);
@ -211,13 +211,38 @@
--shadow-2xl: var(--shadow-2xl);
}
@utility fill-device {
height: 100vh;
height: 100dvh; /* Dynamic viewport height - newer browsers */
/* Alternative: Use env() for safe areas */
min-height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom));
/* Ensure full width */
width: 100vw;
width: 100dvw; /* Dynamic viewport width */
/* Remove default margins/padding */
margin: 0;
padding: 0;
overflow-x: hidden;
}
@utility fill-device-height {
height: 100svh;
/* min-height: calc(100vh - env(safe-area-inset-top) - env(safe-area-inset-bottom)); */
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground bg-noise;
@apply bg-background text-foreground bg-noise fill-device;
position: fixed;
overflow: clip;
}
}

View file

@ -3,7 +3,10 @@
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"
/>
<title>thom.chat</title>
%sveltekit.head%
</head>

View file

@ -5,6 +5,6 @@
let { class: className, children, ...rest }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div {...rest} class={cn('bg-background bg-noise col-start-2 h-screen', className)}>
<div {...rest} class={cn('bg-background bg-noise fill-device-height col-start-2', className)}>
{@render children?.()}
</div>

View file

@ -12,7 +12,7 @@
<div
{...rest}
class={cn(
'bg-sidebar border-sidebar-border col-start-1 h-screen w-[--sidebar-width] border-r',
'bg-sidebar border-sidebar-border fill-device-height col-start-1 w-[--sidebar-width] border-r',
className
)}
>

View file

@ -40,6 +40,8 @@
import { callGenerateMessage } from '../api/generate-message/call.js';
import ModelPicker from './model-picker.svelte';
import SearchModal from './search-modal.svelte';
import { shortcut } from '$lib/actions/shortcut.svelte.js';
import { mergeAttrs } from 'melt';
const client = useConvexClient();
@ -378,9 +380,13 @@
<title>Chat | thom.chat</title>
</svelte:head>
<svelte:window
use:shortcut={[{ ctrl: true, key: 'd', callback: () => scrollState.scrollToBottom() }]}
/>
<Sidebar.Root
bind:open={sidebarOpen}
class="h-screen overflow-clip"
class="fill-device-height overflow-clip"
{...currentModelSupportsImages ? omit(fileUpload.dropzone, ['onclick']) : {}}
>
<AppSidebar bind:searchModalOpen />
@ -443,7 +449,7 @@
<LightSwitch variant="ghost" class="size-8" />
</div>
<div class="relative">
<div bind:this={conversationList} class="h-screen overflow-y-auto">
<div bind:this={conversationList} class="fill-device-height overflow-y-auto">
<div
class={cn('mx-auto flex max-w-3xl flex-col', {
'pt-10': page.url.pathname !== '/chat',
@ -452,6 +458,8 @@
>
{@render children()}
</div>
<Tooltip placement="top">
{#snippet trigger(tooltip)}
<Button
onclick={() => scrollState.scrollToBottom()}
variant="secondary"
@ -460,11 +468,16 @@
'text-muted-foreground !border-border absolute bottom-0 left-1/2 z-10 -translate-x-1/2 rounded-full !border !pl-3 text-xs transition',
notAtBottom.current ? 'opacity-100' : 'pointer-events-none scale-95 opacity-0',
]}
style="bottom: {wrapperSize.height + 5}px;"
{...mergeAttrs(tooltip.trigger, {
style: `bottom: ${wrapperSize.height + 5}px;`,
})}
>
Scroll to bottom
<ChevronDownIcon class="inline" />
</Button>
{/snippet}
{cmdOrCtrl} + D
</Tooltip>
</div>
<div
@ -572,10 +585,10 @@
{...pick(popover.trigger, ['id', 'style', 'onfocusout', 'onfocus'])}
bind:this={textarea}
disabled={textareaDisabled}
class="text-foreground placeholder:text-muted-foreground/60 max-h-64 min-h-[80px] w-full resize-none !overflow-y-auto bg-transparent px-3 text-base leading-6 outline-none disabled:cursor-not-allowed disabled:opacity-50"
class="text-foreground placeholder:text-muted-foreground/60 max-h-64 min-h-[60px] w-full resize-none !overflow-y-auto bg-transparent px-3 text-base leading-6 outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:min-h-[80px]"
placeholder={isGenerating
? 'Generating response...'
: 'Type your message here... Tag rules with @'}
: 'Type your message here, tag rules with @'}
name="message"
onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !popover.open) {
@ -634,23 +647,23 @@
{isGenerating ? 'Stop generation' : 'Send message'}
</Tooltip>
</div>
<div class="flex flex-col items-start gap-2 pr-2 sm:flex-row sm:items-center">
<div class="flex flex-row flex-wrap items-center gap-2 pr-2">
<ModelPicker onlyImageModels={selectedImages.length > 0} />
<button
type="button"
class={cn(
'border-border flex items-center gap-1 rounded-full border px-2 py-1 text-xs transition-colors',
'border-border flex items-center gap-1 rounded-full border px-1 py-1 text-xs transition-colors sm:px-2',
settings.webSearchEnabled ? 'bg-accent/50' : 'hover:bg-accent/20'
)}
onclick={() => (settings.webSearchEnabled = !settings.webSearchEnabled)}
>
<SearchIcon class="!size-3" />
<span class="whitespace-nowrap">Web search</span>
<span class="hidden whitespace-nowrap sm:inline">Web search</span>
</button>
{#if currentModelSupportsImages}
<button
type="button"
class="border-border hover:bg-accent/20 flex items-center gap-1 rounded-full border px-2 py-1 text-xs transition-colors disabled:opacity-50"
class="border-border hover:bg-accent/20 flex items-center gap-1 rounded-full border px-1 py-1 text-xs transition-colors disabled:opacity-50 sm:px-2"
onclick={() => fileInput?.click()}
disabled={isUploading}
>
@ -661,7 +674,7 @@
{:else}
<ImageIcon class="!size-3" />
{/if}
<span class="whitespace-nowrap">Attach image</span>
<span class="hidden whitespace-nowrap sm:inline">Attach image</span>
</button>
{/if}
</div>

View file

@ -214,7 +214,7 @@
<button
{...popover.trigger}
class={cn(
'ring-offset-background focus:ring-ring flex w-full items-center justify-between rounded-lg px-2 py-1 text-xs transition hover:text-white focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
'ring-offset-background focus:ring-ring flex items-center justify-between rounded-lg px-2 py-1 text-xs transition hover:text-white focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
className
)}
aria-expanded={open}

View file

@ -77,7 +77,7 @@
}
</script>
<svelte:window use:shortcut={{ ctrl: true, key: 'k', callback: () => (open = true) }} />
<svelte:window use:shortcut={[{ ctrl: true, key: 'k', callback: () => (open = true) }]} />
<Modal bind:open>
<div class="space-y-4">
@ -121,7 +121,7 @@
</div>
{:else if search.data?.length}
<div class="max-h-96 space-y-2 overflow-y-auto">
{#each search.data as { conversation, messages, score, titleMatch }, index}
{#each search.data as { conversation, messages, titleMatch }, index}
<div
data-result-index={index}
class="border-border flex cursor-pointer items-center justify-between gap-2 rounded-lg border px-3 py-2 transition-colors {index ===

View file

@ -34,7 +34,7 @@
<meta name="description" content="A shared conversation from thom.chat" />
</svelte:head>
<div class="min-h-screen">
<div class="fill-device-height">
<!-- Header -->
<header
class="border-border bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 border-b backdrop-blur"

View file

@ -4,6 +4,8 @@ import { sveltekit } from '@sveltejs/kit/vite';
import Icons from 'unplugin-icons/vite';
import { defineConfig } from 'vite';
const isDev = process.env.NODE_ENV === 'development';
export default defineConfig({
plugins: [
tailwindcss(),
@ -37,4 +39,7 @@ export default defineConfig({
},
],
},
server: {
allowedHosts: isDev ? true : undefined,
},
});