detached d7110ac

feat(desktop): reuse the original running-man icon, XP-style desktop icons, drop legacy login window

commit
d7110acbeaace5019c051a9362c14f618c7a384e
author
Kit · · 2026-09-27 13:13 UTC
parent
a2662b9
signature
SSH UnverifiedSHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6Tk The key is unknown
19 files · +25 -651
D src/lib/components/aim-login.svelte +0 -626
@@ -1,626 +0,0 @@
1-<!-- AIM Login Component Updated for Svelte5 with Sign In/Sign Up Tabs -->
2-<script lang="ts">
3- import { draggable } from '$lib/actions/draggable';
4- import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5- import { invalidate } from '$app/navigation';
6- import { chatState } from '$lib/states/chat.svelte';
7- import { createSafeUser } from '$lib/types/chat';
8- import { onMount } from 'svelte';
9- import PasswordStrengthIndicator from './password-strength-indicator.svelte';
10- import {
11- loginSchema,
12- createRegistrationSchema,
13- DEFAULT_PASSWORD_CONSTRAINTS,
14- type PasswordConstraints
15- } from '../validation/password';
16- import { z } from 'zod';
17-
18- let {
19- showAuth = $bindable(),
20- onLoginSuccess,
21- activeTab = $bindable<'signin' | 'signup'>('signin')
22- } = $props<{
23- showAuth?: boolean;
24- onLoginSuccess?: () => void;
25- activeTab?: 'signin' | 'signup';
26- }>();
27-
28- // Sign In state
29- let siUsername = $state('');
30- let siPassword = $state('');
31- let error = $state('');
32- let loginStatus = $state('idle');
33-
34- // Sign Up state
35- let suUsername = $state('');
36- let suPassword = $state('');
37- let suConfirmPassword = $state('');
38- let captchaAnswer = $state('');
39- let signupError = $state('');
40- let signupStatus = $state('idle');
41- let suTurnstileToken = $state('');
42-
43- // Password validation state
44- let passwordConstraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS;
45- let showPasswordStrength = $state(false);
46-
47- // Added dragging state and handler similar to chat-room
48- let windowX = $state(0);
49- let windowY = $state(0);
50-
51- // Initialize window position on mount
52- onMount(() => {
53- if (window.innerWidth > 768) {
54- // Desktop mode
55- windowX = window.innerWidth * 0.2;
56- windowY = window.innerHeight * 0.2;
57- }
58- });
59-
60- function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {
61- windowX = event.detail.x;
62- windowY = event.detail.y;
63- }
64-
65- function handleClose() {
66- console.debug('Closing AIM Login component.');
67- showAuth = false;
68- }
69-
70- async function handleSigninSubmit() {
71- error = '';
72- loginStatus = 'loading';
73-
74- // Validate login data
75- const validationError = validateLoginData();
76- if (validationError) {
77- error = validationError;
78- loginStatus = 'error';
79- return;
80- }
81-
82- console.debug('Attempting login for', siUsername);
83- try {
84- const res = await fetch('/api/session/login', {
85- method: 'POST',
86- headers: { 'Content-Type': 'application/json' },
87- body: JSON.stringify({
88- username: siUsername,
89- password: siPassword
90- })
91- });
92-
93- const data = await res.json();
94- if (!res.ok) {
95- error = data.error || 'Login failed.';
96- console.debug('Login failed:', data.error);
97- loginStatus = 'error';
98- return;
99- }
100-
101- console.debug('Login succeeded:', {
102- ...data,
103- user: data.user ? { ...data.user, password: '[REDACTED]' } : null
104- });
105- await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
106- loginStatus = 'success';
107-
108- // Refresh session-dependent data
109- await invalidate('app:session');
110- await invalidate('app:chat');
111-
112- // Call the callback prop if provided
113- onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
114- // Auto close login window after 3 seconds on successful login
115- setTimeout(() => {
116- console.debug('Auto closing AIM Login component after successful login');
117- handleClose();
118- }, 3000);
119-
120- return;
121- } catch (err) {
122- console.debug('Login error:', err);
123- error = 'Login error. Please try again.';
124- loginStatus = 'error';
125- }
126- }
127-
128- // Validation using Zod schemas
129- function validateLoginData() {
130- try {
131- loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() });
132- return null;
133- } catch (err) {
134- if (err instanceof z.ZodError) {
135- return err.issues?.[0]?.message || 'Invalid input';
136- }
137- return 'Invalid input';
138- }
139- }
140-
141- function validateSignupData() {
142- try {
143- const registrationSchema = createRegistrationSchema(passwordConstraints);
144- registrationSchema.parse({
145- suUsername: suUsername.trim(),
146- suPassword: suPassword.trim(),
147- suConfirmPassword: suConfirmPassword.trim(),
148- captchaAnswer: captchaAnswer.trim(),
149- turnstileToken: suTurnstileToken
150- });
151- return null;
152- } catch (err) {
153- if (err instanceof z.ZodError) {
154- return err.issues?.[0]?.message || 'Invalid input';
155- }
156- return 'Invalid input';
157- }
158- }
159-
160- async function handleSignupSubmit() {
161- signupError = '';
162- signupStatus = 'loading';
163-
164- // Client-side validations before submitting the form.
165- const validationError = validateSignupData();
166- if (validationError) {
167- signupError = validationError;
168- signupStatus = 'error';
169- return;
170- }
171-
172- // Additional check for captcha
173- if (!captchaAnswer) {
174- signupError = 'Please complete all fields including the PDR question';
175- signupStatus = 'error';
176- return;
177- }
178-
179- console.debug('Attempting registration for', suUsername);
180- try {
181- const res = await fetch('/api/register', {
182- method: 'POST',
183- headers: { 'Content-Type': 'application/json' },
184- body: JSON.stringify({
185- suUsername,
186- suPassword,
187- suConfirmPassword,
188- captchaAnswer,
189- turnstileToken: suTurnstileToken
190- })
191- });
192-
193- const data = (await res.json()) as RegisterResponse;
194- if (!res.ok) {
195- const errorData = data as RegisterResponseError;
196- signupError = errorData.error || 'Registration failed.';
197- signupStatus = 'error';
198- return;
199- }
200- console.debug('Registration succeeded:', data);
201- signupStatus = 'success';
202-
203- // Auto login after registration.
204- await handleAutoLoginSignup();
205- return;
206- } catch (err) {
207- console.debug('Registration error', err);
208- signupError = 'Registration error. Please try again.';
209- signupStatus = 'error';
210- }
211- }
212-
213- async function handleAutoLoginSignup() {
214- try {
215- const res = await fetch('/api/session/login', {
216- method: 'POST',
217- headers: { 'Content-Type': 'application/json' },
218- body: JSON.stringify({ username: suUsername, password: suPassword })
219- });
220- const data = await res.json();
221- if (!res.ok) {
222- signupError = data.error || 'Login failed after registration.';
223- signupStatus = 'error';
224- return;
225- }
226- console.debug('Auto login after registration succeeded:', data);
227- await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
228- await invalidate('app:session');
229- await invalidate('app:chat');
230- onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
231- setTimeout(() => {
232- console.debug('Auto closing AIM Login component after auto login on registration');
233- handleClose();
234- }, 3000);
235- } catch (err) {
236- console.debug('Auto login error:', err);
237- signupError = 'Auto login error. Please try to login manually.';
238- signupStatus = 'error';
239- }
240- }
241-
242- function switchTab(tab: 'signin' | 'signup') {
243- activeTab = tab;
244- error = '';
245- signupError = '';
246- }
247-
248- function handleKeydown(event: KeyboardEvent) {
249- if (event.key === 'Escape') {
250- console.debug('Escape key pressed, closing AIM Login component.');
251- handleClose();
252- }
253- }
254-</script>
255-
256-<svelte:window onkeydown={handleKeydown} />
257-
258-<!-- Updated outer div to handle dragging with inline style and drag event -->
259-<div
260- class="login-window window"
261- use:draggable={{ handle: '.title-bar' }}
262- ondragmove={handleDragMove}
263- style="left: {windowX}px; top: {windowY}px;"
264->
265- <div class="title-bar">
266- <div class="title-bar-text">
267- <img
268- src="/desktop/aim-desktop-icon.png"
269- alt="AIM"
270- class="title-icon"
271- width="16"
272- height="16"
273- />
274- {activeTab === 'signin' ? 'Connexion' : 'Inscription'}
275- </div>
276- <div class="title-bar-controls">
277- <button aria-label="Close" onclick={handleClose}>×</button>
278- </div>
279- </div>
280-
281- <div class="tabs">
282- <button class:active={activeTab === 'signin'} onclick={() => switchTab('signin')}
283- >Connexion</button
284- >
285- <button class:active={activeTab === 'signup'} onclick={() => switchTab('signup')}
286- >Inscription</button
287- >
288- </div>
289-
290- <div class="window-content">
291- {#if activeTab === 'signin'}
292- <form onsubmit={handleSigninSubmit}>
293- <div class="form-group">
294- <label for="si-username">Nom d'utilisateur</label>
295- <input
296- type="text"
297- id="si-username"
298- bind:value={siUsername}
299- placeholder="Nom d'utilisateur"
300- />
301- </div>
302- <div class="form-group">
303- <label for="si-password">Mot de passe</label>
304- <input
305- type="password"
306- id="si-password"
307- bind:value={siPassword}
308- placeholder="Mot de passe"
309- />
310- </div>
311- {#if error}
312- <div class="error">{error}</div>
313- {/if}
314- <div class="button-bar">
315- <button
316- type="submit"
317- class="icon-button {loginStatus === 'success'
318- ? 'success-border'
319- : loginStatus === 'error'
320- ? 'error-border'
321- : ''}"
322- >
323- {#if loginStatus === 'loading'}
324- <svg width="24" height="24" viewBox="0 0 24 24">
325- <circle
326- cx="12"
327- cy="12"
328- r="10"
329- stroke="currentColor"
330- stroke-width="2"
331- fill="none"
332- opacity="0.25"
333- ></circle>
334- <path d="M12 2a10 10 0 0 1 0 20" stroke="currentColor" stroke-width="2" fill="none">
335- <animateTransform
336- attributeName="transform"
337- type="rotate"
338- values="0 12 12;360 12 12"
339- dur="1s"
340- repeatCount="indefinite"
341- />
342- </path>
343- </svg>
344- {:else if loginStatus === 'success'}
345- <svg width="24" height="24" viewBox="0 0 24 24">
346- <path
347- d="M20 6L9 17l-5-5"
348- stroke="green"
349- stroke-width="2"
350- fill="none"
351- stroke-linecap="round"
352- stroke-linejoin="round"
353- />
354- </svg>
355- {:else if loginStatus === 'error'}
356- <svg width="24" height="24" viewBox="0 0 24 24">
357- <circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="none" />
358- <line
359- x1="12"
360- y1="8"
361- x2="12"
362- y2="13"
363- stroke="red"
364- stroke-width="2"
365- stroke-linecap="round"
366- />
367- <circle cx="12" cy="17" r="1" fill="red" />
368- </svg>
369- {:else}
370- <svg
371- width="24"
372- height="24"
373- viewBox="0 0 24 24"
374- fill="none"
375- xmlns="http://www.w3.org/2000/svg"
376- >
377- <path
378- d="M12 4C9.24 4 7 6.24 7 9c0 2.76 2.24 5 5 5s5-2.24 5-5c0-2.76-2.24-5-5-5zm0 2c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 10c-3.33 0-7 1.67-7 5v1h14v-1c0-3.33-3.67-5-7-5zm0 2c2.67 0 5 1.33 5 3H7c0-1.67 2.33-3 5-3z"
379- fill="currentColor"
380- />
381- </svg>
382- {/if}
383- Se connecter
384- </button>
385- </div>
386- </form>
387- {:else}
388- <form onsubmit={handleSignupSubmit}>
389- <div class="form-group">
390- <label for="su-username">Nom d'utilisateur</label>
391- <input
392- type="text"
393- id="su-username"
394- bind:value={suUsername}
395- placeholder="Choisissez un nom d'utilisateur"
396- />
397- </div>
398- <div class="form-group">
399- <label for="su-password">Mot de passe</label>
400- <input
401- type="password"
402- id="su-password"
403- bind:value={suPassword}
404- placeholder="Mot de passe"
405- onfocus={() => (showPasswordStrength = true)}
406- />
407- {#if showPasswordStrength}
408- <PasswordStrengthIndicator
409- password={suPassword}
410- constraints={passwordConstraints}
411- showDetails={true}
412- />
413- {/if}
414- </div>
415- <div class="form-group">
416- <label for="su-confirm-password">Confirmer le mot de passe</label>
417- <input
418- type="password"
419- id="su-confirm-password"
420- bind:value={suConfirmPassword}
421- placeholder="Confirmer le mot de passe"
422- />
423- </div>
424- <div class="form-group">
425- <label for="captcha">Quelle est la signification de PDR ?</label>
426- <input
427- type="text"
428- id="captcha"
429- bind:value={captchaAnswer}
430- placeholder="Entrez votre réponse"
431- />
432- </div>
433- {#if signupError}
434- <div class="error">{signupError}</div>
435- {/if}
436- <div class="button-bar">
437- <button
438- type="submit"
439- class="icon-button {signupStatus === 'success'
440- ? 'success-border'
441- : signupStatus === 'error'
442- ? 'error-border'
443- : ''}"
444- >
445- {#if signupStatus === 'loading'}
446- <svg width="24" height="24" viewBox="0 0 24 24">
447- <circle
448- cx="12"
449- cy="12"
450- r="10"
451- stroke="currentColor"
452- stroke-width="2"
453- fill="none"
454- opacity="0.25"
455- ></circle>
456- <path d="M12 2a10 10 0 0 1 0 20" stroke="currentColor" stroke-width="2" fill="none">
457- <animateTransform
458- attributeName="transform"
459- type="rotate"
460- values="0 12 12;360 12 12"
461- dur="1s"
462- repeatCount="indefinite"
463- ></animateTransform>
464- </path>
465- </svg>
466- {:else if signupStatus === 'success'}
467- <svg width="24" height="24" viewBox="0 0 24 24">
468- <path
469- d="M20 6L9 17l-5-5"
470- stroke="green"
471- stroke-width="2"
472- fill="none"
473- stroke-linecap="round"
474- stroke-linejoin="round"
475- ></path>
476- </svg>
477- {:else if signupStatus === 'error'}
478- <svg width="24" height="24" viewBox="0 0 24 24">
479- <circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="none"></circle>
480- <line
481- x1="12"
482- y1="8"
483- x2="12"
484- y2="13"
485- stroke="red"
486- stroke-width="2"
487- stroke-linecap="round"
488- ></line>
489- <circle cx="12" cy="17" r="1" fill="red"></circle>
490- </svg>
491- {:else}
492- <svg
493- width="24"
494- height="24"
495- viewBox="0 0 24 24"
496- fill="none"
497- xmlns="http://www.w3.org/2000/svg"
498- >
499- <path
500- d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
501- fill="currentColor"
502- ></path>
503- </svg>
504- {/if}
505- S'inscrire
506- </button>
507- </div>
508- </form>
509- {/if}
510- </div>
511-</div>
512-
513-<style>
514- .login-window {
515- width: 300px;
516- background: #ece9d8;
517- /* Changed to fixed positioning for correct dragging */
518- position: fixed;
519- }
520-
521- @media (max-width: 768px) {
522- .login-window {
523- width: 100% !important;
524- height: 100% !important;
525- top: 0 !important;
526- left: 0 !important;
527- right: 0 !important;
528- bottom: 0 !important;
529- margin: 0 !important;
530- border-radius: 0 !important;
531- transform: none !important;
532- }
533-
534- .window-content {
535- height: calc(100% - 80px); /* Account for title bar and tabs */
536- overflow-y: auto;
537- }
538-
539- form {
540- max-width: 400px;
541- margin: 0 auto;
542- padding: 1rem;
543- }
544- }
545-
546- .title-bar {
547- background: linear-gradient(180deg, #0054e3 0%, #0047ab 100%);
548- padding: 4px 6px;
549- display: flex;
550- justify-content: space-between;
551- align-items: center;
552- color: white;
553- font-weight: bold;
554- user-select: none;
555- position: relative;
556- }
557- .title-bar-text {
558- display: flex;
559- align-items: center;
560- gap: 4px;
561- font-size: 0.875rem;
562- }
563-
564- .tabs {
565- padding: 0 3px;
566- display: flex;
567- border-bottom: 1px solid #0054e3;
568- }
569- .tabs button {
570- flex: 1;
571- padding: 8px;
572- border: none;
573- background: #ece9d8;
574- font-size: 1rem;
575- cursor: pointer;
576- }
577- .tabs button.active {
578- background: #fff;
579- border-bottom: 2px solid #0054e3;
580- font-weight: bold;
581- }
582- .window-content {
583- padding: 16px;
584- }
585- .form-group {
586- margin-bottom: 16px;
587- }
588- .form-group label {
589- display: block;
590- margin-bottom: 4px;
591- font-size: 1rem;
592- }
593- .form-group input {
594- width: 100%;
595- padding: 4px;
596- font-size: 1rem;
597- font-family: Arial, Verdana, Tahoma, sans-serif;
598- }
599- .button-bar {
600- display: flex;
601- justify-content: center;
602- margin-top: 20px;
603- }
604- .icon-button {
605- display: flex;
606- flex-direction: column;
607- align-items: center;
608- gap: 4px;
609- background: none;
610- border: none;
611- cursor: pointer;
612- font-size: 1rem;
613- color: black;
614- }
615- .error {
616- color: red;
617- font-size: 1rem;
618- margin-top: 8px;
619- }
620- .success-border {
621- border: 2px solid green;
622- }
623- .error-border {
624- border: 2px solid red;
625- }
626-</style>
@@ -1,626 +0,0 @@
1-<!-- AIM Login Component Updated for Svelte5 with Sign In/Sign Up Tabs -->
2-<script lang="ts">
3- import { draggable } from '$lib/actions/draggable';
4- import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5- import { invalidate } from '$app/navigation';
6- import { chatState } from '$lib/states/chat.svelte';
7- import { createSafeUser } from '$lib/types/chat';
8- import { onMount } from 'svelte';
9- import PasswordStrengthIndicator from './password-strength-indicator.svelte';
10- import {
11- loginSchema,
12- createRegistrationSchema,
13- DEFAULT_PASSWORD_CONSTRAINTS,
14- type PasswordConstraints
15- } from '../validation/password';
16- import { z } from 'zod';
17-
18- let {
19- showAuth = $bindable(),
20- onLoginSuccess,
21- activeTab = $bindable<'signin' | 'signup'>('signin')
22- } = $props<{
23- showAuth?: boolean;
24- onLoginSuccess?: () => void;
25- activeTab?: 'signin' | 'signup';
26- }>();
27-
28- // Sign In state
29- let siUsername = $state('');
30- let siPassword = $state('');
31- let error = $state('');
32- let loginStatus = $state('idle');
33-
34- // Sign Up state
35- let suUsername = $state('');
36- let suPassword = $state('');
37- let suConfirmPassword = $state('');
38- let captchaAnswer = $state('');
39- let signupError = $state('');
40- let signupStatus = $state('idle');
41- let suTurnstileToken = $state('');
42-
43- // Password validation state
44- let passwordConstraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS;
45- let showPasswordStrength = $state(false);
46-
47- // Added dragging state and handler similar to chat-room
48- let windowX = $state(0);
49- let windowY = $state(0);
50-
51- // Initialize window position on mount
52- onMount(() => {
53- if (window.innerWidth > 768) {
54- // Desktop mode
55- windowX = window.innerWidth * 0.2;
56- windowY = window.innerHeight * 0.2;
57- }
58- });
59-
60- function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {
61- windowX = event.detail.x;
62- windowY = event.detail.y;
63- }
64-
65- function handleClose() {
66- console.debug('Closing AIM Login component.');
67- showAuth = false;
68- }
69-
70- async function handleSigninSubmit() {
71- error = '';
72- loginStatus = 'loading';
73-
74- // Validate login data
75- const validationError = validateLoginData();
76- if (validationError) {
77- error = validationError;
78- loginStatus = 'error';
79- return;
80- }
81-
82- console.debug('Attempting login for', siUsername);
83- try {
84- const res = await fetch('/api/session/login', {
85- method: 'POST',
86- headers: { 'Content-Type': 'application/json' },
87- body: JSON.stringify({
88- username: siUsername,
89- password: siPassword
90- })
91- });
92-
93- const data = await res.json();
94- if (!res.ok) {
95- error = data.error || 'Login failed.';
96- console.debug('Login failed:', data.error);
97- loginStatus = 'error';
98- return;
99- }
100-
101- console.debug('Login succeeded:', {
102- ...data,
103- user: data.user ? { ...data.user, password: '[REDACTED]' } : null
104- });
105- await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
106- loginStatus = 'success';
107-
108- // Refresh session-dependent data
109- await invalidate('app:session');
110- await invalidate('app:chat');
111-
112- // Call the callback prop if provided
113- onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
114- // Auto close login window after 3 seconds on successful login
115- setTimeout(() => {
116- console.debug('Auto closing AIM Login component after successful login');
117- handleClose();
118- }, 3000);
119-
120- return;
121- } catch (err) {
122- console.debug('Login error:', err);
123- error = 'Login error. Please try again.';
124- loginStatus = 'error';
125- }
126- }
127-
128- // Validation using Zod schemas
129- function validateLoginData() {
130- try {
131- loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() });
132- return null;
133- } catch (err) {
134- if (err instanceof z.ZodError) {
135- return err.issues?.[0]?.message || 'Invalid input';
136- }
137- return 'Invalid input';
138- }
139- }
140-
141- function validateSignupData() {
142- try {
143- const registrationSchema = createRegistrationSchema(passwordConstraints);
144- registrationSchema.parse({
145- suUsername: suUsername.trim(),
146- suPassword: suPassword.trim(),
147- suConfirmPassword: suConfirmPassword.trim(),
148- captchaAnswer: captchaAnswer.trim(),
149- turnstileToken: suTurnstileToken
150- });
151- return null;
152- } catch (err) {
153- if (err instanceof z.ZodError) {
154- return err.issues?.[0]?.message || 'Invalid input';
155- }
156- return 'Invalid input';
157- }
158- }
159-
160- async function handleSignupSubmit() {
161- signupError = '';
162- signupStatus = 'loading';
163-
164- // Client-side validations before submitting the form.
165- const validationError = validateSignupData();
166- if (validationError) {
167- signupError = validationError;
168- signupStatus = 'error';
169- return;
170- }
171-
172- // Additional check for captcha
173- if (!captchaAnswer) {
174- signupError = 'Please complete all fields including the PDR question';
175- signupStatus = 'error';
176- return;
177- }
178-
179- console.debug('Attempting registration for', suUsername);
180- try {
181- const res = await fetch('/api/register', {
182- method: 'POST',
183- headers: { 'Content-Type': 'application/json' },
184- body: JSON.stringify({
185- suUsername,
186- suPassword,
187- suConfirmPassword,
188- captchaAnswer,
189- turnstileToken: suTurnstileToken
190- })
191- });
192-
193- const data = (await res.json()) as RegisterResponse;
194- if (!res.ok) {
195- const errorData = data as RegisterResponseError;
196- signupError = errorData.error || 'Registration failed.';
197- signupStatus = 'error';
198- return;
199- }
200- console.debug('Registration succeeded:', data);
201- signupStatus = 'success';
202-
203- // Auto login after registration.
204- await handleAutoLoginSignup();
205- return;
206- } catch (err) {
207- console.debug('Registration error', err);
208- signupError = 'Registration error. Please try again.';
209- signupStatus = 'error';
210- }
211- }
212-
213- async function handleAutoLoginSignup() {
214- try {
215- const res = await fetch('/api/session/login', {
216- method: 'POST',
217- headers: { 'Content-Type': 'application/json' },
218- body: JSON.stringify({ username: suUsername, password: suPassword })
219- });
220- const data = await res.json();
221- if (!res.ok) {
222- signupError = data.error || 'Login failed after registration.';
223- signupStatus = 'error';
224- return;
225- }
226- console.debug('Auto login after registration succeeded:', data);
227- await chatState.setCurrentUser(data.user ? createSafeUser(data.user) : null);
228- await invalidate('app:session');
229- await invalidate('app:chat');
230- onLoginSuccess?.(data.user ? createSafeUser(data.user) : null);
231- setTimeout(() => {
232- console.debug('Auto closing AIM Login component after auto login on registration');
233- handleClose();
234- }, 3000);
235- } catch (err) {
236- console.debug('Auto login error:', err);
237- signupError = 'Auto login error. Please try to login manually.';
238- signupStatus = 'error';
239- }
240- }
241-
242- function switchTab(tab: 'signin' | 'signup') {
243- activeTab = tab;
244- error = '';
245- signupError = '';
246- }
247-
248- function handleKeydown(event: KeyboardEvent) {
249- if (event.key === 'Escape') {
250- console.debug('Escape key pressed, closing AIM Login component.');
251- handleClose();
252- }
253- }
254-</script>
255-
256-<svelte:window onkeydown={handleKeydown} />
257-
258-<!-- Updated outer div to handle dragging with inline style and drag event -->
259-<div
260- class="login-window window"
261- use:draggable={{ handle: '.title-bar' }}
262- ondragmove={handleDragMove}
263- style="left: {windowX}px; top: {windowY}px;"
264->
265- <div class="title-bar">
266- <div class="title-bar-text">
267- <img
268- src="/desktop/aim-desktop-icon.png"
269- alt="AIM"
270- class="title-icon"
271- width="16"
272- height="16"
273- />
274- {activeTab === 'signin' ? 'Connexion' : 'Inscription'}
275- </div>
276- <div class="title-bar-controls">
277- <button aria-label="Close" onclick={handleClose}>×</button>
278- </div>
279- </div>
280-
281- <div class="tabs">
282- <button class:active={activeTab === 'signin'} onclick={() => switchTab('signin')}
283- >Connexion</button
284- >
285- <button class:active={activeTab === 'signup'} onclick={() => switchTab('signup')}
286- >Inscription</button
287- >
288- </div>
289-
290- <div class="window-content">
291- {#if activeTab === 'signin'}
292- <form onsubmit={handleSigninSubmit}>
293- <div class="form-group">
294- <label for="si-username">Nom d'utilisateur</label>
295- <input
296- type="text"
297- id="si-username"
298- bind:value={siUsername}
299- placeholder="Nom d'utilisateur"
300- />
301- </div>
302- <div class="form-group">
303- <label for="si-password">Mot de passe</label>
304- <input
305- type="password"
306- id="si-password"
307- bind:value={siPassword}
308- placeholder="Mot de passe"
309- />
310- </div>
311- {#if error}
312- <div class="error">{error}</div>
313- {/if}
314- <div class="button-bar">
315- <button
316- type="submit"
317- class="icon-button {loginStatus === 'success'
318- ? 'success-border'
319- : loginStatus === 'error'
320- ? 'error-border'
321- : ''}"
322- >
323- {#if loginStatus === 'loading'}
324- <svg width="24" height="24" viewBox="0 0 24 24">
325- <circle
326- cx="12"
327- cy="12"
328- r="10"
329- stroke="currentColor"
330- stroke-width="2"
331- fill="none"
332- opacity="0.25"
333- ></circle>
334- <path d="M12 2a10 10 0 0 1 0 20" stroke="currentColor" stroke-width="2" fill="none">
335- <animateTransform
336- attributeName="transform"
337- type="rotate"
338- values="0 12 12;360 12 12"
339- dur="1s"
340- repeatCount="indefinite"
341- />
342- </path>
343- </svg>
344- {:else if loginStatus === 'success'}
345- <svg width="24" height="24" viewBox="0 0 24 24">
346- <path
347- d="M20 6L9 17l-5-5"
348- stroke="green"
349- stroke-width="2"
350- fill="none"
351- stroke-linecap="round"
352- stroke-linejoin="round"
353- />
354- </svg>
355- {:else if loginStatus === 'error'}
356- <svg width="24" height="24" viewBox="0 0 24 24">
357- <circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="none" />
358- <line
359- x1="12"
360- y1="8"
361- x2="12"
362- y2="13"
363- stroke="red"
364- stroke-width="2"
365- stroke-linecap="round"
366- />
367- <circle cx="12" cy="17" r="1" fill="red" />
368- </svg>
369- {:else}
370- <svg
371- width="24"
372- height="24"
373- viewBox="0 0 24 24"
374- fill="none"
375- xmlns="http://www.w3.org/2000/svg"
376- >
377- <path
378- d="M12 4C9.24 4 7 6.24 7 9c0 2.76 2.24 5 5 5s5-2.24 5-5c0-2.76-2.24-5-5-5zm0 2c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 10c-3.33 0-7 1.67-7 5v1h14v-1c0-3.33-3.67-5-7-5zm0 2c2.67 0 5 1.33 5 3H7c0-1.67 2.33-3 5-3z"
379- fill="currentColor"
380- />
381- </svg>
382- {/if}
383- Se connecter
384- </button>
385- </div>
386- </form>
387- {:else}
388- <form onsubmit={handleSignupSubmit}>
389- <div class="form-group">
390- <label for="su-username">Nom d'utilisateur</label>
391- <input
392- type="text"
393- id="su-username"
394- bind:value={suUsername}
395- placeholder="Choisissez un nom d'utilisateur"
396- />
397- </div>
398- <div class="form-group">
399- <label for="su-password">Mot de passe</label>
400- <input
401- type="password"
402- id="su-password"
403- bind:value={suPassword}
404- placeholder="Mot de passe"
405- onfocus={() => (showPasswordStrength = true)}
406- />
407- {#if showPasswordStrength}
408- <PasswordStrengthIndicator
409- password={suPassword}
410- constraints={passwordConstraints}
411- showDetails={true}
412- />
413- {/if}
414- </div>
415- <div class="form-group">
416- <label for="su-confirm-password">Confirmer le mot de passe</label>
417- <input
418- type="password"
419- id="su-confirm-password"
420- bind:value={suConfirmPassword}
421- placeholder="Confirmer le mot de passe"
422- />
423- </div>
424- <div class="form-group">
425- <label for="captcha">Quelle est la signification de PDR ?</label>
426- <input
427- type="text"
428- id="captcha"
429- bind:value={captchaAnswer}
430- placeholder="Entrez votre réponse"
431- />
432- </div>
433- {#if signupError}
434- <div class="error">{signupError}</div>
435- {/if}
436- <div class="button-bar">
437- <button
438- type="submit"
439- class="icon-button {signupStatus === 'success'
440- ? 'success-border'
441- : signupStatus === 'error'
442- ? 'error-border'
443- : ''}"
444- >
445- {#if signupStatus === 'loading'}
446- <svg width="24" height="24" viewBox="0 0 24 24">
447- <circle
448- cx="12"
449- cy="12"
450- r="10"
451- stroke="currentColor"
452- stroke-width="2"
453- fill="none"
454- opacity="0.25"
455- ></circle>
456- <path d="M12 2a10 10 0 0 1 0 20" stroke="currentColor" stroke-width="2" fill="none">
457- <animateTransform
458- attributeName="transform"
459- type="rotate"
460- values="0 12 12;360 12 12"
461- dur="1s"
462- repeatCount="indefinite"
463- ></animateTransform>
464- </path>
465- </svg>
466- {:else if signupStatus === 'success'}
467- <svg width="24" height="24" viewBox="0 0 24 24">
468- <path
469- d="M20 6L9 17l-5-5"
470- stroke="green"
471- stroke-width="2"
472- fill="none"
473- stroke-linecap="round"
474- stroke-linejoin="round"
475- ></path>
476- </svg>
477- {:else if signupStatus === 'error'}
478- <svg width="24" height="24" viewBox="0 0 24 24">
479- <circle cx="12" cy="12" r="10" stroke="red" stroke-width="2" fill="none"></circle>
480- <line
481- x1="12"
482- y1="8"
483- x2="12"
484- y2="13"
485- stroke="red"
486- stroke-width="2"
487- stroke-linecap="round"
488- ></line>
489- <circle cx="12" cy="17" r="1" fill="red"></circle>
490- </svg>
491- {:else}
492- <svg
493- width="24"
494- height="24"
495- viewBox="0 0 24 24"
496- fill="none"
497- xmlns="http://www.w3.org/2000/svg"
498- >
499- <path
500- d="M15 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm-9-2V7H4v3H1v2h3v3h2v-3h3v-2H6zm9 4c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
501- fill="currentColor"
502- ></path>
503- </svg>
504- {/if}
505- S'inscrire
506- </button>
507- </div>
508- </form>
509- {/if}
510- </div>
511-</div>
512-
513-<style>
514- .login-window {
515- width: 300px;
516- background: #ece9d8;
517- /* Changed to fixed positioning for correct dragging */
518- position: fixed;
519- }
520-
521- @media (max-width: 768px) {
522- .login-window {
523- width: 100% !important;
524- height: 100% !important;
525- top: 0 !important;
526- left: 0 !important;
527- right: 0 !important;
528- bottom: 0 !important;
529- margin: 0 !important;
530- border-radius: 0 !important;
531- transform: none !important;
532- }
533-
534- .window-content {
535- height: calc(100% - 80px); /* Account for title bar and tabs */
536- overflow-y: auto;
537- }
538-
539- form {
540- max-width: 400px;
541- margin: 0 auto;
542- padding: 1rem;
543- }
544- }
545-
546- .title-bar {
547- background: linear-gradient(180deg, #0054e3 0%, #0047ab 100%);
548- padding: 4px 6px;
549- display: flex;
550- justify-content: space-between;
551- align-items: center;
552- color: white;
553- font-weight: bold;
554- user-select: none;
555- position: relative;
556- }
557- .title-bar-text {
558- display: flex;
559- align-items: center;
560- gap: 4px;
561- font-size: 0.875rem;
562- }
563-
564- .tabs {
565- padding: 0 3px;
566- display: flex;
567- border-bottom: 1px solid #0054e3;
568- }
569- .tabs button {
570- flex: 1;
571- padding: 8px;
572- border: none;
573- background: #ece9d8;
574- font-size: 1rem;
575- cursor: pointer;
576- }
577- .tabs button.active {
578- background: #fff;
579- border-bottom: 2px solid #0054e3;
580- font-weight: bold;
581- }
582- .window-content {
583- padding: 16px;
584- }
585- .form-group {
586- margin-bottom: 16px;
587- }
588- .form-group label {
589- display: block;
590- margin-bottom: 4px;
591- font-size: 1rem;
592- }
593- .form-group input {
594- width: 100%;
595- padding: 4px;
596- font-size: 1rem;
597- font-family: Arial, Verdana, Tahoma, sans-serif;
598- }
599- .button-bar {
600- display: flex;
601- justify-content: center;
602- margin-top: 20px;
603- }
604- .icon-button {
605- display: flex;
606- flex-direction: column;
607- align-items: center;
608- gap: 4px;
609- background: none;
610- border: none;
611- cursor: pointer;
612- font-size: 1rem;
613- color: black;
614- }
615- .error {
616- color: red;
617- font-size: 1rem;
618- margin-top: 8px;
619- }
620- .success-border {
621- border: 2px solid green;
622- }
623- .error-border {
624- border: 2px solid red;
625- }
626-</style>
M src/lib/components/desktop-icons.svelte +5 -10
@@ -16,7 +16,9 @@
1616 {
1717 id: 'aim',
1818 label: 'PDR AIM',
19- icon: '/desktop/aim-desktop-icon.png',
19+ icon: '/desktop/pdraim-icon.png',
20+ // Signed out: the sign-on window (which opens the buddy list and unlocks the
21+ // chat room once logged in). Signed in: the buddy list.
2022 action: () => (currentUser ? desktop.openBuddyList() : desktop.openLogin('signin'))
2123 },
2224 {
@@ -25,13 +27,6 @@
2527 icon: '/aim/chat-room.svg',
2628 action: () => desktop.openChatRoom(null, 'General')
2729 },
28- {
29- id: 'login',
30- label: 'Connexion',
31- icon: '/desktop/application-x-shellscript.png',
32- action: () => desktop.openLogin('signin'),
33- hidden: Boolean(currentUser)
34- },
3530 {
3631 id: 'away',
3732 label: 'Message d’absence',
@@ -42,13 +37,13 @@
4237 {
4338 id: 'prefs',
4439 label: 'Préférences',
45- icon: '/aim/setup-icon.png',
40+ icon: '/aim/prefs.svg',
4641 action: () => desktop.openPreferences()
4742 },
4843 {
4944 id: 'about',
5045 label: 'Aide',
51- icon: '/aim/help-icon.png',
46+ icon: '/aim/help.svg',
5247 action: () => desktop.openAbout()
5348 }
5449 ]);
@@ -16,7 +16,9 @@
16 {16 {
17 id: 'aim',17 id: 'aim',
18 label: 'PDR AIM',18 label: 'PDR AIM',
19- icon: '/desktop/aim-desktop-icon.png',19+ icon: '/desktop/pdraim-icon.png',
20+ // Signed out: the sign-on window (which opens the buddy list and unlocks the
21+ // chat room once logged in). Signed in: the buddy list.
20 action: () => (currentUser ? desktop.openBuddyList() : desktop.openLogin('signin'))22 action: () => (currentUser ? desktop.openBuddyList() : desktop.openLogin('signin'))
21 },23 },
22 {24 {
@@ -25,13 +27,6 @@
25 icon: '/aim/chat-room.svg',27 icon: '/aim/chat-room.svg',
26 action: () => desktop.openChatRoom(null, 'General')28 action: () => desktop.openChatRoom(null, 'General')
27 },29 },
28- {
29- id: 'login',
30- label: 'Connexion',
31- icon: '/desktop/application-x-shellscript.png',
32- action: () => desktop.openLogin('signin'),
33- hidden: Boolean(currentUser)
34- },
35 {30 {
36 id: 'away',31 id: 'away',
37 label: 'Message d’absence',32 label: 'Message d’absence',
@@ -42,13 +37,13 @@
42 {37 {
43 id: 'prefs',38 id: 'prefs',
44 label: 'Préférences',39 label: 'Préférences',
45- icon: '/aim/setup-icon.png',40+ icon: '/aim/prefs.svg',
46 action: () => desktop.openPreferences()41 action: () => desktop.openPreferences()
47 },42 },
48 {43 {
49 id: 'about',44 id: 'about',
50 label: 'Aide',45 label: 'Aide',
51- icon: '/aim/help-icon.png',46+ icon: '/aim/help.svg',
52 action: () => desktop.openAbout()47 action: () => desktop.openAbout()
53 }48 }
54 ]);49 ]);
M src/lib/components/desktop/about-window.svelte +1 -1
@@ -5,7 +5,7 @@
55
66 <XpWindow id="about" width={360} height={260} resizableWindow={false}>
77 <div class="content">
8- <img src="/aim/running-man.svg" alt="" width="48" height="48" />
8+ <img src="/aim/running-man-48.png" alt="" width="48" height="48" />
99 <h2>PDR AIM</h2>
1010 <p>v0.3.0</p>
1111 <p>
@@ -5,7 +5,7 @@
5 5
6 <XpWindow id="about" width={360} height={260} resizableWindow={false}>6 <XpWindow id="about" width={360} height={260} resizableWindow={false}>
7 <div class="content">7 <div class="content">
8- <img src="/aim/running-man.svg" alt="" width="48" height="48" />8+ <img src="/aim/running-man-48.png" alt="" width="48" height="48" />
9 <h2>PDR AIM</h2>9 <h2>PDR AIM</h2>
10 <p>v0.3.0</p>10 <p>v0.3.0</p>
11 <p>11 <p>
M src/lib/components/desktop/buddy-list.svelte +1 -1
@@ -311,7 +311,7 @@
311311 </div>
312312 <div class="identity-text">
313313 <strong
314- ><img src="/aim/running-man.svg" alt="" width="16" height="16" /> {me.nickname}</strong
314+ ><img src="/aim/running-man-16.png" alt="" width="16" height="16" /> {me.nickname}</strong
315315 ><span title={meQuery?.data?.awayMessage ?? ''}
316316 >{meQuery?.data?.status === 'away'
317317 ? `Absent${meQuery.data.awayMessage ? ` : ${meQuery.data.awayMessage}` : ''}`
@@ -311,7 +311,7 @@
311 </div>311 </div>
312 <div class="identity-text">312 <div class="identity-text">
313 <strong313 <strong
314- ><img src="/aim/running-man.svg" alt="" width="16" height="16" /> {me.nickname}</strong314+ ><img src="/aim/running-man-16.png" alt="" width="16" height="16" /> {me.nickname}</strong
315 ><span title={meQuery?.data?.awayMessage ?? ''}315 ><span title={meQuery?.data?.awayMessage ?? ''}
316 >{meQuery?.data?.status === 'away'316 >{meQuery?.data?.status === 'away'
317 ? `Absent${meQuery.data.awayMessage ? ` : ${meQuery.data.awayMessage}` : ''}`317 ? `Absent${meQuery.data.awayMessage ? ` : ${meQuery.data.awayMessage}` : ''}`
M src/lib/components/desktop/sign-on-window.svelte +1 -1
@@ -181,7 +181,7 @@
181181 >
182182 <div class="signon">
183183 <div class="banner">
184- <img src="/aim/running-man.svg" alt="" width="40" height="40" />
184+ <img src="/aim/running-man-48.png" alt="" width="40" height="40" />
185185 <div class="brand">
186186 <div class="brand-title">PDR AIM</div>
187187 <div class="brand-sub">Instant Messenger</div>
@@ -181,7 +181,7 @@
181 >181 >
182 <div class="signon">182 <div class="signon">
183 <div class="banner">183 <div class="banner">
184- <img src="/aim/running-man.svg" alt="" width="40" height="40" />184+ <img src="/aim/running-man-48.png" alt="" width="40" height="40" />
185 <div class="brand">185 <div class="brand">
186 <div class="brand-title">PDR AIM</div>186 <div class="brand-title">PDR AIM</div>
187 <div class="brand-sub">Instant Messenger</div>187 <div class="brand-sub">Instant Messenger</div>
M src/lib/components/desktop/taskbar.svelte +1 -1
@@ -111,7 +111,7 @@
111111 }}><span aria-hidden="true">{soundOn ? '🔊' : '🔇'}</span></button
112112 >
113113 <img
114- src="/aim/running-man.svg"
114+ src="/aim/running-man-16.png"
115115 alt=""
116116 title={user ? `Statut : ${user.status}` : 'Hors ligne'}
117117 width="16"
@@ -111,7 +111,7 @@
111 }}><span aria-hidden="true">{soundOn ? '🔊' : '🔇'}</span></button111 }}><span aria-hidden="true">{soundOn ? '🔊' : '🔇'}</span></button
112 >112 >
113 <img113 <img
114- src="/aim/running-man.svg"114+ src="/aim/running-man-16.png"
115 alt=""115 alt=""
116 title={user ? `Statut : ${user.status}` : 'Hors ligne'}116 title={user ? `Statut : ${user.status}` : 'Hors ligne'}
117 width="16"117 width="16"
M src/lib/states/desktop.svelte.ts +4 -4
@@ -142,7 +142,7 @@ class DesktopState {
142142 id: 'buddy-list',
143143 kind: 'buddy-list',
144144 title: 'Liste de contacts',
145- icon: '/aim/running-man.svg',
145+ icon: '/aim/running-man-16.png',
146146 props: {}
147147 });
148148 }
@@ -173,7 +173,7 @@ class DesktopState {
173173 id: 'login',
174174 kind: 'login',
175175 title: 'Connexion',
176- icon: '/aim/running-man.svg',
176+ icon: '/aim/running-man-16.png',
177177 props: { tab }
178178 });
179179 }
@@ -213,7 +213,7 @@ class DesktopState {
213213 id: 'preferences',
214214 kind: 'preferences',
215215 title: 'Préférences',
216- icon: '/aim/setup-icon.png',
216+ icon: '/aim/prefs.svg',
217217 props: {}
218218 });
219219 }
@@ -223,7 +223,7 @@ class DesktopState {
223223 id: 'about',
224224 kind: 'about',
225225 title: 'À propos de PDR AIM',
226- icon: '/aim/help-icon.png',
226+ icon: '/aim/help.svg',
227227 props: {}
228228 });
229229 }
@@ -142,7 +142,7 @@ class DesktopState {
142 id: 'buddy-list',142 id: 'buddy-list',
143 kind: 'buddy-list',143 kind: 'buddy-list',
144 title: 'Liste de contacts',144 title: 'Liste de contacts',
145- icon: '/aim/running-man.svg',145+ icon: '/aim/running-man-16.png',
146 props: {}146 props: {}
147 });147 });
148 }148 }
@@ -173,7 +173,7 @@ class DesktopState {
173 id: 'login',173 id: 'login',
174 kind: 'login',174 kind: 'login',
175 title: 'Connexion',175 title: 'Connexion',
176- icon: '/aim/running-man.svg',176+ icon: '/aim/running-man-16.png',
177 props: { tab }177 props: { tab }
178 });178 });
179 }179 }
@@ -213,7 +213,7 @@ class DesktopState {
213 id: 'preferences',213 id: 'preferences',
214 kind: 'preferences',214 kind: 'preferences',
215 title: 'Préférences',215 title: 'Préférences',
216- icon: '/aim/setup-icon.png',216+ icon: '/aim/prefs.svg',
217 props: {}217 props: {}
218 });218 });
219 }219 }
@@ -223,7 +223,7 @@ class DesktopState {
223 id: 'about',223 id: 'about',
224 kind: 'about',224 kind: 'about',
225 title: 'À propos de PDR AIM',225 title: 'À propos de PDR AIM',
226- icon: '/aim/help-icon.png',226+ icon: '/aim/help.svg',
227 props: {}227 props: {}
228 });228 });
229 }229 }
D static/aim/help-icon.png +0 -0 binary file: no text diff

