v0.4.0 f6d8405

taskbar.svelte RawHistoryFolder

1<script lang="ts">
2	import { onMount } from 'svelte';
3	import { desktop } from '$lib/states/desktop.svelte';
4	import { chatState } from '$lib/states/chat.svelte';
5	import { soundsEnabled, setSoundsEnabled, unlockAudio } from '$lib/aim/sounds.svelte';
6
7	let menuOpen = $state(false);
8	const soundOn = $derived(soundsEnabled());
9	let clock = $state('');
10	let menuElement = $state<HTMLElement>();
11	let startElement = $state<HTMLButtonElement>();
12	const user = $derived(chatState.getCurrentUser());
13
14	function updateClock() {
15		clock = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
16	}
17
18	onMount(() => {
19		updateClock();
20		const timer = setInterval(updateClock, 30_000);
21		function outside(event: PointerEvent) {
22			if (
23				menuOpen &&
24				!menuElement?.contains(event.target as Node) &&
25				!startElement?.contains(event.target as Node)
26			)
27				menuOpen = false;
28		}
29		function escape(event: KeyboardEvent) {
30			if (event.key === 'Escape') menuOpen = false;
31		}
32		document.addEventListener('pointerdown', outside);
33		document.addEventListener('keydown', escape);
34		return () => {
35			clearInterval(timer);
36			document.removeEventListener('pointerdown', outside);
37			document.removeEventListener('keydown', escape);
38		};
39	});
40
41	function choose(action: () => void) {
42		menuOpen = false;
43		action();
44	}
45
46	async function logout() {
47		const response = await fetch('/api/session/logout', { method: 'POST' });
48		if (response.ok) window.location.reload();
49	}
50</script>
51
52<div class="taskbar">
53	<button
54		class="start"
55		bind:this={startElement}
56		aria-expanded={menuOpen}
57		aria-controls="start-menu"
58		onclick={() => (menuOpen = !menuOpen)}><span class="start-label">démarrer</span></button
59	>
60	{#if menuOpen}
61		<div class="start-menu" id="start-menu" bind:this={menuElement}>
62			<div class="menu-header">{user?.nickname ?? 'Invité'}</div>
63			<button onclick={() => choose(() => desktop.openBuddyList())}
64				>PDR AIM — Liste de contacts</button
65			>
66			<button onclick={() => choose(() => desktop.openChatRoom(null, 'General'))}
67				>Salon General</button
68			>
69			<button onclick={() => choose(() => desktop.openNewRoom())}>Nouveau salon…</button>
70			<hr />
71			<button onclick={() => choose(() => desktop.openPreferences())}>Préférences</button>
72			<button onclick={() => choose(() => desktop.openAbout())}>À propos</button>
73			<hr />
74			{#if user}
75				<button
76					onclick={() => {
77						menuOpen = false;
78						void logout();
79					}}>Se déconnecter</button
80				>
81			{:else}
82				<button onclick={() => choose(() => desktop.openLogin('signin'))}>Se connecter</button>
83				<button onclick={() => choose(() => desktop.openLogin('signup'))}>S'inscrire</button>
84			{/if}
85		</div>
86	{/if}
87	<div class="tasks">
88		{#each desktop.taskbarWindows as w (w.id)}
89			<button
90				class="task"
91				class:pressed={desktop.focusedId === w.id}
92				class:minimized={w.minimized}
93				class:attention={w.attention}
94				title={w.title}
95				onclick={() => desktop.toggleFromTaskbar(w.id)}
96				><img src={w.icon} alt="" width="16" height="16" /><span>{w.title}</span></button
97			>
98		{/each}
99	</div>
100	<div class="tray">
101		<button
102			class="tray-button"
103			title={soundOn ? 'Désactiver les sons' : 'Activer les sons'}
104			aria-label={soundOn ? 'Désactiver les sons' : 'Activer les sons'}
105			onclick={() => {
106				unlockAudio();
107				setSoundsEnabled(!soundOn);
108			}}
109			><img
110				src={soundOn ? '/aim/xp-volume-16.png' : '/aim/xp-volume-muted-16.png'}
111				alt=""
112				width="16"
113				height="16"
114				class:muted={!soundOn}
115			/></button
116		>
117		<img
118			src="/aim/running-man-16.png"
119			alt=""
120			title={user ? `Statut : ${user.status}` : 'Hors ligne'}
121			width="16"
122			height="16"
123		/>
124		<time>{clock}</time>
125	</div>
126</div>
127
128<style>
129	.taskbar {
130		position: fixed;
131		bottom: 0;
132		left: 0;
133		right: 0;
134		height: 30px;
135		z-index: 5000;
136		display: flex;
137		align-items: stretch;
138		gap: 4px;
139		background: linear-gradient(
140			to bottom,
141			#1f2f86 0,
142			#3165c4 3%,
143			#3682e5 6%,
144			#4490e6 10%,
145			#3883e5 12%,
146			#2b71e0 15%,
147			#2663da 18%,
148			#235bd6 20%,
149			#2258d5 23%,
150			#2157d6 38%,
151			#245ddb 54%,
152			#2562df 86%,
153			#245fdc 89%,
154			#2158d4 92%,
155			#1d4ec0 95%,
156			#1941a5 98%
157		);
158		border-top: 0;
159		box-sizing: border-box;
160		color: white;
161		font:
162			11px Tahoma,
163			'Pixelated MS Sans Serif',
164			sans-serif;
165	}
166	/* Real XP "start" button: 3-slice of the original bitmap (logo | stretch | rounded cap) */
167	.start {
168		display: flex;
169		align-items: center;
170		border: 0;
171		border-radius: 0;
172		height: 30px;
173		padding: 0 17px 0 34px;
174		margin: 0;
175		background:
176			url('/desktop/start-left.png') left / auto 100% no-repeat,
177			url('/desktop/start-right.png') right / auto 100% no-repeat,
178			url('/desktop/start-mid.png') 34px 0 / calc(100% - 51px) 100% no-repeat;
179		color: white;
180		font:
181			bold italic 15px 'Franklin Gothic Medium',
182			'Trebuchet MS',
183			Tahoma,
184			sans-serif;
185		cursor: pointer;
186		text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.6);
187	}
188	.start:hover {
189		filter: brightness(1.08);
190	}
191	.start:active,
192	.start[aria-expanded='true'] {
193		filter: brightness(0.85);
194	}
195	.start-label {
196		line-height: 1;
197		padding-bottom: 2px;
198	}
199	.start-menu {
200		position: absolute;
201		bottom: 30px;
202		left: 0;
203		width: 250px;
204		background: white;
205		border: 2px solid #245edb;
206		box-shadow: 2px 2px 8px #3339;
207		color: #222;
208		padding-bottom: 4px;
209	}
210	.menu-header {
211		background: linear-gradient(#245edb, #4f91ef);
212		color: white;
213		font-weight: bold;
214		padding: 10px;
215	}
216	.start-menu button {
217		display: block;
218		width: 100%;
219		text-align: left;
220		background: transparent;
221		border: 0;
222		box-shadow: none;
223		border-radius: 0;
224		padding: 6px 12px;
225		font: inherit;
226		color: #222;
227		cursor: pointer;
228	}
229	.start-menu button:hover,
230	.start-menu button:focus-visible {
231		background: #316ac5;
232		color: white;
233	}
234	.start-menu hr {
235		border: 0;
236		border-top: 1px solid #ccc;
237		margin: 4px 8px;
238	}
239	.tasks {
240		display: flex;
241		gap: 3px;
242		align-items: center;
243		flex: 1;
244		overflow: hidden;
245		min-width: 0;
246	}
247	.task {
248		display: flex;
249		align-items: center;
250		gap: 4px;
251		width: 160px;
252		max-width: 160px;
253		min-width: 36px;
254		height: 24px;
255		flex-shrink: 1;
256		color: white;
257		background: #ffffff30;
258		border: 1px solid #ffffff44;
259		border-radius: 3px;
260		padding: 2px 6px;
261		font: inherit;
262		text-align: left;
263		cursor: pointer;
264	}
265	.task span {
266		overflow: hidden;
267		text-overflow: ellipsis;
268		white-space: nowrap;
269	}
270	.task img {
271		flex: 0 0 16px;
272	}
273	.task.pressed {
274		background: #103b96;
275		box-shadow: inset 1px 1px 3px #082a76;
276	}
277	.task.minimized {
278		background: #ffffff18;
279	}
280	.task.attention {
281		animation: flash 1s infinite alternate;
282	}
283	@keyframes flash {
284		from {
285			background: #e69a12;
286		}
287		to {
288			background: #ffdc55;
289			color: #222;
290		}
291	}
292	.tray {
293		display: flex;
294		align-items: center;
295		gap: 8px;
296		padding: 0 8px;
297		background: linear-gradient(#0c91e8, #1684d1);
298		border-left: 1px solid #78baff;
299		white-space: nowrap;
300	}
301	.tray-button img.muted {
302		opacity: 0.45;
303		filter: grayscale(1);
304	}
305
306	.tray-button {
307		min-width: 0;
308		padding: 0;
309		border: 0;
310		box-shadow: none;
311		background: none;
312		cursor: pointer;
313		font-size: 14px;
314	}
315	.tray time {
316		font: inherit;
317	}
318</style>

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close