mirror of
				https://github.com/Blah-IM/Weblah.git
				synced 2025-11-04 03:41:37 +00:00 
			
		
		
		
	feat: view transition, chat item sizing & group / unread
This commit is contained in:
		
							parent
							
								
									6c83b2dc6b
								
							
						
					
					
						commit
						f3f32a5326
					
				
					 18 changed files with 368 additions and 101 deletions
				
			
		| 
						 | 
				
			
			@ -25,6 +25,16 @@ export default [
 | 
			
		|||
			parserOptions: {
 | 
			
		||||
				parser: ts.parser
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
		rules: {
 | 
			
		||||
			'@typescript-eslint/no-unused-vars': [
 | 
			
		||||
				'error',
 | 
			
		||||
				{ varsIgnorePattern: '^(\\$\\$(Props|Events|Slots)$|_)' }
 | 
			
		||||
			],
 | 
			
		||||
			'@typescript-eslint/no-empty-object-type': [
 | 
			
		||||
				'error',
 | 
			
		||||
				{ allowInterfaces: 'with-single-extends' }
 | 
			
		||||
			]
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	{
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
	"version": "0.0.1",
 | 
			
		||||
	"private": true,
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"dev": "vite dev",
 | 
			
		||||
		"dev": "vite dev --host",
 | 
			
		||||
		"build": "vite build",
 | 
			
		||||
		"preview": "vite preview",
 | 
			
		||||
		"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										42
									
								
								src/app.css
									
										
									
									
									
								
							
							
						
						
									
										42
									
								
								src/app.css
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -1,3 +1,45 @@
 | 
			
		|||
@import 'tailwindcss/base';
 | 
			
		||||
@import 'tailwindcss/components';
 | 
			
		||||
@import 'tailwindcss/utilities';
 | 
			
		||||
 | 
			
		||||
@layer base {
 | 
			
		||||
	.weblah-light-theme {
 | 
			
		||||
		--weblah-color-sb-primary: theme(colors.slate.50);
 | 
			
		||||
		--weblah-color-sb-secondary: theme(colors.slate.100);
 | 
			
		||||
		--weblah-color-sb-tertiary: theme(colors.slate.200);
 | 
			
		||||
 | 
			
		||||
		--weblah-color-sf-primary: theme(colors.slate.900);
 | 
			
		||||
		--weblah-color-sf-secondary: theme(colors.slate.500);
 | 
			
		||||
		--weblah-color-sf-tertiary: theme(colors.slate.400);
 | 
			
		||||
 | 
			
		||||
		--weblah-color-ss-primary: theme(colors.slate.300);
 | 
			
		||||
		--weblah-color-ss-secondary: theme(colors.slate.300 / 60%);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.weblah-dark-theme {
 | 
			
		||||
		--weblah-color-sb-primary: theme(colors.slate.950);
 | 
			
		||||
		--weblah-color-sb-secondary: theme(colors.slate.900);
 | 
			
		||||
		--weblah-color-sb-tertiary: theme(colors.slate.800);
 | 
			
		||||
 | 
			
		||||
		--weblah-color-sf-primary: theme(colors.slate.100);
 | 
			
		||||
		--weblah-color-sf-secondary: theme(colors.slate.400);
 | 
			
		||||
		--weblah-color-sf-tertiary: theme(colors.slate.500);
 | 
			
		||||
 | 
			
		||||
		--weblah-color-ss-primary: theme(colors.slate.700);
 | 
			
		||||
		--weblah-color-ss-secondary: theme(colors.slate.700 / 60%);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:root {
 | 
			
		||||
		@apply weblah-light-theme;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:is([data-weblah-color-scheme='dark'] *) {
 | 
			
		||||
		@apply weblah-dark-theme;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@media (prefers-color-scheme: dark) {
 | 
			
		||||
		:not([data-weblah-color-scheme='light'] *) {
 | 
			
		||||
			@apply weblah-dark-theme;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,17 @@
 | 
			
		|||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<html lang="en" class="max-w-[100vw] overflow-hidden">
 | 
			
		||||
	<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.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"
 | 
			
		||||
		/>
 | 
			
		||||
		%sveltekit.head%
 | 
			
		||||
	</head>
 | 
			
		||||
	<body
 | 
			
		||||
		data-sveltekit-preload-data="hover"
 | 
			
		||||
		class="relative h-screen select-none overflow-hidden bg-slate-100 text-black dark:bg-slate-900 dark:text-white"
 | 
			
		||||
		class="relative h-screen max-w-[100vw] touch-pan-x touch-pan-y select-none overflow-hidden bg-sb-secondary text-sf-primary"
 | 
			
		||||
	>
 | 
			
		||||
		<div style="display: contents">%sveltekit.body%</div>
 | 
			
		||||
	</body>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										28
									
								
								src/lib/components/Button.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/lib/components/Button.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,28 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { tw } from '$lib/tw';
 | 
			
		||||
	import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
 | 
			
		||||
 | 
			
		||||
	type HTMLButtonOrAnchorAttributes = Partial<HTMLAnchorAttributes> & Partial<HTMLButtonAttributes>;
 | 
			
		||||
 | 
			
		||||
	interface $$Props extends HTMLButtonOrAnchorAttributes {}
 | 
			
		||||
 | 
			
		||||
	let className: string | null = '';
 | 
			
		||||
	export { className as class };
 | 
			
		||||
 | 
			
		||||
	export let href: string | null = null;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<svelte:element
 | 
			
		||||
	this={href ? 'a' : 'button'}
 | 
			
		||||
	{href}
 | 
			
		||||
	class={tw(
 | 
			
		||||
		'inline-flex cursor-default items-center justify-center rounded-md px-2 py-1 text-sf-secondary shadow-sm ring-1 ring-ss-secondary transition-shadow duration-200 hover:ring-ss-primary active:shadow-inner',
 | 
			
		||||
		className
 | 
			
		||||
	)}
 | 
			
		||||
	{...$$restProps}
 | 
			
		||||
	on:click
 | 
			
		||||
	role="button"
 | 
			
		||||
	tabindex="0"
 | 
			
		||||
>
 | 
			
		||||
	<slot />
 | 
			
		||||
</svelte:element>
 | 
			
		||||
							
								
								
									
										15
									
								
								src/lib/components/InputFrame.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/lib/components/InputFrame.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,15 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { tw } from '$lib/tw';
 | 
			
		||||
 | 
			
		||||
	let className = '';
 | 
			
		||||
	export { className as class };
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class={tw(
 | 
			
		||||
		'flex items-center gap-1 rounded-md px-2 py-1.5 shadow-[inset_0_1px_2px_0_rgb(0_0_0/0.05)] ring-1 ring-ss-secondary',
 | 
			
		||||
		className
 | 
			
		||||
	)}
 | 
			
		||||
>
 | 
			
		||||
	<slot />
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										68
									
								
								src/routes/(app)/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/routes/(app)/+layout.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,68 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import { onNavigate } from '$app/navigation';
 | 
			
		||||
	import ChatList from './ChatList.svelte';
 | 
			
		||||
 | 
			
		||||
	onNavigate((navigation) => {
 | 
			
		||||
		if (!document.startViewTransition) return;
 | 
			
		||||
 | 
			
		||||
		return new Promise((resolve) => {
 | 
			
		||||
			document.startViewTransition(async () => {
 | 
			
		||||
				resolve();
 | 
			
		||||
				await navigation.complete;
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	$: mainVisible = !!$page.params.chatId;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="group h-screen sm:flex sm:items-stretch"
 | 
			
		||||
	data-weblah-main-visible={mainVisible ? 'true' : undefined}
 | 
			
		||||
>
 | 
			
		||||
	<aside
 | 
			
		||||
		class="relative h-screen min-h-0 overflow-hidden border-ss-primary bg-sb-primary shadow-lg [view-transition-name:chat-list] after:pointer-events-none after:absolute after:inset-0 after:size-full after:bg-transparent group-data-[weblah-main-visible]:after:bg-black/30 sm:w-1/3 sm:border-e sm:after:hidden lg:w-1/4"
 | 
			
		||||
	>
 | 
			
		||||
		<ChatList />
 | 
			
		||||
	</aside>
 | 
			
		||||
	{#if mainVisible}
 | 
			
		||||
		<main
 | 
			
		||||
			class="absolute inset-0 w-full bg-sb-secondary shadow-lg [view-transition-name:main] sm:relative sm:flex-1 sm:shadow-none"
 | 
			
		||||
		>
 | 
			
		||||
			<slot></slot>
 | 
			
		||||
		</main>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<div class="hidden flex-1 sm:block"><slot /></div>
 | 
			
		||||
	{/if}
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	@keyframes slide-in {
 | 
			
		||||
		from {
 | 
			
		||||
			transform: translateX(100%);
 | 
			
		||||
		}
 | 
			
		||||
		to {
 | 
			
		||||
			transform: translateX(0);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	@keyframes slide-out {
 | 
			
		||||
		from {
 | 
			
		||||
			transform: translateX(0);
 | 
			
		||||
		}
 | 
			
		||||
		to {
 | 
			
		||||
			transform: translateX(100%);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	:root::view-transition-old(root),
 | 
			
		||||
	:root::view-transition-new(root) {
 | 
			
		||||
		animation-duration: 250ms;
 | 
			
		||||
	}
 | 
			
		||||
	:root::view-transition-old(main) {
 | 
			
		||||
		animation: 250ms ease-out slide-out;
 | 
			
		||||
	}
 | 
			
		||||
	:root::view-transition-new(main) {
 | 
			
		||||
		animation: 250ms ease-out slide-in;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -5,13 +5,50 @@
 | 
			
		|||
 | 
			
		||||
<div class="flex h-screen flex-col justify-stretch">
 | 
			
		||||
	<ChatListHeader />
 | 
			
		||||
	<div class="min-h-0 flex-1 overflow-y-auto">
 | 
			
		||||
	<div class="min-h-0 flex-1 touch-pan-y overflow-y-auto">
 | 
			
		||||
		<ul>
 | 
			
		||||
			<ChatListItem
 | 
			
		||||
				chat={{
 | 
			
		||||
					id: 'room-1',
 | 
			
		||||
					name: 'Blah IM Interest Group',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '1', name: 'septs' },
 | 
			
		||||
						content: '窄带通信吧,asn1 + bzip2 效果还是可以的',
 | 
			
		||||
						date: new Date('2024-08-29T02:11Z')
 | 
			
		||||
					},
 | 
			
		||||
					unreadCount: 3
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
			<ChatListItem
 | 
			
		||||
				chat={{
 | 
			
		||||
					id: 'room-2',
 | 
			
		||||
					name: 'Laoself Chat',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '10', name: 'Richard Luo 🐱' },
 | 
			
		||||
						content: '如果durov没事 那你是不是又可以拖延症复发了(',
 | 
			
		||||
						date: new Date('2024-08-29T02:11Z')
 | 
			
		||||
					}
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
			<ChatListItem
 | 
			
		||||
				chat={{
 | 
			
		||||
					id: 'room-3',
 | 
			
		||||
					name: 'Ubuntu中文',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '15', name: 'chen jianhao' },
 | 
			
		||||
						content:
 | 
			
		||||
							'就什么也没安装,昨晚好好的,就打开了pycharm而已,写完代码直接按设置好的快捷键alt+s 关机 重启就不行了',
 | 
			
		||||
						date: new Date('2024-08-29T02:27Z')
 | 
			
		||||
					},
 | 
			
		||||
					unreadCount: 827469
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
			<ChatListItem
 | 
			
		||||
				chat={{
 | 
			
		||||
					id: '1',
 | 
			
		||||
					name: 'septs',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '1', name: 'septs' },
 | 
			
		||||
						content: '验证 checksum 是否正确的代价还是可以接受的',
 | 
			
		||||
						date: new Date('2024-08-28T02:54Z')
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			@ -22,6 +59,7 @@
 | 
			
		|||
					id: '2',
 | 
			
		||||
					name: 'oxa',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '2', name: 'oxa' },
 | 
			
		||||
						content: '但似乎现在大家都讨厌 pgp ,觉得太复杂',
 | 
			
		||||
						date: new Date('2024-08-28T02:37Z')
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			@ -32,9 +70,11 @@
 | 
			
		|||
					id: '3',
 | 
			
		||||
					name: 'omo',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '3', name: 'omo' },
 | 
			
		||||
						content: '我對 revalidate 的理解是不經過 cache 直接重拉一遍',
 | 
			
		||||
						date: new Date('2024-08-28T02:11Z')
 | 
			
		||||
					}
 | 
			
		||||
					},
 | 
			
		||||
					unreadCount: 8
 | 
			
		||||
				}}
 | 
			
		||||
			/>
 | 
			
		||||
			<ChatListItem
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +82,7 @@
 | 
			
		|||
					id: '4',
 | 
			
		||||
					name: 'Inno Aiolos',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '4', name: 'Inno Aiolos' },
 | 
			
		||||
						content: '至少得把信息分发给所有广播自己是这个public key的destination',
 | 
			
		||||
						date: new Date('2024-07-28T02:11Z')
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +93,7 @@
 | 
			
		|||
					id: '5',
 | 
			
		||||
					name: 'Gary です',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '5', name: 'Gary です' },
 | 
			
		||||
						content: '没必要8,长毛象那样挺麻烦的',
 | 
			
		||||
						date: new Date('2023-07-28T02:11Z')
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			@ -62,6 +104,7 @@
 | 
			
		|||
					id: '6',
 | 
			
		||||
					name: 'Chtholly Nota Seniorious',
 | 
			
		||||
					lastMessage: {
 | 
			
		||||
						sender: { id: '6', name: 'Chtholly Nota Seniorious' },
 | 
			
		||||
						content: '遥遥领先!\n隔壁 nostr 最开始没有注意到这个问题,然后被狂灌置顶 spam',
 | 
			
		||||
						date: new Date('2022-07-28T02:11Z')
 | 
			
		||||
					}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,12 +1,12 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import Button from '$lib/components/Button.svelte';
 | 
			
		||||
	import InputFrame from '$lib/components/InputFrame.svelte';
 | 
			
		||||
	import { AvatarBeam } from 'svelte-boring-avatars';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<header class="flex items-center justify-stretch gap-2 border-b border-slate-200 p-2 shadow-sm">
 | 
			
		||||
<header class="flex items-center justify-stretch gap-2 border-b border-ss-secondary p-2 shadow-sm">
 | 
			
		||||
	<div><AvatarBeam size={30} name="Shibo Lyu" /></div>
 | 
			
		||||
	<div
 | 
			
		||||
		class="flex flex-1 items-center gap-1 rounded-md bg-slate-50 px-2 py-1 ring-1 ring-slate-100"
 | 
			
		||||
	>
 | 
			
		||||
	<InputFrame class="flex-1">
 | 
			
		||||
		<svg
 | 
			
		||||
			xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
			viewBox="0 0 24 24"
 | 
			
		||||
| 
						 | 
				
			
			@ -24,10 +24,8 @@
 | 
			
		|||
			placeholder="Search"
 | 
			
		||||
			class="w-full flex-1 bg-transparent text-sm leading-4 text-slate-900 focus:outline-none"
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<button
 | 
			
		||||
		class="flex size-7 cursor-default items-center justify-center rounded-md text-slate-500 ring-1 ring-slate-100 transition-shadow duration-200 hover:shadow-sm"
 | 
			
		||||
	>
 | 
			
		||||
	</InputFrame>
 | 
			
		||||
	<Button class="size-8">
 | 
			
		||||
		<svg
 | 
			
		||||
			xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
			fill="none"
 | 
			
		||||
| 
						 | 
				
			
			@ -43,5 +41,5 @@
 | 
			
		|||
			/>
 | 
			
		||||
		</svg>
 | 
			
		||||
		<span class="sr-only">Compose</span>
 | 
			
		||||
	</button>
 | 
			
		||||
	</Button>
 | 
			
		||||
</header>
 | 
			
		||||
							
								
								
									
										73
									
								
								src/routes/(app)/ChatListItem.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/routes/(app)/ChatListItem.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,73 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { AvatarBeam } from 'svelte-boring-avatars';
 | 
			
		||||
 | 
			
		||||
	export let chat: {
 | 
			
		||||
		id: string;
 | 
			
		||||
		name: string;
 | 
			
		||||
		lastMessage: { sender: { id: string; name: string }; content: string; date: Date };
 | 
			
		||||
		unreadCount?: number;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const sameDayFormatter = new Intl.DateTimeFormat('default', {
 | 
			
		||||
		hour: '2-digit',
 | 
			
		||||
		minute: '2-digit'
 | 
			
		||||
	});
 | 
			
		||||
	const sameYearFormatter = new Intl.DateTimeFormat('default', {
 | 
			
		||||
		month: 'short',
 | 
			
		||||
		day: 'numeric'
 | 
			
		||||
	});
 | 
			
		||||
	const otherYearFormatter = new Intl.DateTimeFormat('default', {
 | 
			
		||||
		year: 'numeric',
 | 
			
		||||
		month: 'short',
 | 
			
		||||
		day: 'numeric'
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const formatDate = (date: Date) => {
 | 
			
		||||
		const now = new Date();
 | 
			
		||||
		if (date.getFullYear() === now.getFullYear()) {
 | 
			
		||||
			if (date.getMonth() === now.getMonth() && date.getDate() === now.getDate()) {
 | 
			
		||||
				return sameDayFormatter.format(date);
 | 
			
		||||
			} else {
 | 
			
		||||
				return sameYearFormatter.format(date);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return otherYearFormatter.format(date);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const unreadCountFormatter = new Intl.NumberFormat('default', {
 | 
			
		||||
		notation: 'compact',
 | 
			
		||||
		compactDisplay: 'short'
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<li
 | 
			
		||||
	class="relative after:absolute after:bottom-0 after:end-0 after:start-14 after:border-t-[0.5px] after:border-ss-secondary"
 | 
			
		||||
>
 | 
			
		||||
	<a href="/chats/{chat.id}" class="flex h-20 cursor-default items-center gap-2 px-2">
 | 
			
		||||
		<div class="size-10">
 | 
			
		||||
			<AvatarBeam size={40} name={chat.name} />
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="relative min-w-0 flex-1">
 | 
			
		||||
			<div class="flex items-center gap-1">
 | 
			
		||||
				<h3 class="flex-1 truncate text-sm font-semibold">{chat.name}</h3>
 | 
			
		||||
				<time class="truncate text-xs text-sf-tertiary">{formatDate(chat.lastMessage.date)}</time>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div class="flex items-end gap-1">
 | 
			
		||||
				<p class="line-clamp-2 h-[2.5em] text-sm leading-tight text-sf-secondary">
 | 
			
		||||
					{#if chat.id !== chat.lastMessage.sender.id}
 | 
			
		||||
						<span class="text-sf-primary">{chat.lastMessage.sender.name}: </span>
 | 
			
		||||
					{/if}
 | 
			
		||||
					{chat.lastMessage.content}
 | 
			
		||||
				</p>
 | 
			
		||||
				{#if chat.unreadCount}
 | 
			
		||||
					<span
 | 
			
		||||
						class="whitespace-nowrap rounded-full bg-slate-400 px-1.5 py-0.5 text-xs text-slate-50 dark:bg-slate-500 dark:text-slate-950"
 | 
			
		||||
					>
 | 
			
		||||
						{unreadCountFormatter.format(chat.unreadCount)}
 | 
			
		||||
					</span>
 | 
			
		||||
				{/if}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</a>
 | 
			
		||||
</li>
 | 
			
		||||
							
								
								
									
										6
									
								
								src/routes/(app)/chats/[chatId]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/routes/(app)/chats/[chatId]/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import Button from '$lib/components/Button.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<p><Button href="/">Close</Button> History Page for {$page.params.chatId}</p>
 | 
			
		||||
							
								
								
									
										16
									
								
								src/routes/(internal)/_design-system/+page.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/routes/(internal)/_design-system/+page.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
<script>
 | 
			
		||||
	import ColorPalette from './ColorPalette.svelte';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="p-4">
 | 
			
		||||
	<h1 class="text-2xl">Colors</h1>
 | 
			
		||||
 | 
			
		||||
	<div class="flex">
 | 
			
		||||
		<div data-weblah-color-scheme="light">
 | 
			
		||||
			<ColorPalette title="Light" />
 | 
			
		||||
		</div>
 | 
			
		||||
		<div data-weblah-color-scheme="dark">
 | 
			
		||||
			<ColorPalette title="Dark" />
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										24
									
								
								src/routes/(internal)/_design-system/ColorPalette.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/routes/(internal)/_design-system/ColorPalette.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,24 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	export let title: string = 'Color Palette';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div class="rounded-xl bg-sb-secondary p-4 text-sf-primary">
 | 
			
		||||
	<h2 class="my-2 text-xl">{title}</h2>
 | 
			
		||||
	<div class="flex gap-4">
 | 
			
		||||
		<div class="w-32 rounded-md border border-ss-primary bg-sb-primary py-3 text-center">
 | 
			
		||||
			<div class="mb-3 border-b border-ss-secondary pb-3 shadow-sm">Primary BG</div>
 | 
			
		||||
			<p class="text-sf-secondary">Secondary FG</p>
 | 
			
		||||
			<p class="text-sf-tertiary">Tertiary FG</p>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="w-32 rounded-md border border-ss-primary bg-sb-secondary py-3 text-center">
 | 
			
		||||
			<div class="mb-3 border-b border-ss-secondary pb-3 shadow-sm">Secondary BG</div>
 | 
			
		||||
			<p class="text-sf-secondary">Secondary FG</p>
 | 
			
		||||
			<p class="text-sf-tertiary">Tertiary FG</p>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="w-32 rounded-md border border-ss-primary bg-sb-tertiary py-3 text-center">
 | 
			
		||||
			<div class="mb-3 border-b border-ss-secondary pb-3 shadow-sm">Tertiary BG</div>
 | 
			
		||||
			<p class="text-sf-secondary">Secondary FG</p>
 | 
			
		||||
			<p class="text-sf-tertiary">Tertiary FG</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,33 +1,5 @@
 | 
			
		|||
<script>
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import '../app.css';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import { onNavigate } from '$app/navigation';
 | 
			
		||||
	import ChatList from './ChatList.svelte';
 | 
			
		||||
 | 
			
		||||
	onNavigate((navigation) => {
 | 
			
		||||
		if (!document.startViewTransition) return;
 | 
			
		||||
 | 
			
		||||
		return new Promise((resolve) => {
 | 
			
		||||
			document.startViewTransition(async () => {
 | 
			
		||||
				resolve();
 | 
			
		||||
				await navigation.complete;
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="group h-screen sm:flex sm:items-stretch"
 | 
			
		||||
	data-weblah-main-visible={$page.params.chatId ? 'true' : undefined}
 | 
			
		||||
>
 | 
			
		||||
	<aside
 | 
			
		||||
		class="h-screen min-h-0 overflow-hidden border-slate-300 bg-white shadow-lg sm:w-1/3 sm:border-e lg:w-1/4 dark:bg-black"
 | 
			
		||||
	>
 | 
			
		||||
		<ChatList />
 | 
			
		||||
	</aside>
 | 
			
		||||
	<main
 | 
			
		||||
		class="absolute inset-0 -z-10 w-full translate-x-[110vw] bg-slate-100 shadow-lg transition-transform duration-300 group-data-[weblah-main-visible]:z-0 group-data-[weblah-main-visible]:translate-x-0 sm:relative sm:flex-1 sm:translate-x-0 sm:shadow-none dark:bg-slate-900"
 | 
			
		||||
	>
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</main>
 | 
			
		||||
</div>
 | 
			
		||||
<slot />
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,49 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { AvatarBeam } from 'svelte-boring-avatars';
 | 
			
		||||
 | 
			
		||||
	export let chat: { id: string; name: string; lastMessage: { content: string; date: Date } };
 | 
			
		||||
 | 
			
		||||
	const sameDayFormatter = new Intl.DateTimeFormat('default', {
 | 
			
		||||
		hour: '2-digit',
 | 
			
		||||
		minute: '2-digit'
 | 
			
		||||
	});
 | 
			
		||||
	const sameYearFormatter = new Intl.DateTimeFormat('default', {
 | 
			
		||||
		month: 'short',
 | 
			
		||||
		day: 'numeric'
 | 
			
		||||
	});
 | 
			
		||||
	const otherYearFormatter = new Intl.DateTimeFormat('default', {
 | 
			
		||||
		year: 'numeric',
 | 
			
		||||
		month: 'short',
 | 
			
		||||
		day: 'numeric'
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const formatDate = (date: Date) => {
 | 
			
		||||
		const now = new Date();
 | 
			
		||||
		if (date.getFullYear() === now.getFullYear()) {
 | 
			
		||||
			if (date.getMonth() === now.getMonth() && date.getDate() === now.getDate()) {
 | 
			
		||||
				return sameDayFormatter.format(date);
 | 
			
		||||
			} else {
 | 
			
		||||
				return sameYearFormatter.format(date);
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			return otherYearFormatter.format(date);
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<li
 | 
			
		||||
	class="relative after:absolute after:bottom-0 after:end-0 after:start-14 after:border-t after:border-slate-100"
 | 
			
		||||
>
 | 
			
		||||
	<a href="/chats/{chat.id}" class="flex h-16 cursor-default items-center gap-2 px-2">
 | 
			
		||||
		<div class="size-10">
 | 
			
		||||
			<AvatarBeam size={40} name={chat.name} />
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="relative h-16 min-w-0 flex-1 space-y-0.5 py-1">
 | 
			
		||||
			<div class="flex items-center">
 | 
			
		||||
				<h3 class="flex-1 truncate text-sm font-semibold">{chat.name}</h3>
 | 
			
		||||
				<time class="truncate text-xs text-gray-500">{formatDate(chat.lastMessage.date)}</time>
 | 
			
		||||
			</div>
 | 
			
		||||
			<p class="line-clamp-2 text-xs text-gray-500">{chat.lastMessage.content}</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</a>
 | 
			
		||||
</li>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
<script lang="ts">
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<p>Chat History Page for {$page.params.chatId}</p>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +1,36 @@
 | 
			
		|||
import type { Config } from 'tailwindcss';
 | 
			
		||||
import colors from 'tailwindcss/colors';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
	content: ['./src/**/*.{html,js,svelte,ts}'],
 | 
			
		||||
 | 
			
		||||
	darkMode: [
 | 
			
		||||
		'variant',
 | 
			
		||||
		[
 | 
			
		||||
			'@media (prefers-color-scheme: dark) { &:not([data-weblah-color-scheme="light"] *) }',
 | 
			
		||||
			'&:is([data-weblah-color-scheme="dark"] *)'
 | 
			
		||||
		]
 | 
			
		||||
	],
 | 
			
		||||
 | 
			
		||||
	theme: {
 | 
			
		||||
		extend: {
 | 
			
		||||
			colors: {
 | 
			
		||||
				accent: colors.blue
 | 
			
		||||
				// Semantic Background
 | 
			
		||||
				sb: {
 | 
			
		||||
					primary: 'var(--weblah-color-sb-primary)',
 | 
			
		||||
					secondary: 'var(--weblah-color-sb-secondary)',
 | 
			
		||||
					tertiary: 'var(--weblah-color-sb-tertiary)'
 | 
			
		||||
				},
 | 
			
		||||
				// Semantic Foreground
 | 
			
		||||
				sf: {
 | 
			
		||||
					primary: 'var(--weblah-color-sf-primary)',
 | 
			
		||||
					secondary: 'var(--weblah-color-sf-secondary)',
 | 
			
		||||
					tertiary: 'var(--weblah-color-sf-tertiary)'
 | 
			
		||||
				},
 | 
			
		||||
				// Semantic Stroke
 | 
			
		||||
				ss: {
 | 
			
		||||
					primary: 'var(--weblah-color-ss-primary)',
 | 
			
		||||
					secondary: 'var(--weblah-color-ss-secondary)'
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue