File size: 6,156 Bytes
654f56a
fbef5e8
84f775e
3df1e9f
af10150
84f775e
64cfbce
 
aac02fe
84f775e
b2170a7
 
dab40ed
84f775e
b1426d3
f77d645
7450ebd
1778c9e
6a8491a
af10150
 
 
 
 
6a8491a
1778c9e
654f56a
af10150
1778c9e
af10150
1778c9e
af10150
 
654f56a
aac02fe
84f775e
 
 
 
 
 
 
 
 
 
 
 
 
1778c9e
84f775e
 
 
aac02fe
84f775e
 
 
1778c9e
84f775e
 
 
 
 
 
654f56a
 
aac02fe
654f56a
af10150
654f56a
aac02fe
7e80e42
654f56a
 
 
1778c9e
654f56a
64cfbce
 
 
 
 
654f56a
f77d645
cf47645
7450ebd
cf47645
 
 
84f775e
cf47645
 
 
1778c9e
cf47645
 
 
 
1778c9e
cf47645
 
 
 
 
 
 
aac02fe
654f56a
af10150
1778c9e
af10150
7450ebd
af10150
6a8491a
aac02fe
6a8491a
7450ebd
 
dab40ed
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7450ebd
84f775e
7450ebd
 
 
 
 
 
aac02fe
 
 
 
af10150
aac02fe
1778c9e
aac02fe
 
 
 
 
 
af10150
aac02fe
1778c9e
aac02fe
 
 
 
 
 
6a8491a
af10150
aac02fe
654f56a
84f775e
 
 
 
 
 
 
 
 
fbef5e8
84f775e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
184
185
186
187
188
189
190
191
192
193
<script lang="ts">
	import { autofocus } from "$lib/attachments/autofocus.js";
	import { checkpoints } from "$lib/state/checkpoints.svelte";
	import { cn } from "$lib/utils/cn.js";
	import { Select } from "melt/builders";
	import type { EventHandler } from "svelte/elements";
	import IconCaret from "~icons/carbon/chevron-down";
	import IconCross from "~icons/carbon/close";
	import IconEdit from "~icons/carbon/edit";
	import IconHistory from "~icons/carbon/recently-viewed";
	import IconSave from "~icons/carbon/save";
	import IconDelete from "~icons/carbon/trash-can";
	import ArrowSplitRounded from "~icons/material-symbols/arrow-split-rounded";
	import Dialog from "../dialog.svelte";
	import { prompt } from "../prompts.svelte";
	import Tooltip from "../tooltip.svelte";
	import CheckpointsMenu from "./checkpoints-menu.svelte";
	import { projects } from "$lib/state/projects.svelte";

	interface Props {
		class?: string;
	}

	let { class: classNames = "" }: Props = $props();

	const isDefault = $derived(projects.activeId === "default");

	const select = new Select({
		value: () => projects.activeId,
		onValueChange(v) {
			if (v) projects.activeId = v;
		},
		sameWidth: true,
	});

	type SaveDialogState = {
		open: boolean;
		moveCheckpoints: boolean;
		name: string;
	};

	const defaultSdState: SaveDialogState = {
		open: false,
		moveCheckpoints: true,
		name: "",
	};

	let sdState = $state(defaultSdState);
	const projectPlaceholder = $derived(`Project #${projects.all.length}`);

	function openSaveDialog() {
		sdState = { ...defaultSdState, open: true };
	}

	const saveDialog = async function (e) {
		e.preventDefault();
		projects.saveProject({
			...sdState,
			name: sdState.name || projectPlaceholder,
		});

		sdState = { ...defaultSdState };
	} satisfies EventHandler<SubmitEvent>;
</script>

