user-preferences-modal.svelte RawHistoryFolder

1<script lang="ts">
2	import { onMount } from 'svelte';
3	import TextStyleSelector from './text-style-selector.svelte';
4	import {
5		DEFAULT_USER_PREFERENCES,
6		DEFAULT_TEXT_STYLE,
7		type UserTextPreferences,
8		type TextStyle
9	} from '../types/text-formatting';
10
11	// Props
12	let { showModal = $bindable(false), onSave } = $props<{
13		showModal: boolean;
14		onSave?: (preferences: UserTextPreferences) => void;
15	}>();
16
17	// State
18	let preferences = $state<UserTextPreferences>({ ...DEFAULT_USER_PREFERENCES });
19	let currentStyle = $state<TextStyle>({ ...DEFAULT_TEXT_STYLE });
20	let isSaving = $state(false);
21	let hasChanges = $derived(
22		JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES)
23	);
24
25	// Update preferences when style changes
26	$effect(() => {
27		preferences.defaultStyle = { ...currentStyle };
28	});
29
30	// Load user preferences on mount
31	onMount(async () => {
32		try {
33			const response = await fetch('/api/user/text-preferences');
34			if (response.ok) {
35				const data = await response.json();
36				if (data.success && data.preferences) {
37					preferences = { ...DEFAULT_USER_PREFERENCES, ...data.preferences };
38					currentStyle = { ...DEFAULT_TEXT_STYLE, ...preferences.defaultStyle };
39				}
40			}
41		} catch (error) {
42			console.error('Failed to load user preferences:', error);
43		}
44	});
45
46	// Save preferences
47	async function savePreferences() {
48		isSaving = true;
49		try {
50			const response = await fetch('/api/user/text-preferences', {
51				method: 'POST',
52				headers: { 'Content-Type': 'application/json' },
53				body: JSON.stringify(preferences)
54			});
55
56			if (!response.ok) {
57				throw new Error('Failed to save preferences');
58			}
59
60			await onSave?.(preferences);
61			showModal = false;
62		} catch (error) {
63			console.error('Failed to save preferences:', error);
64			alert('Failed to save preferences. Please try again.');
65		} finally {
66			isSaving = false;
67		}
68	}
69
70	// Reset to defaults
71	function resetToDefaults() {
72		preferences = { ...DEFAULT_USER_PREFERENCES };
73		currentStyle = { ...DEFAULT_TEXT_STYLE };
74	}
75
76	// Cancel changes
77	function cancel() {
78		showModal = false;
79	}
80
81	// Handle keyboard shortcuts
82	function handleKeydown(event: KeyboardEvent) {
83		if (event.key === 'Escape') {
84			cancel();
85		} else if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
86			savePreferences();
87		}
88	}
89</script>
90
91{#if showModal}
92	<!-- Modal backdrop -->
93	<div
94		class="modal-backdrop"
95		onclick={cancel}
96		onkeydown={handleKeydown}
97		role="dialog"
98		aria-modal="true"
99		aria-labelledby="preferences-title"
100		tabindex="-1"
101	>
102		<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
103		<div class="modal-window window" onclick={(e) => e.stopPropagation()}>
104			<div class="title-bar">
105				<div class="title-bar-text" id="preferences-title">Text Formatting Preferences</div>
106				<div class="title-bar-controls">
107					<button onclick={cancel} aria-label="Close"></button>
108				</div>
109			</div>
110
111			<div class="window-body">
112				<div class="preferences-content">
113					<!-- Enable/Disable Formatting -->
114					<div class="preference-group">
115						<label class="checkbox-label">
116							<input type="checkbox" bind:checked={preferences.allowFormatting} />
117							Enable text formatting in messages
118						</label>
119						<small class="help-text">
120							When enabled, you can use formatting options like bold, italic, colors, and gradients.
121						</small>
122					</div>
123
124					<!-- Default Text Style -->
125					<div class="preference-group">
126						<h3>Default Text Style</h3>
127						<p class="description">
128							These settings will be used as defaults when you send new messages.
129						</p>
130
131						<TextStyleSelector
132							bind:style={currentStyle}
133							previewText="This is how your messages will look by default"
134							showPreview={true}
135							compact={false}
136						/>
137					</div>
138
139					<!-- Message Settings -->
140					<div class="preference-group">
141						<h3>Message Settings</h3>
142
143						<div class="setting-row">
144							<label for="max-length">Maximum message length:</label>
145							<input
146								id="max-length"
147								type="number"
148								min="100"
149								max="2000"
150								bind:value={preferences.maxMessageLength}
151								class="length-input"
152							/>
153							<span class="unit">characters</span>
154						</div>
155
156						<small class="help-text">
157							Longer messages may take more time to process and display.
158						</small>
159					</div>
160
161					<!-- Format Help -->
162					<div class="preference-group">
163						<details class="format-help">
164							<summary>Formatting Guide</summary>
165							<div class="format-examples">
166								<h4>BBCode Style:</h4>
167								<ul>
168									<li><code>[b]bold text[/b]</code> → <strong>bold text</strong></li>
169									<li><code>[i]italic text[/i]</code> → <em>italic text</em></li>
170									<li><code>[u]underlined[/u]</code> → <u>underlined</u></li>
171									<li><code>[s]strikethrough[/s]</code> → <s>strikethrough</s></li>
172									<li>
173										<code>[color=red]red text[/color]</code> →
174										<span style="color: red">red text</span>
175									</li>
176								</ul>
177
178								<h4>Markdown Style:</h4>
179								<ul>
180									<li><code>**bold text**</code> → <strong>bold text</strong></li>
181									<li><code>*italic text*</code> → <em>italic text</em></li>
182									<li><code>~~strikethrough~~</code> → <s>strikethrough</s></li>
183									<li><code>`code text`</code> → <code>code text</code></li>
184								</ul>
185							</div>
186						</details>
187					</div>
188				</div>
189
190				<!-- Modal buttons -->
191				<div class="modal-buttons">
192					<button class="reset-button" onclick={resetToDefaults} disabled={isSaving}>
193						Reset to Defaults
194					</button>
195
196					<div class="main-buttons">
197						<button onclick={cancel} disabled={isSaving}> Cancel </button>
198
199						<button
200							class="save-button"
201							onclick={savePreferences}
202							disabled={isSaving || !hasChanges}
203						>
204							{#if isSaving}
205								Saving...
206							{:else}
207								Save Changes
208							{/if}
209						</button>
210					</div>
211				</div>
212			</div>
213		</div>
214	</div>
215{/if}
216
217<style>
218	.modal-backdrop {
219		position: fixed;
220		top: 0;
221		left: 0;
222		right: 0;
223		bottom: 0;
224		background: rgba(0, 0, 0, 0.5);
225		display: flex;
226		align-items: center;
227		justify-content: center;
228		z-index: 1000;
229		padding: 20px;
230	}
231
232	.modal-window {
233		width: 100%;
234		max-width: 600px;
235		max-height: 90vh;
236		overflow: hidden;
237		box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.3);
238	}
239
240	.window-body {
241		display: flex;
242		flex-direction: column;
243		height: 70vh;
244		max-height: 600px;
245	}
246
247	.preferences-content {
248		flex: 1;
249		overflow-y: auto;
250		padding: 16px;
251	}
252
253	.preference-group {
254		margin-bottom: 24px;
255		padding-bottom: 16px;
256		border-bottom: 1px solid #ddd;
257	}
258
259	.preference-group:last-child {
260		border-bottom: none;
261		margin-bottom: 0;
262	}
263
264	.preference-group h3 {
265		margin: 0 0 8px 0;
266		font-size: 14px;
267		font-weight: bold;
268		color: #2d31a6;
269	}
270
271	.description {
272		margin: 0 0 12px 0;
273		font-size: 12px;
274		color: #666;
275	}
276
277	.checkbox-label {
278		display: flex;
279		align-items: center;
280		gap: 8px;
281		font-size: 12px;
282		cursor: pointer;
283	}
284
285	.help-text {
286		display: block;
287		margin-top: 4px;
288		font-size: 11px;
289		color: #666;
290		font-style: italic;
291	}
292
293	.setting-row {
294		display: flex;
295		align-items: center;
296		gap: 8px;
297		margin-bottom: 8px;
298	}
299
300	.setting-row label {
301		font-size: 12px;
302		min-width: 140px;
303	}
304
305	.length-input {
306		width: 80px;
307		padding: 2px 4px;
308		border: 1px inset #dfdfdf;
309		background: white;
310		font-size: 11px;
311	}
312
313	.unit {
314		font-size: 11px;
315		color: #666;
316	}
317
318	.format-help {
319		margin-top: 8px;
320	}
321
322	.format-help summary {
323		font-size: 12px;
324		font-weight: bold;
325		cursor: pointer;
326		padding: 4px 0;
327	}
328
329	.format-examples {
330		padding: 8px 0;
331		font-size: 11px;
332	}
333
334	.format-examples h4 {
335		margin: 12px 0 4px 0;
336		font-size: 11px;
337		color: #2d31a6;
338	}
339
340	.format-examples ul {
341		margin: 0;
342		padding-left: 16px;
343	}
344
345	.format-examples li {
346		margin-bottom: 2px;
347	}
348
349	.format-examples code {
350		background: #f0f0f0;
351		padding: 1px 3px;
352		border-radius: 2px;
353		font-family: 'Courier New', monospace;
354		font-size: 10px;
355	}
356
357	.modal-buttons {
358		display: flex;
359		justify-content: space-between;
360		align-items: center;
361		padding: 12px 16px;
362		border-top: 1px solid #ddd;
363		background: #f8f8f8;
364	}
365
366	.main-buttons {
367		display: flex;
368		gap: 8px;
369	}
370
371	.reset-button {
372		padding: 4px 12px;
373		font-size: 11px;
374		border: 1px outset #dfdfdf;
375		background: #f0f0f0;
376		cursor: pointer;
377	}
378
379	.reset-button:hover {
380		background: #e0e0e0;
381	}
382
383	.reset-button:disabled {
384		opacity: 0.6;
385		cursor: not-allowed;
386	}
387
388	.save-button {
389		padding: 4px 16px;
390		font-size: 11px;
391		font-weight: bold;
392		border: 1px outset #dfdfdf;
393		background: #0078d4;
394		color: white;
395		cursor: pointer;
396	}
397
398	.save-button:hover:not(:disabled) {
399		background: #106ebe;
400	}
401
402	.save-button:disabled {
403		opacity: 0.6;
404		cursor: not-allowed;
405		background: #999;
406	}
407
408	button {
409		padding: 4px 12px;
410		font-size: 11px;
411		border: 1px outset #dfdfdf;
412		background: #f0f0f0;
413		cursor: pointer;
414	}
415
416	button:hover:not(:disabled) {
417		background: #e0e0e0;
418	}
419
420	button:disabled {
421		opacity: 0.6;
422		cursor: not-allowed;
423	}
424
425	/* Scrollbar styling */
426	.preferences-content::-webkit-scrollbar {
427		width: 12px;
428	}
429
430	.preferences-content::-webkit-scrollbar-track {
431		background: #f0f0f0;
432		border: 1px inset #dfdfdf;
433	}
434
435	.preferences-content::-webkit-scrollbar-thumb {
436		background: #c0c0c0;
437		border: 1px outset #dfdfdf;
438	}
439
440	.preferences-content::-webkit-scrollbar-thumb:hover {
441		background: #a0a0a0;
442	}
443
444	/* Mobile responsive */
445	@media (max-width: 768px) {
446		.modal-backdrop {
447			padding: 10px;
448		}
449
450		.modal-window {
451			max-width: none;
452			max-height: 95vh;
453		}
454
455		.window-body {
456			height: 80vh;
457		}
458
459		.preferences-content {
460			padding: 12px;
461		}
462
463		.setting-row {
464			flex-direction: column;
465			align-items: flex-start;
466			gap: 4px;
467		}
468
469		.setting-row label {
470			min-width: auto;
471		}
472
473		.modal-buttons {
474			flex-direction: column;
475			gap: 8px;
476		}
477
478		.main-buttons {
479			order: -1;
480		}
481	}
482</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