File size: 3,848 Bytes
3baea8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
<script lang="ts">
	import { browser } from "$app/environment";
	import { beforeNavigate } from "$app/navigation";
	import { base } from "$app/paths";
	import { page } from "$app/state";
	import IconNew from "$lib/components/icons/IconNew.svelte";
	import { Spring } from "svelte/motion";
	import CarbonClose from "~icons/carbon/close";
	import CarbonTextAlignJustify from "~icons/carbon/text-align-justify";
	import { pan, type GestureCustomEvent, type PanCustomEvent } from "svelte-gestures";
	interface Props {
		title: string | undefined;
		children?: import("svelte").Snippet;
	}

	let { title = $bindable(), children }: Props = $props();

	let closeEl: HTMLButtonElement | undefined = $state();
	let openEl: HTMLButtonElement | undefined = $state();

	let isOpen = $state(false);
	let panX: number | undefined = $state(undefined);
	let panStart: number | undefined = $state(undefined);
	let panStartTime: number | undefined = undefined;

	const tween = Spring.of(
		() => {
			if (panX !== undefined) {
				return panX;
			}
			if (isOpen) {
				return 0 as number;
			}
			return -100 as number;
		},
		{ stiffness: 0.2, damping: 0.8 }
	);

	$effect(() => {
		title ??= "New Chat";
	});

	beforeNavigate(() => {
		isOpen = false;
		panX = undefined;
	});

	let shouldFocusClose = $derived(isOpen && closeEl);
	let shouldRefocusOpen = $derived(!isOpen && browser && document.activeElement === closeEl);

	$effect(() => {
		if (shouldFocusClose) {
			closeEl?.focus();
		} else if (shouldRefocusOpen) {
			openEl?.focus();
		}
	});
</script>

<nav
	class="flex h-12 items-center justify-between border-b bg-gray-50 px-3 dark:border-gray-800 dark:bg-gray-800/70 md:hidden"
>
	<button
		type="button"
		class="-ml-3 flex size-12 shrink-0 items-center justify-center text-lg"
		onclick={() => (isOpen = true)}
		aria-label="Open menu"
		bind:this={openEl}><CarbonTextAlignJustify /></button
	>
	<div class="flex h-full items-center justify-center">
		{#if page.params?.id}
			<span class="truncate px-4" data-testid="chat-title">{title}</span>
		{/if}
	</div>
	<a
		class:invisible={!page.params?.id}
		href="{base}/"
		class="-mr-3 flex size-12 shrink-0 items-center justify-center text-lg"><IconNew /></a
	>
</nav>

<nav
	use:pan={() => ({ delay: 0, preventdefault: true, touchAction: "pan-left" })}
	onpanup={(e: GestureCustomEvent) => {
		if (!panStart || !panStartTime || !panX) {
			return;
		}
		// measure the pan velocity to determine if the menu should snap open or closed
		const drawerWidth = window.innerWidth;

		const trueX = e.detail.x + (panX / 100) * drawerWidth;

		const panDuration = Date.now() - panStartTime;
		const panVelocity = (trueX - panStart) / panDuration;

		panX = undefined;
		panStart = undefined;
		panStartTime = undefined;

		if (panVelocity < -0.5 || trueX < 50) {
			isOpen = !isOpen;
		}
	}}
	onpan={(e: PanCustomEvent) => {
		if (e.detail.pointerType !== "touch") {
			panX = undefined;
			panStart = undefined;
			panStartTime = undefined;
			return;
		}

		panX ??= 0;
		panStart ??= e.detail.x;
		panStartTime ??= Date.now();

		const drawerWidth = window.innerWidth;

		const trueX = e.detail.x + (panX / 100) * drawerWidth;
		const percentage = ((trueX - panStart) / drawerWidth) * 100;

		panX = Math.max(-100, Math.min(0, percentage));
		tween.set(panX, { instant: true });
	}}
	style="transform: translateX({Math.max(-100, Math.min(0, tween.current))}%);"
	class="fixed inset-0 z-30 grid max-h-screen
	grid-cols-1 grid-rows-[auto,1fr,auto,auto] bg-white pt-4 dark:bg-gray-900 md:hidden"
>
	{#if page.url.pathname === base + "/"}
		<button
			type="button"
			class="absolute right-0 top-0 z-50 flex size-12 items-center justify-center text-lg"
			onclick={() => (isOpen = false)}
			aria-label="Close menu"
			bind:this={closeEl}><CarbonClose /></button
		>
	{/if}
	{@render children?.()}
</nav>