binary file: no text diff

A static/aim/help.svg +5 -0
@@ -0,0 +1,5 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2+ <defs><radialGradient id="b" cx="0.35" cy="0.3" r="0.8"><stop offset="0" stop-color="#8fc2ff"/><stop offset="1" stop-color="#1d4fa8"/></radialGradient></defs>
3+ <circle cx="16" cy="16" r="14" fill="url(#b)" stroke="#0f3d9a" stroke-width="1.2"/>
4+ <text x="16" y="23" text-anchor="middle" font-family="Tahoma,Arial,sans-serif" font-weight="bold" font-size="20" fill="#fff">?</text>
5+</svg>
@@ -0,0 +1,5 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2+ <defs><radialGradient id="b" cx="0.35" cy="0.3" r="0.8"><stop offset="0" stop-color="#8fc2ff"/><stop offset="1" stop-color="#1d4fa8"/></radialGradient></defs>
3+ <circle cx="16" cy="16" r="14" fill="url(#b)" stroke="#0f3d9a" stroke-width="1.2"/>
4+ <text x="16" y="23" text-anchor="middle" font-family="Tahoma,Arial,sans-serif" font-weight="bold" font-size="20" fill="#fff">?</text>
5+</svg>
A static/aim/prefs.svg +7 -0
@@ -0,0 +1,7 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2+ <defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e8e8e8"/><stop offset="1" stop-color="#8d8d8d"/></linearGradient></defs>
3+ <g transform="translate(16 16)" fill="url(#g)" stroke="#3a3a3a" stroke-width="1.2">
4+ <path d="M-3 -14h6l1 3.5 3 1.3 3.2-1.8 4.2 4.2-1.8 3.2 1.3 3 3.5 1v6l-3.5 1-1.3 3 1.8 3.2-4.2 4.2-3.2-1.8-3 1.3-1 3.5h-6l-1-3.5-3-1.3-3.2 1.8-4.2-4.2 1.8-3.2-1.3-3-3.5-1v-6l3.5-1 1.3-3-1.8-3.2 4.2-4.2 3.2 1.8 3-1.3z"/>
5+ <circle r="4.5" fill="#3b7ddd"/>
6+ </g>
7+</svg>
@@ -0,0 +1,7 @@
1+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2+ <defs><linearGradient id="g" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e8e8e8"/><stop offset="1" stop-color="#8d8d8d"/></linearGradient></defs>
3+ <g transform="translate(16 16)" fill="url(#g)" stroke="#3a3a3a" stroke-width="1.2">
4+ <path d="M-3 -14h6l1 3.5 3 1.3 3.2-1.8 4.2 4.2-1.8 3.2 1.3 3 3.5 1v6l-3.5 1-1.3 3 1.8 3.2-4.2 4.2-3.2-1.8-3 1.3-1 3.5h-6l-1-3.5-3-1.3-3.2 1.8-4.2-4.2 1.8-3.2-1.3-3-3.5-1v-6l3.5-1 1.3-3-1.8-3.2 4.2-4.2 3.2 1.8 3-1.3z"/>
5+ <circle r="4.5" fill="#3b7ddd"/>
6+ </g>
7+</svg>
A static/aim/running-man-16.png +0 -0 binary file: no text diff

