File size: 5,101 Bytes
c6ccfa4
 
 
 
 
 
 
 
7086abd
 
c5304fa
7086abd
c5304fa
 
7bf1507
 
6c59df2
 
7bf1507
a1a6daf
 
 
 
 
6c59df2
7086abd
a1a6daf
 
7086abd
6c59df2
 
 
 
7bf1507
 
 
 
 
39e1646
 
 
6c59df2
 
 
 
 
 
 
 
 
 
 
 
 
c5304fa
 
 
7086abd
c5304fa
6c59df2
9af277e
a1a6daf
7086abd
c5304fa
 
6c59df2
c5304fa
 
 
 
a1a6daf
 
 
39e1646
 
 
 
 
 
7086abd
 
7174ecf
7bf1507
7174ecf
7086abd
 
d9327c0
6c59df2
7086abd
7bf1507
7086abd
e854d93
a7560a6
17a7bb7
 
 
a7560a6
c5304fa
7bf1507
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7086abd
4f54883
39e1646
 
 
 
 
 
 
 
 
 
 
6c59df2
dc89e59
6c59df2
 
 
 
 
39e1646
6c59df2
 
 
 
 
 
 
 
 
 
dc89e59
6c59df2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39e1646
6c59df2
 
 
 
 
 
 
39e1646
 
 
 
39db86d
7bf1507
 
6c59df2
 
 
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
<script lang="ts" module>
	let isOpen = $state(false);

	export function closeMobileNav() {
		isOpen = false;
	}
</script>

<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 IconShare from "$lib/components/icons/IconShare.svelte";
	import IconBurger from "$lib/components/icons/IconBurger.svelte";
	import { Spring } from "svelte/motion";
	import { pan, type GestureCustomEvent, type PanCustomEvent } from "svelte-gestures";
	import { shareModal } from "$lib/stores/shareModal";
	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 panX: number | undefined = $state(undefined);
	let panStart: number | undefined = $state(undefined);
	let panStartTime: number | undefined = undefined;

	const isHuggingChat = $derived(Boolean(page.data?.publicConfig?.isHuggingChat));
	const canShare = $derived(
		isHuggingChat && Boolean(page.params?.id) && page.route.id?.startsWith("/conversation/")
	);

	// Define the width for the drawer (less than 100% to create the gap)
	const drawerWidthPercentage = 85;

	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();
		}
	});

	// Function to close the drawer when background is tapped
	function closeDrawer() {
		isOpen = false;
		panX = undefined;
	}
</script>

<nav
	class="flex h-12 items-center justify-between rounded-b-xl border-b bg-gray-50 px-3 dark:border-gray-800 dark:bg-gray-800/30 dark:shadow-xl 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}><IconBurger /></button
	>
	<div class="flex h-full items-center justify-center overflow-hidden">
		{#if page.params?.id}
			<span class="max-w-full truncate px-4 first-letter:uppercase" data-testid="chat-title"
				>{title}</span
			>
		{/if}
	</div>
	<div class="flex items-center">
		{#if isHuggingChat}
			<button
				type="button"
				class="flex size-8 shrink-0 items-center justify-center text-lg"
				disabled={!canShare}
				onclick={() => {
					if (!canShare) return;
					shareModal.open();
				}}
				aria-label="Share conversation"
			>
				<IconShare classNames={!canShare ? "opacity-40" : ""} />
			</button>
		{/if}
		<a href="{base}/" class="flex size-8 shrink-0 items-center justify-center text-lg">
			<IconNew />
		</a>
	</div>
</nav>

<!-- Mobile drawer overlay - shows when drawer is open -->
{#if isOpen}
	<button
		type="button"
		class="fixed inset-0 z-20 cursor-default bg-black/30 md:hidden"
		style="opacity: {Math.max(0, Math.min(1, (100 + tween.current) / 100))};"
		onclick={closeDrawer}
		aria-label="Close mobile navigation"
	></button>
{/if}

<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 * (drawerWidthPercentage / 100);

		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 * (drawerWidthPercentage / 100);

		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)
	)}%); width: {drawerWidthPercentage}%;"
	class:shadow-[5px_0_15px_0_rgba(0,0,0,0.3)]={isOpen}
	class="fixed bottom-0 left-0 top-0 z-30 grid max-h-screen grid-cols-1
	grid-rows-[auto,1fr,auto,auto] rounded-r-xl bg-white pt-4 dark:bg-gray-900 md:hidden"
>
	{@render children?.()}
</nav>