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>