binary file: no text diff

A static/aim/running-man-32.png +0 -0 binary file: no text diff

binary file: no text diff

A static/aim/running-man-48.png +0 -0 binary file: no text diff

binary file: no text diff

D static/aim/running-man.svg +0 -7
@@ -1,7 +0,0 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2- <circle cx="16" cy="16" r="15" fill="#f6c700" stroke="#c99a00" stroke-width="1"/>
3- <g fill="#1b1b1b">
4- <circle cx="19" cy="8" r="3"/>
5- <path d="M14 12 l6 1 l3 5 l-2 1 l-2.5 -3 l-1 4 l4 4 l-1.5 5 l-2.5 -1 l1 -3.5 l-4 -3 l-3 5 l-2.5 -1.5 l3.5 -6 l1.5 -4 l-4 1 l-1 -2.5z"/>
6- </g>
7-</svg>
@@ -1,7 +0,0 @@
1-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32">
2- <circle cx="16" cy="16" r="15" fill="#f6c700" stroke="#c99a00" stroke-width="1"/>
3- <g fill="#1b1b1b">
4- <circle cx="19" cy="8" r="3"/>
5- <path d="M14 12 l6 1 l3 5 l-2 1 l-2.5 -3 l-1 4 l4 4 l-1.5 5 l-2.5 -1 l1 -3.5 l-4 -3 l-3 5 l-2.5 -1.5 l3.5 -6 l1.5 -4 l-4 1 l-1 -2.5z"/>
6- </g>
7-</svg>
D static/aim/setup-icon.png +0 -0 binary file: no text diff

binary file: no text diff

D static/aim/signin-icon.png +0 -0 binary file: no text diff

binary file: no text diff

D static/desktop/aim-desktop-icon.png +0 -0 binary file: no text diff

binary file: no text diff

D static/desktop/application-x-shellscript.png +0 -0 binary file: no text diff

binary file: no text diff

A static/desktop/pdraim-icon.png +0 -0 binary file: no text diff

binary file: no text diff

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