<div class={cn("flex w-full items-stretch gap-2 ", classNames)}>
	<button
		{...select.trigger}
		class={cn(
			"relative flex grow items-center justify-between gap-6 overflow-hidden rounded-lg border bg-gray-100/80 px-3 py-1.5 leading-tight whitespace-nowrap shadow-sm",
			"hover:brightness-95 dark:border-gray-700 dark:bg-gray-800 dark:hover:brightness-110",
		)}
	>
		<div class="flex items-center gap-1 text-sm">
			{projects.current?.name}
		</div>
		<div
			class="absolute right-2 grid size-4 flex-none place-items-center rounded-sm bg-gray-100 text-xs dark:bg-gray-600"
		>
			<IconCaret />
		</div>
	</button>

	<div class="flex items-center gap-2">
		<CheckpointsMenu />
		{#if isDefault}
			<Tooltip>
				{#snippet trigger(tooltip)}
					<button class="btn size-[32px] p-0" {...tooltip.trigger} onclick={openSaveDialog}>
						<IconSave />
					</button>
				{/snippet}
				Save as project
			</Tooltip>
		{:else}
			<Tooltip>
				{#snippet trigger(tooltip)}
					<button class="btn size-[32px] p-0" {...tooltip.trigger} onclick={() => (projects.activeId = "default")}>
						<IconCross />
					</button>
				{/snippet}
				Close project
			</Tooltip>
		{/if}
	</div>
</div>

<div {...select.content} class="rounded-lg border bg-gray-100 dark:border-gray-700 dark:bg-gray-800">
	{#each projects.all as { name, id } (id)}
		{@const option = select.getOption(id)}
		{@const hasCheckpoints = checkpoints.for(id).length > 0}
		<div {...option} class="group block w-full p-1 text-sm dark:text-white">
			<div
				class="flex items-center gap-2 rounded-md py-1.5 pr-1 pl-2 group-data-[highlighted]:bg-gray-200 dark:group-data-[highlighted]:bg-gray-700"
			>
				<div class="flex items-center gap-2">
					{name}
					{#if projects.all.find(p => p.id === id)?.branchedFromId}
						{@const originalProject = projects.getBranchedFromProject(id)}
						<Tooltip>
							{#snippet trigger(tooltip)}
								<div
									class="text-3xs grid aspect-square place-items-center rounded bg-blue-300 p-0.5 text-blue-700 dark:bg-blue-400/25 dark:text-blue-400"
									aria-label="Branched project"
									{...tooltip.trigger}
								>
									<ArrowSplitRounded />
								</div>
							{/snippet}
							Branched from {originalProject?.name || "unknown project"}
						</Tooltip>
					{:else if hasCheckpoints}
						<div
							class="text-3xs grid aspect-square place-items-center rounded bg-yellow-300 p-0.5 text-yellow-700 dark:bg-yellow-400/25 dark:text-yellow-400"
							aria-label="Project has checkpoints"
						>
							<IconHistory />
						</div>
					{/if}
				</div>
				{#if id !== "default"}
					<div class="ml-auto flex items-center gap-1">
						<button
							class="grid place-items-center rounded-md p-1 text-xs hover:bg-gray-300 dark:hover:bg-gray-600"
							onclick={async e => {
								e.stopPropagation();
								projects.update({ id, name: (await prompt("Edit project name", name)) || name });
							}}
						>
							<IconEdit />
						</button>
						<button
							class="grid place-items-center rounded-md p-1 text-xs hover:bg-gray-300 dark:hover:bg-gray-600"
							onclick={e => {
								e.stopPropagation();
								projects.delete(id);
							}}
						>
							<IconDelete />
						</button>
					</div>
				{/if}
			</div>
		</div>
	{/each}
</div>

<Dialog title="Set project name" open={sdState.open} onClose={() => (sdState.open = false)} onSubmit={saveDialog}>
	<label class="flex flex-col gap-2 font-medium text-gray-900 dark:text-white">
		<p>Project name</p>
		<input
			bind:value={sdState.name}
			placeholder={projectPlaceholder}
			type="text"
			class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400"
			{@attach autofocus()}
		/>
	</label>
	<label class="mt-4 flex gap-2 font-medium text-gray-900 dark:text-white">
		<input bind:checked={sdState.moveCheckpoints} type="checkbox" />
		<p>Move checkpoints over</p>
	</label>

	{#snippet footer()}
		<button
			type="submit"
			class="ml-auto rounded-lg bg-black px-5 py-2.5 text-sm font-medium text-white hover:bg-gray-900 focus:ring-4 focus:ring-gray-300 focus:outline-hidden dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700 dark:focus:ring-gray-700"
		>
			Submit
		</button>
	{/snippet}
</Dialog>