detached c40b6f5

fix: resolve all ESLint errors for CI

- Add MinimizableNode interface export to minimizable.ts
- Fix each block keys in multiple components
- Fix unused imports and variables
- Replace `any` types with proper TypeScript types
- Add eslint-disable comments for intentional @html usage
- Update ESLint config to handle .svelte.ts files
- Fix useless escape sequences in regex patterns
- Use $derived instead of $effect for simple derivations
commit
c40b6f524e97ded62aa8e4b4942c7141c8293eb3
author
Kit · · 2025-12-15 07:50 UTC
parent
676e4cd
signature
SSH UnverifiedSHA256:3RGv7q1rcg0JUU2CjMAhwZcyGvcCDmFuxQQT8Z18wLA · arch-ssh This key is not trusted for the committer email
20 files · +86 -122
M eslint.config.js +10 -0
@@ -30,5 +30,15 @@ export default ts.config(
3030 parser: ts.parser
3131 }
3232 }
33+ },
34+ {
35+ // Svelte 5 .svelte.ts files use runes - need svelte-eslint-parser
36+ files: ['**/*.svelte.ts', '**/*.svelte.js'],
37+ languageOptions: {
38+ parser: svelte.parser,
39+ parserOptions: {
40+ parser: ts.parser
41+ }
42+ }
3343 }
3444 );
@@ -30,5 +30,15 @@ export default ts.config(
30 parser: ts.parser30 parser: ts.parser
31 }31 }
32 }32 }
33+ },
34+ {
35+ // Svelte 5 .svelte.ts files use runes - need svelte-eslint-parser
36+ files: ['**/*.svelte.ts', '**/*.svelte.js'],
37+ languageOptions: {
38+ parser: svelte.parser,
39+ parserOptions: {
40+ parser: ts.parser
41+ }
42+ }
33 }43 }
34 );44 );
M src/lib/actions/minimizable.ts +7 -0
@@ -5,6 +5,13 @@ interface MinimizableParameters {
55 onMinimize?: (isMinimized: boolean) => void;
66 }
77
8+export interface MinimizableNode extends HTMLDivElement {
9+ minimize: () => void;
10+ restore: () => void;
11+ toggleMinimize: () => void;
12+ isMinimized: () => boolean;
13+}
14+
815 interface MinimizableAttributes {
916 onminimize?: (e: CustomEvent<{ isMinimized: boolean }>) => void;
1017 }
@@ -5,6 +5,13 @@ interface MinimizableParameters {
5 onMinimize?: (isMinimized: boolean) => void;5 onMinimize?: (isMinimized: boolean) => void;
6 }6 }
7 7
8+export interface MinimizableNode extends HTMLDivElement {
9+ minimize: () => void;
10+ restore: () => void;
11+ toggleMinimize: () => void;
12+ isMinimized: () => boolean;
13+}
14+
8 interface MinimizableAttributes {15 interface MinimizableAttributes {
9 onminimize?: (e: CustomEvent<{ isMinimized: boolean }>) => void;16 onminimize?: (e: CustomEvent<{ isMinimized: boolean }>) => void;
10 }17 }
M src/lib/components/aim-login.svelte +11 -12
@@ -2,7 +2,6 @@
22 <script lang="ts">
33 import { draggable } from '$lib/actions/draggable';
44 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5- import type { User, SafeUser } from '$lib/types/chat';
65 import { invalidateAll } from '$app/navigation';
76 import { chatState } from '$lib/states/chat.svelte';
87 import { createSafeUser } from '$lib/types/chat';
@@ -11,16 +10,10 @@
1110 import {
1211 loginSchema,
1312 createRegistrationSchema,
14- validatePasswordStrength,
1513 DEFAULT_PASSWORD_CONSTRAINTS,
1614 type PasswordConstraints
1715 } from '../validation/password';
18-
19- interface $$Props {
20- showAuth: boolean;
21- onLoginSuccess?: (user: SafeUser | null) => void;
22- activeTab?: 'signin' | 'signup';
23- }
16+ import { z } from 'zod';
2417
2518 let { showAuth = $bindable(), onLoginSuccess, activeTab: initialTab = 'signin' } = $props();
2619
@@ -128,8 +121,11 @@
128121 try {
129122 loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() });
130123 return null;
131- } catch (err: any) {
132- return err.errors?.[0]?.message || 'Invalid input';
124+ } catch (err) {
125+ if (err instanceof z.ZodError) {
126+ return err.issues?.[0]?.message || 'Invalid input';
127+ }
128+ return 'Invalid input';
133129 }
134130 }
135131
@@ -144,8 +140,11 @@
144140 turnstileToken: suTurnstileToken
145141 });
146142 return null;
147- } catch (err: any) {
148- return err.errors?.[0]?.message || 'Invalid input';
143+ } catch (err) {
144+ if (err instanceof z.ZodError) {
145+ return err.issues?.[0]?.message || 'Invalid input';
146+ }
147+ return 'Invalid input';
149148 }
150149 }
151150
@@ -2,7 +2,6 @@
2 <script lang="ts">2 <script lang="ts">
3 import { draggable } from '$lib/actions/draggable';3 import { draggable } from '$lib/actions/draggable';
4 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';4 import type { RegisterResponse, RegisterResponseError } from '$lib/types/payloads';
5- import type { User, SafeUser } from '$lib/types/chat';
6 import { invalidateAll } from '$app/navigation';5 import { invalidateAll } from '$app/navigation';
7 import { chatState } from '$lib/states/chat.svelte';6 import { chatState } from '$lib/states/chat.svelte';
8 import { createSafeUser } from '$lib/types/chat';7 import { createSafeUser } from '$lib/types/chat';
@@ -11,16 +10,10 @@
11 import {10 import {
12 loginSchema,11 loginSchema,
13 createRegistrationSchema,12 createRegistrationSchema,
14- validatePasswordStrength,
15 DEFAULT_PASSWORD_CONSTRAINTS,13 DEFAULT_PASSWORD_CONSTRAINTS,
16 type PasswordConstraints14 type PasswordConstraints
17 } from '../validation/password';15 } from '../validation/password';
18-16+ import { z } from 'zod';
19- interface $$Props {
20- showAuth: boolean;
21- onLoginSuccess?: (user: SafeUser | null) => void;
22- activeTab?: 'signin' | 'signup';
23- }
24 17
25 let { showAuth = $bindable(), onLoginSuccess, activeTab: initialTab = 'signin' } = $props();18 let { showAuth = $bindable(), onLoginSuccess, activeTab: initialTab = 'signin' } = $props();
26 19
@@ -128,8 +121,11 @@
128 try {121 try {
129 loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() });122 loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() });
130 return null;123 return null;
131- } catch (err: any) {124+ } catch (err) {
132- return err.errors?.[0]?.message || 'Invalid input';125+ if (err instanceof z.ZodError) {
126+ return err.issues?.[0]?.message || 'Invalid input';
127+ }
128+ return 'Invalid input';
133 }129 }
134 }130 }
135 131
@@ -144,8 +140,11 @@
144 turnstileToken: suTurnstileToken140 turnstileToken: suTurnstileToken
145 });141 });
146 return null;142 return null;
147- } catch (err: any) {143+ } catch (err) {
148- return err.errors?.[0]?.message || 'Invalid input';144+ if (err instanceof z.ZodError) {
145+ return err.issues?.[0]?.message || 'Invalid input';
146+ }
147+ return 'Invalid input';
149 }148 }
150 }149 }
151 150
M src/lib/components/chat-room.svelte +10 -47
@@ -1,19 +1,12 @@
11 <script lang="ts">
22 import { chatState } from '../states/chat.svelte';
3- import type {
4- MessageType,
5- User,
6- ChatRoom,
7- Message,
8- EnrichedMessage,
9- SafeUser
10- } from '../types/chat';
3+ import type { User, Message, EnrichedMessage, SafeUser } from '../types/chat';
114 import { onMount } from 'svelte';
125 import { browser } from '$app/environment';
136 import { draggable } from '$lib/actions/draggable';
147 import { resizable } from '$lib/actions/resizable';
158 import { maximizable } from '$lib/actions/maximizable';
16- import { minimizable } from '$lib/actions/minimizable';
9+ import { minimizable, type MinimizableNode } from '$lib/actions/minimizable';
1710 import LoadingButton from './ui/button-loading.svelte';
1811 import Tooltip from './ui/tooltip.svelte';
1912 import LoadingDots from './ui/loading-dots.svelte';
@@ -49,7 +42,6 @@
4942 ...initialTextStyle,
5043 color: initialTextStyle.color || '#000000' // Ensure we always have a valid color
5144 });
52- let showFormattingToolbar = $state(true);
5345
5446 // Rate limiting state
5547 let cooldownEndTime = $state<number | null>(null);
@@ -297,39 +289,6 @@
297289 }
298290 }
299291
300- function formatLastSeen(lastSeen: number | null | undefined) {
301- if (!lastSeen) return 'Never';
302-
303- const now = Date.now();
304- const diff = now - lastSeen;
305-
306- // Less than a minute
307- if (diff < 60000) {
308- return 'Just now';
309- }
310-
311- // Less than an hour
312- if (diff < 3600000) {
313- const minutes = Math.floor(diff / 60000);
314- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
315- }
316-
317- // Less than a day
318- if (diff < 86400000) {
319- const hours = Math.floor(diff / 3600000);
320- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
321- }
322-
323- // Less than a week
324- if (diff < 604800000) {
325- const days = Math.floor(diff / 86400000);
326- return `${days} day${days > 1 ? 's' : ''} ago`;
327- }
328-
329- // More than a week
330- return new Date(lastSeen).toLocaleDateString();
331- }
332-
333292 function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {
334293 windowX = event.detail.x;
335294 windowY = event.detail.y;
@@ -344,7 +303,7 @@
344303 if (node) {
345304 // If minimized, restore first
346305 if (isMinimized) {
347- const minimizableNode = node as any;
306+ const minimizableNode = node as unknown as MinimizableNode;
348307 if (minimizableNode?.toggleMinimize) {
349308 minimizableNode.toggleMinimize();
350309 }
@@ -509,7 +468,9 @@
509468 aria-label="Minimize"
510469 onclick={(e) => {
511470 e.stopPropagation();
512- const node = (e.currentTarget as HTMLElement).closest('.window') as any;
471+ const node = (e.currentTarget as HTMLElement).closest(
472+ '.window'
473+ ) as MinimizableNode | null;
513474 if (node?.toggleMinimize) node.toggleMinimize();
514475 }}
515476 ></button>
@@ -526,7 +487,9 @@
526487 aria-label="Minimize"
527488 onclick={(e) => {
528489 e.stopPropagation();
529- const node = (e.currentTarget as HTMLElement).closest('.window') as any;
490+ const node = (e.currentTarget as HTMLElement).closest(
491+ '.window'
492+ ) as MinimizableNode | null;
530493 if (node?.toggleMinimize) node.toggleMinimize();
531494 }}
532495 ></button>
@@ -695,7 +658,7 @@
695658 <p style="margin: 0 0 0.3rem 0;">
696659 <strong>{totalUsers} membre{totalUsers > 1 ? 's' : ''}</strong>
697660 </p>
698- {#each onlineUsers as user}
661+ {#each onlineUsers as user (user.id)}
699662 <div class="user" class:offline={user.status === 'offline'}>
700663 <span class="status-icon">{getStatusIcon(user.status)}</span>
701664 <div class="user-info">
@@ -1,19 +1,12 @@
1 <script lang="ts">1 <script lang="ts">
2 import { chatState } from '../states/chat.svelte';2 import { chatState } from '../states/chat.svelte';
3- import type {3+ import type { User, Message, EnrichedMessage, SafeUser } from '../types/chat';
4- MessageType,
5- User,
6- ChatRoom,
7- Message,
8- EnrichedMessage,
9- SafeUser
10- } from '../types/chat';
11 import { onMount } from 'svelte';4 import { onMount } from 'svelte';
12 import { browser } from '$app/environment';5 import { browser } from '$app/environment';
13 import { draggable } from '$lib/actions/draggable';6 import { draggable } from '$lib/actions/draggable';
14 import { resizable } from '$lib/actions/resizable';7 import { resizable } from '$lib/actions/resizable';
15 import { maximizable } from '$lib/actions/maximizable';8 import { maximizable } from '$lib/actions/maximizable';
16- import { minimizable } from '$lib/actions/minimizable';9+ import { minimizable, type MinimizableNode } from '$lib/actions/minimizable';
17 import LoadingButton from './ui/button-loading.svelte';10 import LoadingButton from './ui/button-loading.svelte';
18 import Tooltip from './ui/tooltip.svelte';11 import Tooltip from './ui/tooltip.svelte';
19 import LoadingDots from './ui/loading-dots.svelte';12 import LoadingDots from './ui/loading-dots.svelte';
@@ -49,7 +42,6 @@
49 ...initialTextStyle,42 ...initialTextStyle,
50 color: initialTextStyle.color || '#000000' // Ensure we always have a valid color43 color: initialTextStyle.color || '#000000' // Ensure we always have a valid color
51 });44 });
52- let showFormattingToolbar = $state(true);
53 45
54 // Rate limiting state46 // Rate limiting state
55 let cooldownEndTime = $state<number | null>(null);47 let cooldownEndTime = $state<number | null>(null);
@@ -297,39 +289,6 @@
297 }289 }
298 }290 }
299 291
300- function formatLastSeen(lastSeen: number | null | undefined) {
301- if (!lastSeen) return 'Never';
302-
303- const now = Date.now();
304- const diff = now - lastSeen;
305-
306- // Less than a minute
307- if (diff < 60000) {
308- return 'Just now';
309- }
310-
311- // Less than an hour
312- if (diff < 3600000) {
313- const minutes = Math.floor(diff / 60000);
314- return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
315- }
316-
317- // Less than a day
318- if (diff < 86400000) {
319- const hours = Math.floor(diff / 3600000);
320- return `${hours} hour${hours > 1 ? 's' : ''} ago`;
321- }
322-
323- // Less than a week
324- if (diff < 604800000) {
325- const days = Math.floor(diff / 86400000);
326- return `${days} day${days > 1 ? 's' : ''} ago`;
327- }
328-
329- // More than a week
330- return new Date(lastSeen).toLocaleDateString();
331- }
332-
333 function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {292 function handleDragMove(event: CustomEvent<{ x: number; y: number }>) {
334 windowX = event.detail.x;293 windowX = event.detail.x;
335 windowY = event.detail.y;294 windowY = event.detail.y;
@@ -344,7 +303,7 @@
344 if (node) {303 if (node) {
345 // If minimized, restore first304 // If minimized, restore first
346 if (isMinimized) {305 if (isMinimized) {
347- const minimizableNode = node as any;306+ const minimizableNode = node as unknown as MinimizableNode;
348 if (minimizableNode?.toggleMinimize) {307 if (minimizableNode?.toggleMinimize) {
349 minimizableNode.toggleMinimize();308 minimizableNode.toggleMinimize();
350 }309 }
@@ -509,7 +468,9 @@
509 aria-label="Minimize"468 aria-label="Minimize"
510 onclick={(e) => {469 onclick={(e) => {
511 e.stopPropagation();470 e.stopPropagation();
512- const node = (e.currentTarget as HTMLElement).closest('.window') as any;471+ const node = (e.currentTarget as HTMLElement).closest(
472+ '.window'
473+ ) as MinimizableNode | null;
513 if (node?.toggleMinimize) node.toggleMinimize();474 if (node?.toggleMinimize) node.toggleMinimize();
514 }}475 }}
515 ></button>476 ></button>
@@ -526,7 +487,9 @@
526 aria-label="Minimize"487 aria-label="Minimize"
527 onclick={(e) => {488 onclick={(e) => {
528 e.stopPropagation();489 e.stopPropagation();
529- const node = (e.currentTarget as HTMLElement).closest('.window') as any;490+ const node = (e.currentTarget as HTMLElement).closest(
491+ '.window'
492+ ) as MinimizableNode | null;
530 if (node?.toggleMinimize) node.toggleMinimize();493 if (node?.toggleMinimize) node.toggleMinimize();
531 }}494 }}
532 ></button>495 ></button>
@@ -695,7 +658,7 @@
695 <p style="margin: 0 0 0.3rem 0;">658 <p style="margin: 0 0 0.3rem 0;">
696 <strong>{totalUsers} membre{totalUsers > 1 ? 's' : ''}</strong>659 <strong>{totalUsers} membre{totalUsers > 1 ? 's' : ''}</strong>
697 </p>660 </p>
698- {#each onlineUsers as user}661+ {#each onlineUsers as user (user.id)}
699 <div class="user" class:offline={user.status === 'offline'}>662 <div class="user" class:offline={user.status === 'offline'}>
700 <span class="status-icon">{getStatusIcon(user.status)}</span>663 <span class="status-icon">{getStatusIcon(user.status)}</span>
701 <div class="user-info">664 <div class="user-info">
M src/lib/components/color-picker-256.svelte +5 -4
@@ -7,7 +7,7 @@
77 selectedGradient = $bindable(undefined),
88 onColorChange,
99 onGradientChange,
10- compact = true
10+ compact: _compact = true
1111 } = $props<{
1212 selectedColor?: string;
1313 selectedGradient?: string[];
@@ -15,6 +15,7 @@
1515 onGradientChange?: (gradient: string[]) => void;
1616 compact?: boolean;
1717 }>();
18+ void _compact; // Reserved for future use
1819
1920 // State
2021 let showPicker = $state(false);
@@ -142,7 +143,7 @@
142143 <!-- Gradient controls -->
143144 <div class="gradient-controls">
144145 <div class="gradient-slots">
145- {#each gradientColors as color, index}
146+ {#each gradientColors as color, index (index)}
146147 <button
147148 class="gradient-slot"
148149 class:active={activeGradientSlot === index}
@@ -186,7 +187,7 @@
186187 <div class="color-section">
187188 <div class="section-title">Classic Colors</div>
188189 <div class="color-row">
189- {#each classicColors as color}
190+ {#each classicColors as color (color)}
190191 <button
191192 class="color-swatch classic-swatch"
192193 style="--swatch-color: {color}; background-color: {color} !important;"
@@ -201,7 +202,7 @@
201202 <div class="color-section">
202203 <div class="section-title">Full Palette</div>
203204 <div class="color-grid">
204- {#each colors256 as color}
205+ {#each colors256 as color (color)}
205206 <button
206207 class="color-swatch"
207208 style="--swatch-color: {color}; background-color: {color} !important;"
@@ -7,7 +7,7 @@
7 selectedGradient = $bindable(undefined),7 selectedGradient = $bindable(undefined),
8 onColorChange,8 onColorChange,
9 onGradientChange,9 onGradientChange,
10- compact = true10+ compact: _compact = true
11 } = $props<{11 } = $props<{
12 selectedColor?: string;12 selectedColor?: string;
13 selectedGradient?: string[];13 selectedGradient?: string[];
@@ -15,6 +15,7 @@
15 onGradientChange?: (gradient: string[]) => void;15 onGradientChange?: (gradient: string[]) => void;
16 compact?: boolean;16 compact?: boolean;
17 }>();17 }>();
18+ void _compact; // Reserved for future use
18 19
19 // State20 // State
20 let showPicker = $state(false);21 let showPicker = $state(false);
@@ -142,7 +143,7 @@
142 <!-- Gradient controls -->143 <!-- Gradient controls -->
143 <div class="gradient-controls">144 <div class="gradient-controls">
144 <div class="gradient-slots">145 <div class="gradient-slots">
145- {#each gradientColors as color, index}146+ {#each gradientColors as color, index (index)}
146 <button147 <button
147 class="gradient-slot"148 class="gradient-slot"
148 class:active={activeGradientSlot === index}149 class:active={activeGradientSlot === index}
@@ -186,7 +187,7 @@
186 <div class="color-section">187 <div class="color-section">
187 <div class="section-title">Classic Colors</div>188 <div class="section-title">Classic Colors</div>
188 <div class="color-row">189 <div class="color-row">
189- {#each classicColors as color}190+ {#each classicColors as color (color)}
190 <button191 <button
191 class="color-swatch classic-swatch"192 class="color-swatch classic-swatch"
192 style="--swatch-color: {color}; background-color: {color} !important;"193 style="--swatch-color: {color}; background-color: {color} !important;"
@@ -201,7 +202,7 @@
201 <div class="color-section">202 <div class="color-section">
202 <div class="section-title">Full Palette</div>203 <div class="section-title">Full Palette</div>
203 <div class="color-grid">204 <div class="color-grid">
204- {#each colors256 as color}205+ {#each colors256 as color (color)}
205 <button206 <button
206 class="color-swatch"207 class="color-swatch"
207 style="--swatch-color: {color}; background-color: {color} !important;"208 style="--swatch-color: {color}; background-color: {color} !important;"
M src/lib/components/formatted-message.svelte +4 -2
@@ -1,9 +1,8 @@
11 <script lang="ts">
2- import { onMount } from 'svelte';
32 import type { EnrichedMessage } from '../types/chat';
43 import type { TextStyle } from '../types/text-formatting';
54 import { formatText, createGradientText } from '../utils/text-formatter';
6- import { DEFAULT_TEXT_STYLE, generateCSSStyle, RETRO_FONTS } from '../types/text-formatting';
5+ import { DEFAULT_TEXT_STYLE } from '../types/text-formatting';
76
87 // Props
98 let { message, allowFormatting = true } = $props<{
@@ -175,16 +174,19 @@
175174 })()}
176175 {#if parsedStyle?.gradient && Array.isArray(parsedStyle.gradient) && parsedStyle.gradient.length > 1}
177176 <!-- Gradient messages are already wrapped properly -->
177+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedContent is sanitized by createGradientText -->
178178 {@html formattedContent}
179179 {:else}
180180 <!-- Regular formatted messages -->
181181 <span class={getMessageClasses()} style={getMessageStyles()}>
182+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedContent is sanitized by formatText -->
182183 {@html formattedContent}
183184 </span>
184185 {/if}
185186 {:else}
186187 <!-- Plain messages -->
187188 <span class={getMessageClasses()}>
189+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedContent is sanitized by escapeHtml -->
188190 {@html formattedContent}
189191 </span>
190192 {/if}
@@ -1,9 +1,8 @@
1 <script lang="ts">1 <script lang="ts">
2- import { onMount } from 'svelte';
3 import type { EnrichedMessage } from '../types/chat';2 import type { EnrichedMessage } from '../types/chat';
4 import type { TextStyle } from '../types/text-formatting';3 import type { TextStyle } from '../types/text-formatting';
5 import { formatText, createGradientText } from '../utils/text-formatter';4 import { formatText, createGradientText } from '../utils/text-formatter';
6- import { DEFAULT_TEXT_STYLE, generateCSSStyle, RETRO_FONTS } from '../types/text-formatting';5+ import { DEFAULT_TEXT_STYLE } from '../types/text-formatting';
7 6
8 // Props7 // Props
9 let { message, allowFormatting = true } = $props<{8 let { message, allowFormatting = true } = $props<{
@@ -175,16 +174,19 @@
175 })()}174 })()}
176 {#if parsedStyle?.gradient && Array.isArray(parsedStyle.gradient) && parsedStyle.gradient.length > 1}175 {#if parsedStyle?.gradient && Array.isArray(parsedStyle.gradient) && parsedStyle.gradient.length > 1}
177 <!-- Gradient messages are already wrapped properly -->176 <!-- Gradient messages are already wrapped properly -->
177+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedContent is sanitized by createGradientText -->
178 {@html formattedContent}178 {@html formattedContent}
179 {:else}179 {:else}
180 <!-- Regular formatted messages -->180 <!-- Regular formatted messages -->
181 <span class={getMessageClasses()} style={getMessageStyles()}>181 <span class={getMessageClasses()} style={getMessageStyles()}>
182+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedContent is sanitized by formatText -->
182 {@html formattedContent}183 {@html formattedContent}
183 </span>184 </span>
184 {/if}185 {/if}
185 {:else}186 {:else}
186 <!-- Plain messages -->187 <!-- Plain messages -->
187 <span class={getMessageClasses()}>188 <span class={getMessageClasses()}>
189+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedContent is sanitized by escapeHtml -->
188 {@html formattedContent}190 {@html formattedContent}
189 </span>191 </span>
190 {/if}192 {/if}
M src/lib/components/password-strength-indicator.svelte +8 -6
@@ -5,13 +5,15 @@
55 DEFAULT_PASSWORD_CONSTRAINTS
66 } from '../validation/password';
77
8- interface $$Props {
8+ let {
9+ password = '',
10+ constraints = DEFAULT_PASSWORD_CONSTRAINTS,
11+ showDetails = true
12+ } = $props<{
913 password: string;
1014 constraints?: PasswordConstraints;
1115 showDetails?: boolean;
12- }
13-
14- let { password = '', constraints = DEFAULT_PASSWORD_CONSTRAINTS, showDetails = true } = $props();
16+ }>();
1517
1618 // Reactive validation result
1719 let validation = $derived(validatePasswordStrength(password, constraints));
@@ -43,7 +45,7 @@
4345 {#if showDetails}
4446 <!-- Requirements checklist -->
4547 <div class="requirements-list">
46- {#each validation.requirements as requirement}
48+ {#each validation.requirements as requirement (requirement.message)}
4749 <div class="requirement {requirement.met ? 'met' : 'unmet'}">
4850 <span class="requirement-icon">
4951 {requirement.met ? '✓' : '✗'}
@@ -56,7 +58,7 @@
5658 <!-- Error messages -->
5759 {#if validation.errors.length > 0}
5860 <div class="error-messages">
59- {#each validation.errors as error}
61+ {#each validation.errors as error (error)}
6062 <div class="error-message">{error}</div>
6163 {/each}
6264 </div>
@@ -5,13 +5,15 @@
5 DEFAULT_PASSWORD_CONSTRAINTS5 DEFAULT_PASSWORD_CONSTRAINTS
6 } from '../validation/password';6 } from '../validation/password';
7 7
8- interface $$Props {8+ let {
9+ password = '',
10+ constraints = DEFAULT_PASSWORD_CONSTRAINTS,
11+ showDetails = true
12+ } = $props<{
9 password: string;13 password: string;
10 constraints?: PasswordConstraints;14 constraints?: PasswordConstraints;
11 showDetails?: boolean;15 showDetails?: boolean;
12- }16+ }>();
13-
14- let { password = '', constraints = DEFAULT_PASSWORD_CONSTRAINTS, showDetails = true } = $props();
15 17
16 // Reactive validation result18 // Reactive validation result
17 let validation = $derived(validatePasswordStrength(password, constraints));19 let validation = $derived(validatePasswordStrength(password, constraints));
@@ -43,7 +45,7 @@
43 {#if showDetails}45 {#if showDetails}
44 <!-- Requirements checklist -->46 <!-- Requirements checklist -->
45 <div class="requirements-list">47 <div class="requirements-list">
46- {#each validation.requirements as requirement}48+ {#each validation.requirements as requirement (requirement.message)}
47 <div class="requirement {requirement.met ? 'met' : 'unmet'}">49 <div class="requirement {requirement.met ? 'met' : 'unmet'}">
48 <span class="requirement-icon">50 <span class="requirement-icon">
49 {requirement.met ? '✓' : '✗'}51 {requirement.met ? '✓' : '✗'}
@@ -56,7 +58,7 @@
56 <!-- Error messages -->58 <!-- Error messages -->
57 {#if validation.errors.length > 0}59 {#if validation.errors.length > 0}
58 <div class="error-messages">60 <div class="error-messages">
59- {#each validation.errors as error}61+ {#each validation.errors as error (error)}
60 <div class="error-message">{error}</div>62 <div class="error-message">{error}</div>
61 {/each}63 {/each}
62 </div>64 </div>
M src/lib/components/text-formatting-toolbar.svelte +5 -12
@@ -1,8 +1,6 @@
11 <script lang="ts">
22 import {
33 RETRO_FONTS,
4- CLASSIC_COLORS,
5- GRADIENT_PRESETS,
64 DEFAULT_TEXT_STYLE,
75 type TextStyle,
86 type FontFamily
@@ -12,10 +10,10 @@
1210 // Props
1311 let {
1412 style = $bindable(DEFAULT_TEXT_STYLE),
15- onPreviewText,
13+ onPreviewText: _onPreviewText,
1614 compact = true,
1715 showFontSelector = false,
18- showGradients = true
16+ showGradients: _showGradients = true
1917 } = $props<{
2018 style?: TextStyle;
2119 onPreviewText?: (text: string) => void;
@@ -23,6 +21,8 @@
2321 showFontSelector?: boolean;
2422 showGradients?: boolean;
2523 }>();
24+ void _showGradients; // Reserved for future use
25+ void _onPreviewText; // Reserved for future use
2626
2727 // State
2828 let currentColor = $state(style.color || '#000000');
@@ -75,13 +75,6 @@
7575 }
7676 }
7777
78- // Preview text change
79- function previewTextChanged() {
80- if (onPreviewText) {
81- onPreviewText('Sample formatted text');
82- }
83- }
84-
8578 // Update internal state when style prop changes
8679 $effect(() => {
8780 currentColor = style.color || '#000000';
@@ -99,7 +92,7 @@
9992 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
10093 title="Font Family"
10194 >
102- {#each Object.entries(RETRO_FONTS) as [key, font]}
95+ {#each Object.entries(RETRO_FONTS) as [key, font] (key)}
10396 <option value={key} style="font-family: {font.stack};">{font.name}</option>
10497 {/each}
10598 </select>
@@ -1,8 +1,6 @@
1 <script lang="ts">1 <script lang="ts">
2 import {2 import {
3 RETRO_FONTS,3 RETRO_FONTS,
4- CLASSIC_COLORS,
5- GRADIENT_PRESETS,
6 DEFAULT_TEXT_STYLE,4 DEFAULT_TEXT_STYLE,
7 type TextStyle,5 type TextStyle,
8 type FontFamily6 type FontFamily
@@ -12,10 +10,10 @@
12 // Props10 // Props
13 let {11 let {
14 style = $bindable(DEFAULT_TEXT_STYLE),12 style = $bindable(DEFAULT_TEXT_STYLE),
15- onPreviewText,13+ onPreviewText: _onPreviewText,
16 compact = true,14 compact = true,
17 showFontSelector = false,15 showFontSelector = false,
18- showGradients = true16+ showGradients: _showGradients = true
19 } = $props<{17 } = $props<{
20 style?: TextStyle;18 style?: TextStyle;
21 onPreviewText?: (text: string) => void;19 onPreviewText?: (text: string) => void;
@@ -23,6 +21,8 @@
23 showFontSelector?: boolean;21 showFontSelector?: boolean;
24 showGradients?: boolean;22 showGradients?: boolean;
25 }>();23 }>();
24+ void _showGradients; // Reserved for future use
25+ void _onPreviewText; // Reserved for future use
26 26
27 // State27 // State
28 let currentColor = $state(style.color || '#000000');28 let currentColor = $state(style.color || '#000000');
@@ -75,13 +75,6 @@
75 }75 }
76 }76 }
77 77
78- // Preview text change
79- function previewTextChanged() {
80- if (onPreviewText) {
81- onPreviewText('Sample formatted text');
82- }
83- }
84-
85 // Update internal state when style prop changes78 // Update internal state when style prop changes
86 $effect(() => {79 $effect(() => {
87 currentColor = style.color || '#000000';80 currentColor = style.color || '#000000';
@@ -99,7 +92,7 @@
99 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}92 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
100 title="Font Family"93 title="Font Family"
101 >94 >
102- {#each Object.entries(RETRO_FONTS) as [key, font]}95+ {#each Object.entries(RETRO_FONTS) as [key, font] (key)}
103 <option value={key} style="font-family: {font.stack};">{font.name}</option>96 <option value={key} style="font-family: {font.stack};">{font.name}</option>
104 {/each}97 {/each}
105 </select>98 </select>
M src/lib/components/text-style-selector.svelte +2 -2
@@ -1,7 +1,6 @@
11 <script lang="ts">
22 import {
33 RETRO_FONTS,
4- CLASSIC_COLORS,
54 DEFAULT_TEXT_STYLE,
65 type TextStyle,
76 type FontFamily,
@@ -106,7 +105,7 @@
106105 bind:value={style.fontFamily}
107106 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
108107 >
109- {#each Object.entries(RETRO_FONTS) as [key, font]}
108+ {#each Object.entries(RETRO_FONTS) as [key, font] (key)}
110109 <option value={key} style="font-family: {font.stack}">
111110 {font.name}
112111 </option>
@@ -185,6 +184,7 @@
185184 {#if isProcessing}
186185 <span class="loading">Processing...</span>
187186 {:else}
187+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedPreview is sanitized by formatText/createGradientText -->
188188 {@html formattedPreview}
189189 {/if}
190190 </div>
@@ -1,7 +1,6 @@
1 <script lang="ts">1 <script lang="ts">
2 import {2 import {
3 RETRO_FONTS,3 RETRO_FONTS,
4- CLASSIC_COLORS,
5 DEFAULT_TEXT_STYLE,4 DEFAULT_TEXT_STYLE,
6 type TextStyle,5 type TextStyle,
7 type FontFamily,6 type FontFamily,
@@ -106,7 +105,7 @@
106 bind:value={style.fontFamily}105 bind:value={style.fontFamily}
107 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}106 onchange={(e) => selectFont((e.target as HTMLSelectElement).value as FontFamily)}
108 >107 >
109- {#each Object.entries(RETRO_FONTS) as [key, font]}108+ {#each Object.entries(RETRO_FONTS) as [key, font] (key)}
110 <option value={key} style="font-family: {font.stack}">109 <option value={key} style="font-family: {font.stack}">
111 {font.name}110 {font.name}
112 </option>111 </option>
@@ -185,6 +184,7 @@
185 {#if isProcessing}184 {#if isProcessing}
186 <span class="loading">Processing...</span>185 <span class="loading">Processing...</span>
187 {:else}186 {:else}
187+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- formattedPreview is sanitized by formatText/createGradientText -->
188 {@html formattedPreview}188 {@html formattedPreview}
189 {/if}189 {/if}
190 </div>190 </div>
M src/lib/components/turnstile.svelte +1 -1
@@ -2,7 +2,7 @@
22 import { onMount } from 'svelte';
33 import { fade } from 'svelte/transition';
44 import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';
5- import type { Turnstile, TurnstileOptions } from '$lib/types/turnstile';
5+ import type { TurnstileOptions } from '$lib/types/turnstile';
66
77 let isVerified = $state(false);
88
@@ -2,7 +2,7 @@
2 import { onMount } from 'svelte';2 import { onMount } from 'svelte';
3 import { fade } from 'svelte/transition';3 import { fade } from 'svelte/transition';
4 import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';4 import { PUBLIC_TURNSTILE_SITE_KEY } from '$env/static/public';
5- import type { Turnstile, TurnstileOptions } from '$lib/types/turnstile';5+ import type { TurnstileOptions } from '$lib/types/turnstile';
6 6
7 let isVerified = $state(false);7 let isVerified = $state(false);
8 8
M src/lib/components/ui/tooltip.svelte +1 -0
@@ -1,4 +1,5 @@
11 <script lang="ts">
2+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- $$Props is used for Svelte type inference
23 type $$Props = {
34 data: {
45 text: string;
@@ -1,4 +1,5 @@
1 <script lang="ts">1 <script lang="ts">
2+ // eslint-disable-next-line @typescript-eslint/no-unused-vars -- $$Props is used for Svelte type inference
2 type $$Props = {3 type $$Props = {
3 data: {4 data: {
4 text: string;5 text: string;
M src/lib/components/user-preferences-modal.svelte +3 -6
@@ -18,12 +18,9 @@
1818 let preferences = $state<UserTextPreferences>({ ...DEFAULT_USER_PREFERENCES });
1919 let currentStyle = $state<TextStyle>({ ...DEFAULT_TEXT_STYLE });
2020 let isSaving = $state(false);
21- let hasChanges = $state(false);
22-
23- // Track changes
24- $effect(() => {
25- hasChanges = JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES);
26- });
21+ let hasChanges = $derived(
22+ JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES)
23+ );
2724
2825 // Update preferences when style changes
2926 $effect(() => {
@@ -18,12 +18,9 @@
18 let preferences = $state<UserTextPreferences>({ ...DEFAULT_USER_PREFERENCES });18 let preferences = $state<UserTextPreferences>({ ...DEFAULT_USER_PREFERENCES });
19 let currentStyle = $state<TextStyle>({ ...DEFAULT_TEXT_STYLE });19 let currentStyle = $state<TextStyle>({ ...DEFAULT_TEXT_STYLE });
20 let isSaving = $state(false);20 let isSaving = $state(false);
21- let hasChanges = $state(false);21+ let hasChanges = $derived(
22-22+ JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES)
23- // Track changes23+ );
24- $effect(() => {
25- hasChanges = JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES);
26- });
27 24
28 // Update preferences when style changes25 // Update preferences when style changes
29 $effect(() => {26 $effect(() => {
M src/lib/states/chat.svelte.ts +6 -7
@@ -5,6 +5,7 @@ import type {
55 SendMessageResponse,
66 GetMessagesResponse
77 } from '../types/payloads';
8+import type { TextStyle } from '../types/text-formatting';
89 import { invalidate } from '$app/navigation';
910 import { env } from '$env/dynamic/public';
1011
@@ -157,6 +158,7 @@ class ChatState {
157158
158159 console.debug('Fetched room buddy list:', data.buddyList);
159160 // Merge fetched buddy list with existing cached users to preserve user details even if offline
161+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- temporary Map for merging, not reactive state
160162 const mergedUsers = new Map<string, (typeof data.buddyList)[0]>();
161163 // Add existing cached users
162164 Object.values(this.userCache).forEach((user) => mergedUsers.set(user.id, user));
@@ -429,6 +431,7 @@ class ChatState {
429431 async initializeMessages() {
430432 try {
431433 // Build the URL with appropriate parameters
434+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- URLSearchParams for URL building, not reactive state
432435 const params = new URLSearchParams();
433436 if (!this.currentUser) {
434437 params.append('public', 'true');
@@ -498,7 +501,7 @@ class ChatState {
498501 async sendMessage(
499502 content: string,
500503 type: Message['type'] = 'chat',
501- textStyle?: any
504+ textStyle?: TextStyle
502505 ): Promise<SendMessageResponse> {
503506 const user = this.getCurrentUser();
504507 if (!user) {
@@ -514,14 +517,10 @@ class ChatState {
514517 content,
515518 type,
516519 userId: user.id,
517- chatRoomId: DEFAULT_CHAT_ROOM_ID
520+ chatRoomId: DEFAULT_CHAT_ROOM_ID,
521+ styleData: textStyle ? JSON.stringify(textStyle) : undefined
518522 };
519523
520- // Add styleData if textStyle is provided
521- if (textStyle) {
522- (payload as any).styleData = JSON.stringify(textStyle);
523- }
524-
525524 console.debug('Sending message with payload:', payload);
526525
527526 const response = await fetch('/api/chat/messages', {
@@ -5,6 +5,7 @@ import type {
5 SendMessageResponse,5 SendMessageResponse,
6 GetMessagesResponse6 GetMessagesResponse
7 } from '../types/payloads';7 } from '../types/payloads';
8+import type { TextStyle } from '../types/text-formatting';
8 import { invalidate } from '$app/navigation';9 import { invalidate } from '$app/navigation';
9 import { env } from '$env/dynamic/public';10 import { env } from '$env/dynamic/public';
10 11
@@ -157,6 +158,7 @@ class ChatState {
157 158
158 console.debug('Fetched room buddy list:', data.buddyList);159 console.debug('Fetched room buddy list:', data.buddyList);
159 // Merge fetched buddy list with existing cached users to preserve user details even if offline160 // Merge fetched buddy list with existing cached users to preserve user details even if offline
161+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- temporary Map for merging, not reactive state
160 const mergedUsers = new Map<string, (typeof data.buddyList)[0]>();162 const mergedUsers = new Map<string, (typeof data.buddyList)[0]>();
161 // Add existing cached users163 // Add existing cached users
162 Object.values(this.userCache).forEach((user) => mergedUsers.set(user.id, user));164 Object.values(this.userCache).forEach((user) => mergedUsers.set(user.id, user));
@@ -429,6 +431,7 @@ class ChatState {
429 async initializeMessages() {431 async initializeMessages() {
430 try {432 try {
431 // Build the URL with appropriate parameters433 // Build the URL with appropriate parameters
434+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- URLSearchParams for URL building, not reactive state
432 const params = new URLSearchParams();435 const params = new URLSearchParams();
433 if (!this.currentUser) {436 if (!this.currentUser) {
434 params.append('public', 'true');437 params.append('public', 'true');
@@ -498,7 +501,7 @@ class ChatState {
498 async sendMessage(501 async sendMessage(
499 content: string,502 content: string,
500 type: Message['type'] = 'chat',503 type: Message['type'] = 'chat',
501- textStyle?: any504+ textStyle?: TextStyle
502 ): Promise<SendMessageResponse> {505 ): Promise<SendMessageResponse> {
503 const user = this.getCurrentUser();506 const user = this.getCurrentUser();
504 if (!user) {507 if (!user) {
@@ -514,14 +517,10 @@ class ChatState {
514 content,517 content,
515 type,518 type,
516 userId: user.id,519 userId: user.id,
517- chatRoomId: DEFAULT_CHAT_ROOM_ID520+ chatRoomId: DEFAULT_CHAT_ROOM_ID,
521+ styleData: textStyle ? JSON.stringify(textStyle) : undefined
518 };522 };
519 523
520- // Add styleData if textStyle is provided
521- if (textStyle) {
522- (payload as any).styleData = JSON.stringify(textStyle);
523- }
524-
525 console.debug('Sending message with payload:', payload);524 console.debug('Sending message with payload:', payload);
526 525
527 const response = await fetch('/api/chat/messages', {526 const response = await fetch('/api/chat/messages', {
M src/lib/utils/security-tests.ts +3 -3
@@ -175,7 +175,7 @@ async function runCSSInjectionTests(): Promise<SecurityTestResult[]> {
175175
176176 for (const vector of CSS_INJECTION_VECTORS) {
177177 try {
178- const testStyle = { ...DEFAULT_TEXT_STYLE, color: 'red' as any };
178+ const testStyle = { ...DEFAULT_TEXT_STYLE, color: 'red' };
179179 const output = await formatText(`Test with style`, testStyle, true);
180180 const vulnerabilities = detectVulnerabilities(output);
181181
@@ -324,7 +324,7 @@ export async function quickSecurityCheck(input: string): Promise<boolean> {
324324 const output = await formatText(input, DEFAULT_TEXT_STYLE, true);
325325 const vulnerabilities = detectVulnerabilities(output);
326326 return vulnerabilities.length === 0;
327- } catch (error) {
327+ } catch {
328328 // Errors on malicious input are acceptable
329329 return true;
330330 }
@@ -340,7 +340,7 @@ export function testStyleValidation(): boolean {
340340 // Invalid styles (should be sanitized)
341341 { fontFamily: 'malicious-font', fontSize: 999, color: 'invalid-color' },
342342 { fontFamily: 'tahoma', fontSize: -10, color: 'red' },
343- { fontFamily: 'tahoma', fontSize: 'invalid' as any, color: 'red' }
343+ { fontFamily: 'tahoma', fontSize: 'invalid' as unknown as number, color: 'red' }
344344 ];
345345
346346 for (const testCase of testCases) {
@@ -175,7 +175,7 @@ async function runCSSInjectionTests(): Promise<SecurityTestResult[]> {
175 175
176 for (const vector of CSS_INJECTION_VECTORS) {176 for (const vector of CSS_INJECTION_VECTORS) {
177 try {177 try {
178- const testStyle = { ...DEFAULT_TEXT_STYLE, color: 'red' as any };178+ const testStyle = { ...DEFAULT_TEXT_STYLE, color: 'red' };
179 const output = await formatText(`Test with style`, testStyle, true);179 const output = await formatText(`Test with style`, testStyle, true);
180 const vulnerabilities = detectVulnerabilities(output);180 const vulnerabilities = detectVulnerabilities(output);
181 181
@@ -324,7 +324,7 @@ export async function quickSecurityCheck(input: string): Promise<boolean> {
324 const output = await formatText(input, DEFAULT_TEXT_STYLE, true);324 const output = await formatText(input, DEFAULT_TEXT_STYLE, true);
325 const vulnerabilities = detectVulnerabilities(output);325 const vulnerabilities = detectVulnerabilities(output);
326 return vulnerabilities.length === 0;326 return vulnerabilities.length === 0;
327- } catch (error) {327+ } catch {
328 // Errors on malicious input are acceptable328 // Errors on malicious input are acceptable
329 return true;329 return true;
330 }330 }
@@ -340,7 +340,7 @@ export function testStyleValidation(): boolean {
340 // Invalid styles (should be sanitized)340 // Invalid styles (should be sanitized)
341 { fontFamily: 'malicious-font', fontSize: 999, color: 'invalid-color' },341 { fontFamily: 'malicious-font', fontSize: 999, color: 'invalid-color' },
342 { fontFamily: 'tahoma', fontSize: -10, color: 'red' },342 { fontFamily: 'tahoma', fontSize: -10, color: 'red' },
343- { fontFamily: 'tahoma', fontSize: 'invalid' as any, color: 'red' }343+ { fontFamily: 'tahoma', fontSize: 'invalid' as unknown as number, color: 'red' }
344 ];344 ];
345 345
346 for (const testCase of testCases) {346 for (const testCase of testCases) {
M src/lib/utils/text-formatter.ts +3 -12
@@ -4,7 +4,7 @@ import remarkRehype from 'remark-rehype';
44 import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
55 import rehypeStringify from 'rehype-stringify';
66 import type { TextStyle } from '../types/text-formatting';
7-import { generateCSSStyle, validateTextStyle } from '../types/text-formatting';
7+import { validateTextStyle } from '../types/text-formatting';
88
99 // Define allowed HTML tags and attributes for security
1010 const ALLOWED_SCHEMA = {
@@ -25,15 +25,6 @@ const ALLOWED_SCHEMA = {
2525 }
2626 };
2727
28-// Custom rehype plugin to apply text styles
29-function rehypeApplyStyles(userStyle: TextStyle) {
30- return function (tree: any) {
31- // We don't need to apply styles here anymore
32- // The formatted-message component handles all styling
33- return tree;
34- };
35-}
36-
3728 // BBCode-style formatting patterns
3829 const BB_CODE_PATTERNS: Array<{
3930 pattern: RegExp;
@@ -130,8 +121,8 @@ export async function formatText(
130121 // If text is empty, return empty string
131122 if (!text) return '';
132123
133- // Validate and apply user style
134- const validatedStyle = validateTextStyle(userStyle || {});
124+ // Validate user style (ensures safe values)
125+ validateTextStyle(userStyle || {});
135126
136127 // Preprocess the text for formatting if allowed
137128 let processedText = text;
@@ -4,7 +4,7 @@ import remarkRehype from 'remark-rehype';
4 import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';4 import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
5 import rehypeStringify from 'rehype-stringify';5 import rehypeStringify from 'rehype-stringify';
6 import type { TextStyle } from '../types/text-formatting';6 import type { TextStyle } from '../types/text-formatting';
7-import { generateCSSStyle, validateTextStyle } from '../types/text-formatting';7+import { validateTextStyle } from '../types/text-formatting';
8 8
9 // Define allowed HTML tags and attributes for security9 // Define allowed HTML tags and attributes for security
10 const ALLOWED_SCHEMA = {10 const ALLOWED_SCHEMA = {
@@ -25,15 +25,6 @@ const ALLOWED_SCHEMA = {
25 }25 }
26 };26 };
27 27
28-// Custom rehype plugin to apply text styles
29-function rehypeApplyStyles(userStyle: TextStyle) {
30- return function (tree: any) {
31- // We don't need to apply styles here anymore
32- // The formatted-message component handles all styling
33- return tree;
34- };
35-}
36-
37 // BBCode-style formatting patterns28 // BBCode-style formatting patterns
38 const BB_CODE_PATTERNS: Array<{29 const BB_CODE_PATTERNS: Array<{
39 pattern: RegExp;30 pattern: RegExp;
@@ -130,8 +121,8 @@ export async function formatText(
130 // If text is empty, return empty string121 // If text is empty, return empty string
131 if (!text) return '';122 if (!text) return '';
132 123
133- // Validate and apply user style124+ // Validate user style (ensures safe values)
134- const validatedStyle = validateTextStyle(userStyle || {});125+ validateTextStyle(userStyle || {});
135 126
136 // Preprocess the text for formatting if allowed127 // Preprocess the text for formatting if allowed
137 let processedText = text;128 let processedText = text;
M src/lib/validation/password.ts +2 -2
@@ -58,7 +58,7 @@ export function createPasswordSchema(
5858
5959 if (constraints.requireSpecialChars) {
6060 schema = schema.regex(
61- /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/,
61+ /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/,
6262 PASSWORD_ERROR_MESSAGES.requireSpecialChars
6363 );
6464 }
@@ -169,7 +169,7 @@ export function validatePasswordStrength(
169169 }
170170
171171 if (constraints.requireSpecialChars) {
172- const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);
172+ const hasSpecialChars = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
173173 if (!hasSpecialChars) errors.push(PASSWORD_ERROR_MESSAGES.requireSpecialChars);
174174 requirements.push({
175175 met: hasSpecialChars,
@@ -58,7 +58,7 @@ export function createPasswordSchema(
58 58
59 if (constraints.requireSpecialChars) {59 if (constraints.requireSpecialChars) {
60 schema = schema.regex(60 schema = schema.regex(
61- /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/,61+ /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/,
62 PASSWORD_ERROR_MESSAGES.requireSpecialChars62 PASSWORD_ERROR_MESSAGES.requireSpecialChars
63 );63 );
64 }64 }
@@ -169,7 +169,7 @@ export function validatePasswordStrength(
169 }169 }
170 170
171 if (constraints.requireSpecialChars) {171 if (constraints.requireSpecialChars) {
172- const hasSpecialChars = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password);172+ const hasSpecialChars = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password);
173 if (!hasSpecialChars) errors.push(PASSWORD_ERROR_MESSAGES.requireSpecialChars);173 if (!hasSpecialChars) errors.push(PASSWORD_ERROR_MESSAGES.requireSpecialChars);
174 requirements.push({174 requirements.push({
175 met: hasSpecialChars,175 met: hasSpecialChars,
M src/routes/+layout.svelte +1 -1
@@ -1,7 +1,7 @@
11 <script lang="ts">
22 import '../app.css';
33 import { chatState } from '$lib/states/chat.svelte';
4- import { onMount, onDestroy } from 'svelte';
4+ import { onMount } from 'svelte';
55 import { browser } from '$app/environment';
66
77 let { children, data } = $props();
@@ -1,7 +1,7 @@
1 <script lang="ts">1 <script lang="ts">
2 import '../app.css';2 import '../app.css';
3 import { chatState } from '$lib/states/chat.svelte';3 import { chatState } from '$lib/states/chat.svelte';
4- import { onMount, onDestroy } from 'svelte';4+ import { onMount } from 'svelte';
5 import { browser } from '$app/environment';5 import { browser } from '$app/environment';
6 6
7 let { children, data } = $props();7 let { children, data } = $props();
M src/routes/+page.server.ts +1 -1
@@ -1,7 +1,7 @@
11 import type { PageServerLoad } from './$types';
22 import { validateSessionToken } from '$lib/api/session.server';
33 import db from '$lib/db/db.server';
4-import { userTextPreferences, users, messages } from '$lib/db/schema';
4+import { messages } from '$lib/db/schema';
55 import { eq, desc } from 'drizzle-orm';
66 import { DEFAULT_TEXT_STYLE, type TextStyle } from '$lib/types/text-formatting';
77
@@ -1,7 +1,7 @@
1 import type { PageServerLoad } from './$types';1 import type { PageServerLoad } from './$types';
2 import { validateSessionToken } from '$lib/api/session.server';2 import { validateSessionToken } from '$lib/api/session.server';
3 import db from '$lib/db/db.server';3 import db from '$lib/db/db.server';
4-import { userTextPreferences, users, messages } from '$lib/db/schema';4+import { messages } from '$lib/db/schema';
5 import { eq, desc } from 'drizzle-orm';5 import { eq, desc } from 'drizzle-orm';
6 import { DEFAULT_TEXT_STYLE, type TextStyle } from '$lib/types/text-formatting';6 import { DEFAULT_TEXT_STYLE, type TextStyle } from '$lib/types/text-formatting';
7 7
M src/routes/api/register/+server.ts +3 -3
@@ -8,7 +8,6 @@ import { createLogger } from '$lib/utils/logger.server';
88 import { eq } from 'drizzle-orm/sql';
99
1010 const log = createLogger('register-server');
11-const isDev = process.env.NODE_ENV === 'development';
1211
1312 // In-memory map to track failed captcha attempts per IP
1413 const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>();
@@ -74,8 +73,9 @@ export const POST: RequestHandler = async ({ request }) => {
7473 captchaAnswer,
7574 turnstileToken
7675 });
77- } catch (err: any) {
78- const errorMessage = err.errors?.[0]?.message || 'Invalid input data';
76+ } catch (err: unknown) {
77+ const zodError = err as { errors?: Array<{ message?: string }> };
78+ const errorMessage = zodError.errors?.[0]?.message || 'Invalid input data';
7979 log.warn('Registration validation failed', { error: errorMessage });
8080 return new Response(JSON.stringify({ error: errorMessage } as RegisterResponseError), {
8181 status: 400
@@ -8,7 +8,6 @@ import { createLogger } from '$lib/utils/logger.server';
8 import { eq } from 'drizzle-orm/sql';8 import { eq } from 'drizzle-orm/sql';
9 9
10 const log = createLogger('register-server');10 const log = createLogger('register-server');
11-const isDev = process.env.NODE_ENV === 'development';
12 11
13 // In-memory map to track failed captcha attempts per IP12 // In-memory map to track failed captcha attempts per IP
14 const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>();13 const captchaAttempts = new Map<string, { count: number; lastAttempt: number }>();
@@ -74,8 +73,9 @@ export const POST: RequestHandler = async ({ request }) => {
74 captchaAnswer,73 captchaAnswer,
75 turnstileToken74 turnstileToken
76 });75 });
77- } catch (err: any) {76+ } catch (err: unknown) {
78- const errorMessage = err.errors?.[0]?.message || 'Invalid input data';77+ const zodError = err as { errors?: Array<{ message?: string }> };
78+ const errorMessage = zodError.errors?.[0]?.message || 'Invalid input data';
79 log.warn('Registration validation failed', { error: errorMessage });79 log.warn('Registration validation failed', { error: errorMessage });
80 return new Response(JSON.stringify({ error: errorMessage } as RegisterResponseError), {80 return new Response(JSON.stringify({ error: errorMessage } as RegisterResponseError), {
81 status: 40081 status: 400
M src/routes/api/sse/+server.ts +0 -1
@@ -1,6 +1,5 @@
11 // At the top of the file, before any code
22 declare global {
3- // eslint-disable-next-line no-var
43 var __sseIntervalsStarted: boolean;
54 }
65
@@ -1,6 +1,5 @@
1 // At the top of the file, before any code1 // At the top of the file, before any code
2 declare global {2 declare global {
3- // eslint-disable-next-line no-var
4 var __sseIntervalsStarted: boolean;3 var __sseIntervalsStarted: boolean;
5 }4 }
6 5

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