feat: enhance authentication UI with improved login component and retro styling
- commit
- bf7e125c6d512952cf7b2251faa810fd4e54e483
- author
- Kit · · 2025-07-03 19:55 UTC
- parent
- 7d98a0f
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
5 files · +1073 -61
M src/app.css +445 -0
| @@ -2,4 +2,449 @@ | |||
| 2 | 2 | @import 'tailwindcss/components'; | |
| 3 | 3 | @import 'tailwindcss/utilities'; | |
| 4 | 4 | @import 'xp.css'; | |
| 5 | + | @import './lib/styles/fonts.css'; | |
| 6 | + | ||
| 7 | + | /* Local font faces with 30-day cache */ | |
| 8 | + | @font-face { | |
| 9 | + | font-family: 'Arial'; | |
| 10 | + | src: url('/fonts/Arial.TTF') format('truetype'); | |
| 11 | + | font-display: swap; | |
| 12 | + | font-weight: normal; | |
| 13 | + | font-style: normal; | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | @font-face { | |
| 17 | + | font-family: 'Arial'; | |
| 18 | + | src: url('/fonts/Arialbd.TTF') format('truetype'); | |
| 19 | + | font-display: swap; | |
| 20 | + | font-weight: bold; | |
| 21 | + | font-style: normal; | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | @font-face { | |
| 25 | + | font-family: 'Arial'; | |
| 26 | + | src: url('/fonts/Ariali.TTF') format('truetype'); | |
| 27 | + | font-display: swap; | |
| 28 | + | font-weight: normal; | |
| 29 | + | font-style: italic; | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | @font-face { | |
| 33 | + | font-family: 'Arial'; | |
| 34 | + | src: url('/fonts/Arialbi.TTF') format('truetype'); | |
| 35 | + | font-display: swap; | |
| 36 | + | font-weight: bold; | |
| 37 | + | font-style: italic; | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | @font-face { | |
| 41 | + | font-family: 'Tahoma'; | |
| 42 | + | src: url('/fonts/tahoma.ttf') format('truetype'); | |
| 43 | + | font-display: swap; | |
| 44 | + | font-weight: 80; | |
| 45 | + | font-style: normal; | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | @font-face { | |
| 49 | + | font-family: 'Tahoma'; | |
| 50 | + | src: url('/fonts/tahomabd.ttf') format('truetype'); | |
| 51 | + | font-display: swap; | |
| 52 | + | font-weight: 200; | |
| 53 | + | font-style: normal; | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | @font-face { | |
| 57 | + | font-family: 'Verdana'; | |
| 58 | + | src: url('/fonts/Verdana.TTF') format('truetype'); | |
| 59 | + | font-display: swap; | |
| 60 | + | font-weight: normal; | |
| 61 | + | font-style: normal; | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | @font-face { | |
| 65 | + | font-family: 'Verdana'; | |
| 66 | + | src: url('/fonts/Verdanab.TTF') format('truetype'); | |
| 67 | + | font-display: swap; | |
| 68 | + | font-weight: bold; | |
| 69 | + | font-style: normal; | |
| 70 | + | } | |
| 71 | + | ||
| 72 | + | @font-face { | |
| 73 | + | font-family: 'Verdana'; | |
| 74 | + | src: url('/fonts/Verdanai.TTF') format('truetype'); | |
| 75 | + | font-display: swap; | |
| 76 | + | font-weight: normal; | |
| 77 | + | font-style: italic; | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | @font-face { | |
| 81 | + | font-family: 'Verdana'; | |
| 82 | + | src: url('/fonts/Verdanaz.TTF') format('truetype'); | |
| 83 | + | font-display: swap; | |
| 84 | + | font-weight: bold; | |
| 85 | + | font-style: italic; | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | @font-face { | |
| 89 | + | font-family: 'Comic Sans MS'; | |
| 90 | + | src: url('/fonts/Comic.TTF') format('truetype'); | |
| 91 | + | font-display: swap; | |
| 92 | + | font-weight: normal; | |
| 93 | + | font-style: normal; | |
| 94 | + | } | |
| 95 | + | ||
| 96 | + | @font-face { | |
| 97 | + | font-family: 'Comic Sans MS'; | |
| 98 | + | src: url('/fonts/Comicbd.TTF') format('truetype'); | |
| 99 | + | font-display: swap; | |
| 100 | + | font-weight: bold; | |
| 101 | + | font-style: normal; | |
| 102 | + | } | |
| 103 | + | ||
| 104 | + | @font-face { | |
| 105 | + | font-family: 'Times New Roman'; | |
| 106 | + | src: url('/fonts/Times.TTF') format('truetype'); | |
| 107 | + | font-display: swap; | |
| 108 | + | font-weight: normal; | |
| 109 | + | font-style: normal; | |
| 110 | + | } | |
| 111 | + | ||
| 112 | + | @font-face { | |
| 113 | + | font-family: 'Times New Roman'; | |
| 114 | + | src: url('/fonts/Timesbd.TTF') format('truetype'); | |
| 115 | + | font-display: swap; | |
| 116 | + | font-weight: bold; | |
| 117 | + | font-style: normal; | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | @font-face { | |
| 121 | + | font-family: 'Times New Roman'; | |
| 122 | + | src: url('/fonts/Timesi.TTF') format('truetype'); | |
| 123 | + | font-display: swap; | |
| 124 | + | font-weight: normal; | |
| 125 | + | font-style: italic; | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | @font-face { | |
| 129 | + | font-family: 'Times New Roman'; | |
| 130 | + | src: url('/fonts/Timesbi.TTF') format('truetype'); | |
| 131 | + | font-display: swap; | |
| 132 | + | font-weight: bold; | |
| 133 | + | font-style: italic; | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | @font-face { | |
| 137 | + | font-family: 'Courier New'; | |
| 138 | + | src: url('/fonts/LiberationMono-Regular.ttf') format('truetype'); | |
| 139 | + | font-display: swap; | |
| 140 | + | font-weight: normal; | |
| 141 | + | font-style: normal; | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | @font-face { | |
| 145 | + | font-family: 'Courier New'; | |
| 146 | + | src: url('/fonts/LiberationMono-Bold.ttf') format('truetype'); | |
| 147 | + | font-display: swap; | |
| 148 | + | font-weight: bold; | |
| 149 | + | font-style: normal; | |
| 150 | + | } | |
| 151 | + | ||
| 152 | + | @font-face { | |
| 153 | + | font-family: 'Courier New'; | |
| 154 | + | src: url('/fonts/LiberationMono-Italic.ttf') format('truetype'); | |
| 155 | + | font-display: swap; | |
| 156 | + | font-weight: normal; | |
| 157 | + | font-style: italic; | |
| 158 | + | } | |
| 159 | + | ||
| 160 | + | @font-face { | |
| 161 | + | font-family: 'Courier New'; | |
| 162 | + | src: url('/fonts/LiberationMono-BoldItalic.ttf') format('truetype'); | |
| 163 | + | font-display: swap; | |
| 164 | + | font-weight: bold; | |
| 165 | + | font-style: italic; | |
| 166 | + | } | |
| 167 | + | ||
| 168 | + | /* ===== RETRO FONT STYLING ===== */ | |
| 169 | + | /* Retro Windows fonts with proper fallbacks */ | |
| 170 | + | .retro-font-pixelated { | |
| 171 | + | font-family: "Pixelated MS Sans Serif", "Tahoma", Geneva, sans-serif; | |
| 172 | + | } | |
| 173 | + | ||
| 174 | + | .retro-font-tahoma { | |
| 175 | + | font-family: Tahoma, "Pixelated MS Sans Serif", Geneva, sans-serif; | |
| 176 | + | } | |
| 177 | + | ||
| 178 | + | .retro-font-verdana { | |
| 179 | + | font-family: Verdana, "Pixelated MS Sans Serif", Geneva, sans-serif; | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | .retro-font-comicSans { | |
| 183 | + | font-family: "Comic Sans MS", "Pixelated MS Sans Serif", cursive, sans-serif; | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | .retro-font-timesNewRoman { | |
| 187 | + | font-family: "Times New Roman", Times, serif; | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | .retro-font-arial { | |
| 191 | + | font-family: Arial, "Pixelated MS Sans Serif", Helvetica, sans-serif; | |
| 192 | + | } | |
| 193 | + | ||
| 194 | + | .retro-font-courierNew { | |
| 195 | + | font-family: "Courier New", "Perfect DOS VGA 437 Win", Courier, monospace; | |
| 196 | + | } | |
| 197 | + | ||
| 198 | + | .retro-font-dos { | |
| 199 | + | font-family: "Perfect DOS VGA 437 Win", "Courier New", Courier, monospace; | |
| 200 | + | } | |
| 201 | + | ||
| 202 | + | /* ===== GRADIENT TEXT EFFECTS ===== */ | |
| 203 | + | .gradient-text { | |
| 204 | + | background-size: 200% 200%; | |
| 205 | + | -webkit-background-clip: text; | |
| 206 | + | -webkit-text-fill-color: transparent; | |
| 207 | + | background-clip: text; | |
| 208 | + | animation: gradientShift 3s ease infinite; | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | .gradient-text-static { | |
| 212 | + | /* Remove text-fill-color to allow inline color styles to work */ | |
| 213 | + | /* Individual spans will have their own colors set inline */ | |
| 214 | + | letter-spacing: normal; | |
| 215 | + | word-spacing: normal; | |
| 216 | + | } | |
| 217 | + | ||
| 218 | + | /* Gradient text container uses flexbox */ | |
| 219 | + | .gradient-text-static { | |
| 220 | + | display: inline-flex !important; | |
| 221 | + | flex-wrap: wrap !important; | |
| 222 | + | align-items: baseline !important; | |
| 223 | + | } | |
| 224 | + | ||
| 225 | + | /* Ensure gradient character spans don't add spacing */ | |
| 226 | + | .gradient-text-static > span { | |
| 227 | + | display: inline !important; | |
| 228 | + | margin: 0 !important; | |
| 229 | + | padding: 0 !important; | |
| 230 | + | letter-spacing: 0 !important; | |
| 231 | + | line-height: inherit !important; | |
| 232 | + | font-size: inherit !important; | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | /* Ensure gradient character colors from inline styles are respected */ | |
| 236 | + | .gradient-text-static .gradient-char { | |
| 237 | + | -webkit-text-fill-color: initial !important; | |
| 238 | + | } | |
| 239 | + | ||
| 240 | + | /* Gradient text in input fields - preserve normal input background */ | |
| 241 | + | input[style*="background-clip: text"] { | |
| 242 | + | /* Keep default input background while allowing gradient text */ | |
| 243 | + | } | |
| 244 | + | ||
| 245 | + | /* Gradient animation for AOL-style effects */ | |
| 246 | + | @keyframes gradientShift { | |
| 247 | + | 0% { background-position: 0% 50%; } | |
| 248 | + | 50% { background-position: 100% 50%; } | |
| 249 | + | 100% { background-position: 0% 50%; } | |
| 250 | + | } | |
| 251 | + | ||
| 252 | + | /* Specific gradient presets */ | |
| 253 | + | .gradient-rainbow { | |
| 254 | + | background: linear-gradient(45deg, | |
| 255 | + | #FF0000, #FF8000, #FFFF00, #80FF00, #00FF00, #00FF80, | |
| 256 | + | #00FFFF, #0080FF, #0000FF, #8000FF, #FF00FF, #FF0080); | |
| 257 | + | } | |
| 258 | + | ||
| 259 | + | .gradient-fire { | |
| 260 | + | background: linear-gradient(45deg, #FF0000, #FF4000, #FF8000, #FFBF00, #FFFF00); | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | .gradient-ocean { | |
| 264 | + | background: linear-gradient(45deg, #000080, #0040FF, #0080FF, #00BFFF, #00FFFF); | |
| 265 | + | } | |
| 266 | + | ||
| 267 | + | .gradient-sunset { | |
| 268 | + | background: linear-gradient(45deg, #FF4500, #FF6347, #FFD700, #FFFF00); | |
| 269 | + | } | |
| 270 | + | ||
| 271 | + | .gradient-neon { | |
| 272 | + | background: linear-gradient(45deg, #FF00FF, #8000FF, #0080FF, #00FFFF); | |
| 273 | + | } | |
| 274 | + | ||
| 275 | + | .gradient-forest { | |
| 276 | + | background: linear-gradient(45deg, #006400, #228B22, #32CD32, #90EE90); | |
| 277 | + | } | |
| 278 | + | ||
| 279 | + | .gradient-royal { | |
| 280 | + | background: linear-gradient(45deg, #4B0082, #6A5ACD, #9370DB, #DDA0DD); | |
| 281 | + | } | |
| 282 | + | ||
| 283 | + | /* ===== CHAT MESSAGE FORMATTING ===== */ | |
| 284 | + | .formatted-message { | |
| 285 | + | word-break: break-word; | |
| 286 | + | line-height: 1.4; | |
| 287 | + | max-width: 100%; | |
| 288 | + | } | |
| 289 | + | ||
| 290 | + | .formatted-message strong { | |
| 291 | + | font-weight: bold; | |
| 292 | + | } | |
| 293 | + | ||
| 294 | + | .formatted-message em { | |
| 295 | + | font-style: italic; | |
| 296 | + | } | |
| 297 | + | ||
| 298 | + | .formatted-message u { | |
| 299 | + | text-decoration: underline; | |
| 300 | + | } | |
| 301 | + | ||
| 302 | + | .formatted-message s { | |
| 303 | + | text-decoration: line-through; | |
| 304 | + | } | |
| 305 | + | ||
| 306 | + | .formatted-message code { | |
| 307 | + | background: #f0f0f0; | |
| 308 | + | padding: 1px 3px; | |
| 309 | + | border-radius: 2px; | |
| 310 | + | font-family: 'Courier New', Courier, monospace; | |
| 311 | + | font-size: 0.9em; | |
| 312 | + | } | |
| 313 | + | ||
| 314 | + | /* ===== INPUT STYLING FIXES ===== */ | |
| 315 | + | /* Fix letter spacing issues with gradient text in inputs */ | |
| 316 | + | input[style*="background-clip: text"] { | |
| 317 | + | letter-spacing: normal !important; | |
| 318 | + | word-spacing: normal !important; | |
| 319 | + | } | |
| 320 | + | ||
| 321 | + | /* Ensure gradient inputs don't have extra padding */ | |
| 322 | + | input.styled-input { | |
| 323 | + | letter-spacing: 0; | |
| 324 | + | word-spacing: 0; | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | /* Ensure proper font weights are used */ | |
| 328 | + | .retro-font-tahoma { | |
| 329 | + | font-weight: 80; | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | /* ===== TEXT STYLE SELECTOR STYLING ===== */ | |
| 333 | + | .text-formatting-toolbar { | |
| 334 | + | background: #f0f0f0; | |
| 335 | + | border: 2px inset #dfdfdf; | |
| 336 | + | padding: 4px; | |
| 337 | + | margin-bottom: 8px; | |
| 338 | + | display: flex; | |
| 339 | + | flex-wrap: wrap; | |
| 340 | + | gap: 4px; | |
| 341 | + | align-items: center; | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | .formatting-button { | |
| 345 | + | width: 24px; | |
| 346 | + | height: 24px; | |
| 347 | + | border: 1px outset #dfdfdf; | |
| 348 | + | background: #f0f0f0; | |
| 349 | + | cursor: pointer; | |
| 350 | + | display: flex; | |
| 351 | + | align-items: center; | |
| 352 | + | justify-content: center; | |
| 353 | + | font-size: 12px; | |
| 354 | + | font-weight: bold; | |
| 355 | + | transition: all 0.1s ease; | |
| 356 | + | } | |
| 357 | + | ||
| 358 | + | .formatting-button:hover { | |
| 359 | + | background: #e0e0e0; | |
| 360 | + | } | |
| 361 | + | ||
| 362 | + | .formatting-button:active, | |
| 363 | + | .formatting-button.active { | |
| 364 | + | border: 1px inset #dfdfdf; | |
| 365 | + | background: #d0d0d0; | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | .font-selector { | |
| 369 | + | padding: 2px 4px; | |
| 370 | + | border: 1px inset #dfdfdf; | |
| 371 | + | background: white; | |
| 372 | + | font-size: 11px; | |
| 373 | + | min-width: 80px; | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | .size-slider { | |
| 377 | + | width: 60px; | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | .color-indicator { | |
| 381 | + | width: 16px; | |
| 382 | + | height: 16px; | |
| 383 | + | border: 1px solid #000; | |
| 384 | + | cursor: pointer; | |
| 385 | + | display: inline-block; | |
| 386 | + | } | |
| 387 | + | ||
| 388 | + | /* ===== ACCESSIBILITY IMPROVEMENTS ===== */ | |
| 389 | + | @media (prefers-reduced-motion: reduce) { | |
| 390 | + | .gradient-text { | |
| 391 | + | animation: none; | |
| 392 | + | } | |
| 393 | + | } | |
| 394 | + | ||
| 395 | + | /* High contrast mode support */ | |
| 396 | + | @media (prefers-contrast: high) { | |
| 397 | + | .gradient-text { | |
| 398 | + | -webkit-text-fill-color: unset; | |
| 399 | + | background: none; | |
| 400 | + | color: inherit; | |
| 401 | + | } | |
| 402 | + | ||
| 403 | + | .gradient-text-static { | |
| 404 | + | -webkit-text-fill-color: unset; | |
| 405 | + | background: none; | |
| 406 | + | color: inherit; | |
| 407 | + | } | |
| 408 | + | } | |
| 409 | + | ||
| 410 | + | /* ===== RETRO STYLING ENHANCEMENTS ===== */ | |
| 411 | + | .retro-message { | |
| 412 | + | font-family: inherit; | |
| 413 | + | line-height: 1.3; | |
| 414 | + | word-wrap: break-word; | |
| 415 | + | } | |
| 416 | + | ||
| 417 | + | .retro-message.style-bold { | |
| 418 | + | font-weight: bold; | |
| 419 | + | } | |
| 420 | + | ||
| 421 | + | .retro-message.style-italic { | |
| 422 | + | font-style: italic; | |
| 423 | + | } | |
| 424 | + | ||
| 425 | + | .retro-message.style-underline { | |
| 426 | + | text-decoration: underline; | |
| 427 | + | } | |
| 428 | + | ||
| 429 | + | .retro-message.style-strikethrough { | |
| 430 | + | text-decoration: line-through; | |
| 431 | + | } | |
| 432 | + | ||
| 433 | + | .retro-message.style-underline.style-strikethrough { | |
| 434 | + | text-decoration: underline line-through; | |
| 435 | + | } | |
| 436 | + | ||
| 437 | + | /* ===== PERFORMANCE OPTIMIZATIONS ===== */ | |
| 438 | + | .gradient-text, | |
| 439 | + | .gradient-text-static { | |
| 440 | + | will-change: background-position; | |
| 441 | + | contain: layout style paint; | |
| 442 | + | } | |
| 443 | + | ||
| 444 | + | /* Limit gradient text length for performance */ | |
| 445 | + | .gradient-text-long { | |
| 446 | + | max-width: 500px; | |
| 447 | + | overflow: hidden; | |
| 448 | + | text-overflow: ellipsis; | |
| 449 | + | } | |
| 5 | 450 | ||
| @@ -2,4 +2,449 @@ | |||||
| 2 | @import 'tailwindcss/components'; | 2 | @import 'tailwindcss/components'; | ||
| 3 | @import 'tailwindcss/utilities'; | 3 | @import 'tailwindcss/utilities'; | ||
| 4 | @import 'xp.css'; | 4 | @import 'xp.css'; | ||
| 5 | + | @import './lib/styles/fonts.css'; | |||
| 6 | + | ||||
| 7 | + | /* Local font faces with 30-day cache */ | |||
| 8 | + | @font-face { | |||
| 9 | + | font-family: 'Arial'; | |||
| 10 | + | src: url('/fonts/Arial.TTF') format('truetype'); | |||
| 11 | + | font-display: swap; | |||
| 12 | + | font-weight: normal; | |||
| 13 | + | font-style: normal; | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | @font-face { | |||
| 17 | + | font-family: 'Arial'; | |||
| 18 | + | src: url('/fonts/Arialbd.TTF') format('truetype'); | |||
| 19 | + | font-display: swap; | |||
| 20 | + | font-weight: bold; | |||
| 21 | + | font-style: normal; | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | @font-face { | |||
| 25 | + | font-family: 'Arial'; | |||
| 26 | + | src: url('/fonts/Ariali.TTF') format('truetype'); | |||
| 27 | + | font-display: swap; | |||
| 28 | + | font-weight: normal; | |||
| 29 | + | font-style: italic; | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | @font-face { | |||
| 33 | + | font-family: 'Arial'; | |||
| 34 | + | src: url('/fonts/Arialbi.TTF') format('truetype'); | |||
| 35 | + | font-display: swap; | |||
| 36 | + | font-weight: bold; | |||
| 37 | + | font-style: italic; | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | @font-face { | |||
| 41 | + | font-family: 'Tahoma'; | |||
| 42 | + | src: url('/fonts/tahoma.ttf') format('truetype'); | |||
| 43 | + | font-display: swap; | |||
| 44 | + | font-weight: 80; | |||
| 45 | + | font-style: normal; | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | @font-face { | |||
| 49 | + | font-family: 'Tahoma'; | |||
| 50 | + | src: url('/fonts/tahomabd.ttf') format('truetype'); | |||
| 51 | + | font-display: swap; | |||
| 52 | + | font-weight: 200; | |||
| 53 | + | font-style: normal; | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | @font-face { | |||
| 57 | + | font-family: 'Verdana'; | |||
| 58 | + | src: url('/fonts/Verdana.TTF') format('truetype'); | |||
| 59 | + | font-display: swap; | |||
| 60 | + | font-weight: normal; | |||
| 61 | + | font-style: normal; | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | @font-face { | |||
| 65 | + | font-family: 'Verdana'; | |||
| 66 | + | src: url('/fonts/Verdanab.TTF') format('truetype'); | |||
| 67 | + | font-display: swap; | |||
| 68 | + | font-weight: bold; | |||
| 69 | + | font-style: normal; | |||
| 70 | + | } | |||
| 71 | + | ||||
| 72 | + | @font-face { | |||
| 73 | + | font-family: 'Verdana'; | |||
| 74 | + | src: url('/fonts/Verdanai.TTF') format('truetype'); | |||
| 75 | + | font-display: swap; | |||
| 76 | + | font-weight: normal; | |||
| 77 | + | font-style: italic; | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | @font-face { | |||
| 81 | + | font-family: 'Verdana'; | |||
| 82 | + | src: url('/fonts/Verdanaz.TTF') format('truetype'); | |||
| 83 | + | font-display: swap; | |||
| 84 | + | font-weight: bold; | |||
| 85 | + | font-style: italic; | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | @font-face { | |||
| 89 | + | font-family: 'Comic Sans MS'; | |||
| 90 | + | src: url('/fonts/Comic.TTF') format('truetype'); | |||
| 91 | + | font-display: swap; | |||
| 92 | + | font-weight: normal; | |||
| 93 | + | font-style: normal; | |||
| 94 | + | } | |||
| 95 | + | ||||
| 96 | + | @font-face { | |||
| 97 | + | font-family: 'Comic Sans MS'; | |||
| 98 | + | src: url('/fonts/Comicbd.TTF') format('truetype'); | |||
| 99 | + | font-display: swap; | |||
| 100 | + | font-weight: bold; | |||
| 101 | + | font-style: normal; | |||
| 102 | + | } | |||
| 103 | + | ||||
| 104 | + | @font-face { | |||
| 105 | + | font-family: 'Times New Roman'; | |||
| 106 | + | src: url('/fonts/Times.TTF') format('truetype'); | |||
| 107 | + | font-display: swap; | |||
| 108 | + | font-weight: normal; | |||
| 109 | + | font-style: normal; | |||
| 110 | + | } | |||
| 111 | + | ||||
| 112 | + | @font-face { | |||
| 113 | + | font-family: 'Times New Roman'; | |||
| 114 | + | src: url('/fonts/Timesbd.TTF') format('truetype'); | |||
| 115 | + | font-display: swap; | |||
| 116 | + | font-weight: bold; | |||
| 117 | + | font-style: normal; | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | @font-face { | |||
| 121 | + | font-family: 'Times New Roman'; | |||
| 122 | + | src: url('/fonts/Timesi.TTF') format('truetype'); | |||
| 123 | + | font-display: swap; | |||
| 124 | + | font-weight: normal; | |||
| 125 | + | font-style: italic; | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | @font-face { | |||
| 129 | + | font-family: 'Times New Roman'; | |||
| 130 | + | src: url('/fonts/Timesbi.TTF') format('truetype'); | |||
| 131 | + | font-display: swap; | |||
| 132 | + | font-weight: bold; | |||
| 133 | + | font-style: italic; | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | @font-face { | |||
| 137 | + | font-family: 'Courier New'; | |||
| 138 | + | src: url('/fonts/LiberationMono-Regular.ttf') format('truetype'); | |||
| 139 | + | font-display: swap; | |||
| 140 | + | font-weight: normal; | |||
| 141 | + | font-style: normal; | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | @font-face { | |||
| 145 | + | font-family: 'Courier New'; | |||
| 146 | + | src: url('/fonts/LiberationMono-Bold.ttf') format('truetype'); | |||
| 147 | + | font-display: swap; | |||
| 148 | + | font-weight: bold; | |||
| 149 | + | font-style: normal; | |||
| 150 | + | } | |||
| 151 | + | ||||
| 152 | + | @font-face { | |||
| 153 | + | font-family: 'Courier New'; | |||
| 154 | + | src: url('/fonts/LiberationMono-Italic.ttf') format('truetype'); | |||
| 155 | + | font-display: swap; | |||
| 156 | + | font-weight: normal; | |||
| 157 | + | font-style: italic; | |||
| 158 | + | } | |||
| 159 | + | ||||
| 160 | + | @font-face { | |||
| 161 | + | font-family: 'Courier New'; | |||
| 162 | + | src: url('/fonts/LiberationMono-BoldItalic.ttf') format('truetype'); | |||
| 163 | + | font-display: swap; | |||
| 164 | + | font-weight: bold; | |||
| 165 | + | font-style: italic; | |||
| 166 | + | } | |||
| 167 | + | ||||
| 168 | + | /* ===== RETRO FONT STYLING ===== */ | |||
| 169 | + | /* Retro Windows fonts with proper fallbacks */ | |||
| 170 | + | .retro-font-pixelated { | |||
| 171 | + | font-family: "Pixelated MS Sans Serif", "Tahoma", Geneva, sans-serif; | |||
| 172 | + | } | |||
| 173 | + | ||||
| 174 | + | .retro-font-tahoma { | |||
| 175 | + | font-family: Tahoma, "Pixelated MS Sans Serif", Geneva, sans-serif; | |||
| 176 | + | } | |||
| 177 | + | ||||
| 178 | + | .retro-font-verdana { | |||
| 179 | + | font-family: Verdana, "Pixelated MS Sans Serif", Geneva, sans-serif; | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | .retro-font-comicSans { | |||
| 183 | + | font-family: "Comic Sans MS", "Pixelated MS Sans Serif", cursive, sans-serif; | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | .retro-font-timesNewRoman { | |||
| 187 | + | font-family: "Times New Roman", Times, serif; | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | .retro-font-arial { | |||
| 191 | + | font-family: Arial, "Pixelated MS Sans Serif", Helvetica, sans-serif; | |||
| 192 | + | } | |||
| 193 | + | ||||
| 194 | + | .retro-font-courierNew { | |||
| 195 | + | font-family: "Courier New", "Perfect DOS VGA 437 Win", Courier, monospace; | |||
| 196 | + | } | |||
| 197 | + | ||||
| 198 | + | .retro-font-dos { | |||
| 199 | + | font-family: "Perfect DOS VGA 437 Win", "Courier New", Courier, monospace; | |||
| 200 | + | } | |||
| 201 | + | ||||
| 202 | + | /* ===== GRADIENT TEXT EFFECTS ===== */ | |||
| 203 | + | .gradient-text { | |||
| 204 | + | background-size: 200% 200%; | |||
| 205 | + | -webkit-background-clip: text; | |||
| 206 | + | -webkit-text-fill-color: transparent; | |||
| 207 | + | background-clip: text; | |||
| 208 | + | animation: gradientShift 3s ease infinite; | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | .gradient-text-static { | |||
| 212 | + | /* Remove text-fill-color to allow inline color styles to work */ | |||
| 213 | + | /* Individual spans will have their own colors set inline */ | |||
| 214 | + | letter-spacing: normal; | |||
| 215 | + | word-spacing: normal; | |||
| 216 | + | } | |||
| 217 | + | ||||
| 218 | + | /* Gradient text container uses flexbox */ | |||
| 219 | + | .gradient-text-static { | |||
| 220 | + | display: inline-flex !important; | |||
| 221 | + | flex-wrap: wrap !important; | |||
| 222 | + | align-items: baseline !important; | |||
| 223 | + | } | |||
| 224 | + | ||||
| 225 | + | /* Ensure gradient character spans don't add spacing */ | |||
| 226 | + | .gradient-text-static > span { | |||
| 227 | + | display: inline !important; | |||
| 228 | + | margin: 0 !important; | |||
| 229 | + | padding: 0 !important; | |||
| 230 | + | letter-spacing: 0 !important; | |||
| 231 | + | line-height: inherit !important; | |||
| 232 | + | font-size: inherit !important; | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | /* Ensure gradient character colors from inline styles are respected */ | |||
| 236 | + | .gradient-text-static .gradient-char { | |||
| 237 | + | -webkit-text-fill-color: initial !important; | |||
| 238 | + | } | |||
| 239 | + | ||||
| 240 | + | /* Gradient text in input fields - preserve normal input background */ | |||
| 241 | + | input[style*="background-clip: text"] { | |||
| 242 | + | /* Keep default input background while allowing gradient text */ | |||
| 243 | + | } | |||
| 244 | + | ||||
| 245 | + | /* Gradient animation for AOL-style effects */ | |||
| 246 | + | @keyframes gradientShift { | |||
| 247 | + | 0% { background-position: 0% 50%; } | |||
| 248 | + | 50% { background-position: 100% 50%; } | |||
| 249 | + | 100% { background-position: 0% 50%; } | |||
| 250 | + | } | |||
| 251 | + | ||||
| 252 | + | /* Specific gradient presets */ | |||
| 253 | + | .gradient-rainbow { | |||
| 254 | + | background: linear-gradient(45deg, | |||
| 255 | + | #FF0000, #FF8000, #FFFF00, #80FF00, #00FF00, #00FF80, | |||
| 256 | + | #00FFFF, #0080FF, #0000FF, #8000FF, #FF00FF, #FF0080); | |||
| 257 | + | } | |||
| 258 | + | ||||
| 259 | + | .gradient-fire { | |||
| 260 | + | background: linear-gradient(45deg, #FF0000, #FF4000, #FF8000, #FFBF00, #FFFF00); | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | .gradient-ocean { | |||
| 264 | + | background: linear-gradient(45deg, #000080, #0040FF, #0080FF, #00BFFF, #00FFFF); | |||
| 265 | + | } | |||
| 266 | + | ||||
| 267 | + | .gradient-sunset { | |||
| 268 | + | background: linear-gradient(45deg, #FF4500, #FF6347, #FFD700, #FFFF00); | |||
| 269 | + | } | |||
| 270 | + | ||||
| 271 | + | .gradient-neon { | |||
| 272 | + | background: linear-gradient(45deg, #FF00FF, #8000FF, #0080FF, #00FFFF); | |||
| 273 | + | } | |||
| 274 | + | ||||
| 275 | + | .gradient-forest { | |||
| 276 | + | background: linear-gradient(45deg, #006400, #228B22, #32CD32, #90EE90); | |||
| 277 | + | } | |||
| 278 | + | ||||
| 279 | + | .gradient-royal { | |||
| 280 | + | background: linear-gradient(45deg, #4B0082, #6A5ACD, #9370DB, #DDA0DD); | |||
| 281 | + | } | |||
| 282 | + | ||||
| 283 | + | /* ===== CHAT MESSAGE FORMATTING ===== */ | |||
| 284 | + | .formatted-message { | |||
| 285 | + | word-break: break-word; | |||
| 286 | + | line-height: 1.4; | |||
| 287 | + | max-width: 100%; | |||
| 288 | + | } | |||
| 289 | + | ||||
| 290 | + | .formatted-message strong { | |||
| 291 | + | font-weight: bold; | |||
| 292 | + | } | |||
| 293 | + | ||||
| 294 | + | .formatted-message em { | |||
| 295 | + | font-style: italic; | |||
| 296 | + | } | |||
| 297 | + | ||||
| 298 | + | .formatted-message u { | |||
| 299 | + | text-decoration: underline; | |||
| 300 | + | } | |||
| 301 | + | ||||
| 302 | + | .formatted-message s { | |||
| 303 | + | text-decoration: line-through; | |||
| 304 | + | } | |||
| 305 | + | ||||
| 306 | + | .formatted-message code { | |||
| 307 | + | background: #f0f0f0; | |||
| 308 | + | padding: 1px 3px; | |||
| 309 | + | border-radius: 2px; | |||
| 310 | + | font-family: 'Courier New', Courier, monospace; | |||
| 311 | + | font-size: 0.9em; | |||
| 312 | + | } | |||
| 313 | + | ||||
| 314 | + | /* ===== INPUT STYLING FIXES ===== */ | |||
| 315 | + | /* Fix letter spacing issues with gradient text in inputs */ | |||
| 316 | + | input[style*="background-clip: text"] { | |||
| 317 | + | letter-spacing: normal !important; | |||
| 318 | + | word-spacing: normal !important; | |||
| 319 | + | } | |||
| 320 | + | ||||
| 321 | + | /* Ensure gradient inputs don't have extra padding */ | |||
| 322 | + | input.styled-input { | |||
| 323 | + | letter-spacing: 0; | |||
| 324 | + | word-spacing: 0; | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | /* Ensure proper font weights are used */ | |||
| 328 | + | .retro-font-tahoma { | |||
| 329 | + | font-weight: 80; | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | /* ===== TEXT STYLE SELECTOR STYLING ===== */ | |||
| 333 | + | .text-formatting-toolbar { | |||
| 334 | + | background: #f0f0f0; | |||
| 335 | + | border: 2px inset #dfdfdf; | |||
| 336 | + | padding: 4px; | |||
| 337 | + | margin-bottom: 8px; | |||
| 338 | + | display: flex; | |||
| 339 | + | flex-wrap: wrap; | |||
| 340 | + | gap: 4px; | |||
| 341 | + | align-items: center; | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | .formatting-button { | |||
| 345 | + | width: 24px; | |||
| 346 | + | height: 24px; | |||
| 347 | + | border: 1px outset #dfdfdf; | |||
| 348 | + | background: #f0f0f0; | |||
| 349 | + | cursor: pointer; | |||
| 350 | + | display: flex; | |||
| 351 | + | align-items: center; | |||
| 352 | + | justify-content: center; | |||
| 353 | + | font-size: 12px; | |||
| 354 | + | font-weight: bold; | |||
| 355 | + | transition: all 0.1s ease; | |||
| 356 | + | } | |||
| 357 | + | ||||
| 358 | + | .formatting-button:hover { | |||
| 359 | + | background: #e0e0e0; | |||
| 360 | + | } | |||
| 361 | + | ||||
| 362 | + | .formatting-button:active, | |||
| 363 | + | .formatting-button.active { | |||
| 364 | + | border: 1px inset #dfdfdf; | |||
| 365 | + | background: #d0d0d0; | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | .font-selector { | |||
| 369 | + | padding: 2px 4px; | |||
| 370 | + | border: 1px inset #dfdfdf; | |||
| 371 | + | background: white; | |||
| 372 | + | font-size: 11px; | |||
| 373 | + | min-width: 80px; | |||
| 374 | + | } | |||
| 375 | + | ||||
| 376 | + | .size-slider { | |||
| 377 | + | width: 60px; | |||
| 378 | + | } | |||
| 379 | + | ||||
| 380 | + | .color-indicator { | |||
| 381 | + | width: 16px; | |||
| 382 | + | height: 16px; | |||
| 383 | + | border: 1px solid #000; | |||
| 384 | + | cursor: pointer; | |||
| 385 | + | display: inline-block; | |||
| 386 | + | } | |||
| 387 | + | ||||
| 388 | + | /* ===== ACCESSIBILITY IMPROVEMENTS ===== */ | |||
| 389 | + | @media (prefers-reduced-motion: reduce) { | |||
| 390 | + | .gradient-text { | |||
| 391 | + | animation: none; | |||
| 392 | + | } | |||
| 393 | + | } | |||
| 394 | + | ||||
| 395 | + | /* High contrast mode support */ | |||
| 396 | + | @media (prefers-contrast: high) { | |||
| 397 | + | .gradient-text { | |||
| 398 | + | -webkit-text-fill-color: unset; | |||
| 399 | + | background: none; | |||
| 400 | + | color: inherit; | |||
| 401 | + | } | |||
| 402 | + | ||||
| 403 | + | .gradient-text-static { | |||
| 404 | + | -webkit-text-fill-color: unset; | |||
| 405 | + | background: none; | |||
| 406 | + | color: inherit; | |||
| 407 | + | } | |||
| 408 | + | } | |||
| 409 | + | ||||
| 410 | + | /* ===== RETRO STYLING ENHANCEMENTS ===== */ | |||
| 411 | + | .retro-message { | |||
| 412 | + | font-family: inherit; | |||
| 413 | + | line-height: 1.3; | |||
| 414 | + | word-wrap: break-word; | |||
| 415 | + | } | |||
| 416 | + | ||||
| 417 | + | .retro-message.style-bold { | |||
| 418 | + | font-weight: bold; | |||
| 419 | + | } | |||
| 420 | + | ||||
| 421 | + | .retro-message.style-italic { | |||
| 422 | + | font-style: italic; | |||
| 423 | + | } | |||
| 424 | + | ||||
| 425 | + | .retro-message.style-underline { | |||
| 426 | + | text-decoration: underline; | |||
| 427 | + | } | |||
| 428 | + | ||||
| 429 | + | .retro-message.style-strikethrough { | |||
| 430 | + | text-decoration: line-through; | |||
| 431 | + | } | |||
| 432 | + | ||||
| 433 | + | .retro-message.style-underline.style-strikethrough { | |||
| 434 | + | text-decoration: underline line-through; | |||
| 435 | + | } | |||
| 436 | + | ||||
| 437 | + | /* ===== PERFORMANCE OPTIMIZATIONS ===== */ | |||
| 438 | + | .gradient-text, | |||
| 439 | + | .gradient-text-static { | |||
| 440 | + | will-change: background-position; | |||
| 441 | + | contain: layout style paint; | |||
| 442 | + | } | |||
| 443 | + | ||||
| 444 | + | /* Limit gradient text length for performance */ | |||
| 445 | + | .gradient-text-long { | |||
| 446 | + | max-width: 500px; | |||
| 447 | + | overflow: hidden; | |||
| 448 | + | text-overflow: ellipsis; | |||
| 449 | + | } | |||
| 5 | 450 | ||||
M src/lib/components/aim-login.svelte +50 -45
| @@ -5,9 +5,10 @@ | |||
| 5 | 5 | import type { User, SafeUser } from '$lib/types/chat'; | |
| 6 | 6 | import { invalidateAll } from '$app/navigation'; | |
| 7 | 7 | import { chatState } from '$lib/states/chat.svelte'; | |
| 8 | - | import Turnstile from './turnstile.svelte'; | |
| 9 | 8 | import { createSafeUser } from '$lib/types/chat'; | |
| 10 | 9 | import { onMount } from 'svelte'; | |
| 10 | + | import PasswordStrengthIndicator from './password-strength-indicator.svelte'; | |
| 11 | + | import { loginSchema, createRegistrationSchema, validatePasswordStrength, DEFAULT_PASSWORD_CONSTRAINTS, type PasswordConstraints } from '../validation/password'; | |
| 11 | 12 | ||
| 12 | 13 | interface $$Props { | |
| 13 | 14 | showAuth: boolean; | |
| @@ -24,7 +25,6 @@ | |||
| 24 | 25 | let siPassword = $state(''); | |
| 25 | 26 | let error = $state(''); | |
| 26 | 27 | let loginStatus = $state('idle'); | |
| 27 | - | let siTurnstileToken = $state(''); | |
| 28 | 28 | ||
| 29 | 29 | // Sign Up state | |
| 30 | 30 | let suUsername = $state(''); | |
| @@ -35,6 +35,10 @@ | |||
| 35 | 35 | let signupStatus = $state('idle'); | |
| 36 | 36 | let suTurnstileToken = $state(''); | |
| 37 | 37 | ||
| 38 | + | // Password validation state | |
| 39 | + | let passwordConstraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS; | |
| 40 | + | let showPasswordStrength = $state(false); | |
| 41 | + | ||
| 38 | 42 | // Added dragging state and handler similar to chat-room | |
| 39 | 43 | let windowX = $state(0); | |
| 40 | 44 | let windowY = $state(0); | |
| @@ -60,8 +64,11 @@ | |||
| 60 | 64 | async function handleSigninSubmit() { | |
| 61 | 65 | error = ''; | |
| 62 | 66 | loginStatus = 'loading'; | |
| 63 | - | if (!siUsername || !siPassword || !siTurnstileToken) { | |
| 64 | - | error = 'Please complete all fields including the security check'; | |
| 67 | + | ||
| 68 | + | // Validate login data | |
| 69 | + | const validationError = validateLoginData(); | |
| 70 | + | if (validationError) { | |
| 71 | + | error = validationError; | |
| 65 | 72 | loginStatus = 'error'; | |
| 66 | 73 | return; | |
| 67 | 74 | } | |
| @@ -73,8 +80,7 @@ | |||
| 73 | 80 | headers: { 'Content-Type': 'application/json' }, | |
| 74 | 81 | body: JSON.stringify({ | |
| 75 | 82 | username: siUsername, | |
| 76 | - | password: siPassword, | |
| 77 | - | turnstileToken: siTurnstileToken | |
| 83 | + | password: siPassword | |
| 78 | 84 | }) | |
| 79 | 85 | }); | |
| 80 | 86 | ||
| @@ -107,41 +113,30 @@ | |||
| 107 | 113 | } | |
| 108 | 114 | } | |
| 109 | 115 | ||
| 110 | - | // New: Create a helper function for client-side validations based on server rules. | |
| 111 | - | function validateSignupData(): string | null { | |
| 112 | - | // Trim values | |
| 113 | - | const username = suUsername.trim(); | |
| 114 | - | const password = suPassword.trim(); | |
| 115 | - | const confirmPasswordValue = suConfirmPassword.trim(); | |
| 116 | - | ||
| 117 | - | if (!username || !password || !confirmPasswordValue) { | |
| 118 | - | return 'All fields must be filled'; | |
| 119 | - | } | |
| 120 | - | ||
| 121 | - | if (username.length < 3) { | |
| 122 | - | return 'Username must be at least 3 characters'; | |
| 123 | - | } | |
| 124 | - | ||
| 125 | - | if (username.length > 32) { | |
| 126 | - | return 'Username must be at most 32 characters'; | |
| 127 | - | } | |
| 128 | - | ||
| 129 | - | // Only allow letters, numbers, underscores, and dashes in the username. | |
| 130 | - | const usernameRegex = /^[a-zA-Z0-9_-]+$/; | |
| 131 | - | if (!usernameRegex.test(username)) { | |
| 132 | - | return 'Invalid username. Only letters, numbers, underscores, and dashes are allowed.'; | |
| 133 | - | } | |
| 134 | - | ||
| 135 | - | if (password !== confirmPasswordValue) { | |
| 136 | - | return 'Passwords do not match'; | |
| 116 | + | // Validation using Zod schemas | |
| 117 | + | function validateLoginData() { | |
| 118 | + | try { | |
| 119 | + | loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() }); | |
| 120 | + | return null; | |
| 121 | + | } catch (err: any) { | |
| 122 | + | return err.errors?.[0]?.message || 'Invalid input'; | |
| 137 | 123 | } | |
| 124 | + | } | |
| 138 | 125 | ||
| 139 | - | if (password.length < 8 || password.length > 64) { | |
| 140 | - | return 'Password must be between 8 and 64 characters'; | |
| 126 | + | function validateSignupData() { | |
| 127 | + | try { | |
| 128 | + | const registrationSchema = createRegistrationSchema(passwordConstraints); | |
| 129 | + | registrationSchema.parse({ | |
| 130 | + | suUsername: suUsername.trim(), | |
| 131 | + | suPassword: suPassword.trim(), | |
| 132 | + | suConfirmPassword: suConfirmPassword.trim(), | |
| 133 | + | captchaAnswer: captchaAnswer.trim(), | |
| 134 | + | turnstileToken: suTurnstileToken | |
| 135 | + | }); | |
| 136 | + | return null; | |
| 137 | + | } catch (err: any) { | |
| 138 | + | return err.errors?.[0]?.message || 'Invalid input'; | |
| 141 | 139 | } | |
| 142 | - | ||
| 143 | - | // All checks passed. | |
| 144 | - | return null; | |
| 145 | 140 | } | |
| 146 | 141 | ||
| 147 | 142 | async function handleSignupSubmit() { | |
| @@ -156,10 +151,9 @@ | |||
| 156 | 151 | return; | |
| 157 | 152 | } | |
| 158 | 153 | ||
| 159 | - | // Additional check for captcha and Turnstile token, | |
| 160 | - | // in case those fields are not filled. | |
| 161 | - | if (!captchaAnswer || !suTurnstileToken) { | |
| 162 | - | signupError = 'Please complete all fields including the security check'; | |
| 154 | + | // Additional check for captcha | |
| 155 | + | if (!captchaAnswer) { | |
| 156 | + | signupError = 'Please complete all fields including the PDR question'; | |
| 163 | 157 | signupStatus = 'error'; | |
| 164 | 158 | return; | |
| 165 | 159 | } | |
| @@ -271,7 +265,6 @@ | |||
| 271 | 265 | <label for="si-password">Mot de passe</label> | |
| 272 | 266 | <input type="password" id="si-password" bind:value={siPassword} placeholder="Mot de passe" /> | |
| 273 | 267 | </div> | |
| 274 | - | <Turnstile onVerify={(token: string) => siTurnstileToken = token} /> | |
| 275 | 268 | {#if error} | |
| 276 | 269 | <div class="error">{error}</div> | |
| 277 | 270 | {/if} | |
| @@ -311,7 +304,20 @@ | |||
| 311 | 304 | </div> | |
| 312 | 305 | <div class="form-group"> | |
| 313 | 306 | <label for="su-password">Mot de passe</label> | |
| 314 | - | <input type="password" id="su-password" bind:value={suPassword} placeholder="Mot de passe" /> | |
| 307 | + | <input | |
| 308 | + | type="password" | |
| 309 | + | id="su-password" | |
| 310 | + | bind:value={suPassword} | |
| 311 | + | placeholder="Mot de passe" | |
| 312 | + | onfocus={() => showPasswordStrength = true} | |
| 313 | + | /> | |
| 314 | + | {#if showPasswordStrength} | |
| 315 | + | <PasswordStrengthIndicator | |
| 316 | + | password={suPassword} | |
| 317 | + | constraints={passwordConstraints} | |
| 318 | + | showDetails={true} | |
| 319 | + | /> | |
| 320 | + | {/if} | |
| 315 | 321 | </div> | |
| 316 | 322 | <div class="form-group"> | |
| 317 | 323 | <label for="su-confirm-password">Confirmer le mot de passe</label> | |
| @@ -321,7 +327,6 @@ | |||
| 321 | 327 | <label for="captcha">Quelle est la signification de PDR ?</label> | |
| 322 | 328 | <input type="text" id="captcha" bind:value={captchaAnswer} placeholder="Entrez votre réponse" /> | |
| 323 | 329 | </div> | |
| 324 | - | <Turnstile onVerify={(token: string) => suTurnstileToken = token} /> | |
| 325 | 330 | {#if signupError} | |
| 326 | 331 | <div class="error">{signupError}</div> | |
| 327 | 332 | {/if} | |
| @@ -5,9 +5,10 @@ | |||||
| 5 | import type { User, SafeUser } from '$lib/types/chat'; | 5 | import type { User, SafeUser } from '$lib/types/chat'; | ||
| 6 | import { invalidateAll } from '$app/navigation'; | 6 | import { invalidateAll } from '$app/navigation'; | ||
| 7 | import { chatState } from '$lib/states/chat.svelte'; | 7 | import { chatState } from '$lib/states/chat.svelte'; | ||
| 8 | - | import Turnstile from './turnstile.svelte'; | |||
| 9 | import { createSafeUser } from '$lib/types/chat'; | 8 | import { createSafeUser } from '$lib/types/chat'; | ||
| 10 | import { onMount } from 'svelte'; | 9 | import { onMount } from 'svelte'; | ||
| 10 | + | import PasswordStrengthIndicator from './password-strength-indicator.svelte'; | |||
| 11 | + | import { loginSchema, createRegistrationSchema, validatePasswordStrength, DEFAULT_PASSWORD_CONSTRAINTS, type PasswordConstraints } from '../validation/password'; | |||
| 11 | 12 | ||||
| 12 | interface $$Props { | 13 | interface $$Props { | ||
| 13 | showAuth: boolean; | 14 | showAuth: boolean; | ||
| @@ -24,7 +25,6 @@ | |||||
| 24 | let siPassword = $state(''); | 25 | let siPassword = $state(''); | ||
| 25 | let error = $state(''); | 26 | let error = $state(''); | ||
| 26 | let loginStatus = $state('idle'); | 27 | let loginStatus = $state('idle'); | ||
| 27 | - | let siTurnstileToken = $state(''); | |||
| 28 | 28 | ||||
| 29 | // Sign Up state | 29 | // Sign Up state | ||
| 30 | let suUsername = $state(''); | 30 | let suUsername = $state(''); | ||
| @@ -35,6 +35,10 @@ | |||||
| 35 | let signupStatus = $state('idle'); | 35 | let signupStatus = $state('idle'); | ||
| 36 | let suTurnstileToken = $state(''); | 36 | let suTurnstileToken = $state(''); | ||
| 37 | 37 | ||||
| 38 | + | // Password validation state | |||
| 39 | + | let passwordConstraints: PasswordConstraints = DEFAULT_PASSWORD_CONSTRAINTS; | |||
| 40 | + | let showPasswordStrength = $state(false); | |||
| 41 | + | ||||
| 38 | // Added dragging state and handler similar to chat-room | 42 | // Added dragging state and handler similar to chat-room | ||
| 39 | let windowX = $state(0); | 43 | let windowX = $state(0); | ||
| 40 | let windowY = $state(0); | 44 | let windowY = $state(0); | ||
| @@ -60,8 +64,11 @@ | |||||
| 60 | async function handleSigninSubmit() { | 64 | async function handleSigninSubmit() { | ||
| 61 | error = ''; | 65 | error = ''; | ||
| 62 | loginStatus = 'loading'; | 66 | loginStatus = 'loading'; | ||
| 63 | - | if (!siUsername || !siPassword || !siTurnstileToken) { | 67 | + | |
| 64 | - | error = 'Please complete all fields including the security check'; | 68 | + | // Validate login data |
| 69 | + | const validationError = validateLoginData(); | |||
| 70 | + | if (validationError) { | |||
| 71 | + | error = validationError; | |||
| 65 | loginStatus = 'error'; | 72 | loginStatus = 'error'; | ||
| 66 | return; | 73 | return; | ||
| 67 | } | 74 | } | ||
| @@ -73,8 +80,7 @@ | |||||
| 73 | headers: { 'Content-Type': 'application/json' }, | 80 | headers: { 'Content-Type': 'application/json' }, | ||
| 74 | body: JSON.stringify({ | 81 | body: JSON.stringify({ | ||
| 75 | username: siUsername, | 82 | username: siUsername, | ||
| 76 | - | password: siPassword, | 83 | + | password: siPassword |
| 77 | - | turnstileToken: siTurnstileToken | |||
| 78 | }) | 84 | }) | ||
| 79 | }); | 85 | }); | ||
| 80 | 86 | ||||
| @@ -107,41 +113,30 @@ | |||||
| 107 | } | 113 | } | ||
| 108 | } | 114 | } | ||
| 109 | 115 | ||||
| 110 | - | // New: Create a helper function for client-side validations based on server rules. | 116 | + | // Validation using Zod schemas |
| 111 | - | function validateSignupData(): string | null { | 117 | + | function validateLoginData() { |
| 112 | - | // Trim values | 118 | + | try { |
| 113 | - | const username = suUsername.trim(); | 119 | + | loginSchema.parse({ username: siUsername.trim(), password: siPassword.trim() }); |
| 114 | - | const password = suPassword.trim(); | 120 | + | return null; |
| 115 | - | const confirmPasswordValue = suConfirmPassword.trim(); | 121 | + | } catch (err: any) { |
| 116 | - | 122 | + | return err.errors?.[0]?.message || 'Invalid input'; | |
| 117 | - | if (!username || !password || !confirmPasswordValue) { | |||
| 118 | - | return 'All fields must be filled'; | |||
| 119 | - | } | |||
| 120 | - | ||||
| 121 | - | if (username.length < 3) { | |||
| 122 | - | return 'Username must be at least 3 characters'; | |||
| 123 | - | } | |||
| 124 | - | ||||
| 125 | - | if (username.length > 32) { | |||
| 126 | - | return 'Username must be at most 32 characters'; | |||
| 127 | - | } | |||
| 128 | - | ||||
| 129 | - | // Only allow letters, numbers, underscores, and dashes in the username. | |||
| 130 | - | const usernameRegex = /^[a-zA-Z0-9_-]+$/; | |||
| 131 | - | if (!usernameRegex.test(username)) { | |||
| 132 | - | return 'Invalid username. Only letters, numbers, underscores, and dashes are allowed.'; | |||
| 133 | - | } | |||
| 134 | - | ||||
| 135 | - | if (password !== confirmPasswordValue) { | |||
| 136 | - | return 'Passwords do not match'; | |||
| 137 | } | 123 | } | ||
| 124 | + | } | |||
| 138 | 125 | ||||
| 139 | - | if (password.length < 8 || password.length > 64) { | 126 | + | function validateSignupData() { |
| 140 | - | return 'Password must be between 8 and 64 characters'; | 127 | + | try { |
| 128 | + | const registrationSchema = createRegistrationSchema(passwordConstraints); | |||
| 129 | + | registrationSchema.parse({ | |||
| 130 | + | suUsername: suUsername.trim(), | |||
| 131 | + | suPassword: suPassword.trim(), | |||
| 132 | + | suConfirmPassword: suConfirmPassword.trim(), | |||
| 133 | + | captchaAnswer: captchaAnswer.trim(), | |||
| 134 | + | turnstileToken: suTurnstileToken | |||
| 135 | + | }); | |||
| 136 | + | return null; | |||
| 137 | + | } catch (err: any) { | |||
| 138 | + | return err.errors?.[0]?.message || 'Invalid input'; | |||
| 141 | } | 139 | } | ||
| 142 | - | ||||
| 143 | - | // All checks passed. | |||
| 144 | - | return null; | |||
| 145 | } | 140 | } | ||
| 146 | 141 | ||||
| 147 | async function handleSignupSubmit() { | 142 | async function handleSignupSubmit() { | ||
| @@ -156,10 +151,9 @@ | |||||
| 156 | return; | 151 | return; | ||
| 157 | } | 152 | } | ||
| 158 | 153 | ||||
| 159 | - | // Additional check for captcha and Turnstile token, | 154 | + | // Additional check for captcha |
| 160 | - | // in case those fields are not filled. | 155 | + | if (!captchaAnswer) { |
| 161 | - | if (!captchaAnswer || !suTurnstileToken) { | 156 | + | signupError = 'Please complete all fields including the PDR question'; |
| 162 | - | signupError = 'Please complete all fields including the security check'; | |||
| 163 | signupStatus = 'error'; | 157 | signupStatus = 'error'; | ||
| 164 | return; | 158 | return; | ||
| 165 | } | 159 | } | ||
| @@ -271,7 +265,6 @@ | |||||
| 271 | <label for="si-password">Mot de passe</label> | 265 | <label for="si-password">Mot de passe</label> | ||
| 272 | <input type="password" id="si-password" bind:value={siPassword} placeholder="Mot de passe" /> | 266 | <input type="password" id="si-password" bind:value={siPassword} placeholder="Mot de passe" /> | ||
| 273 | </div> | 267 | </div> | ||
| 274 | - | <Turnstile onVerify={(token: string) => siTurnstileToken = token} /> | |||
| 275 | {#if error} | 268 | {#if error} | ||
| 276 | <div class="error">{error}</div> | 269 | <div class="error">{error}</div> | ||
| 277 | {/if} | 270 | {/if} | ||
| @@ -311,7 +304,20 @@ | |||||
| 311 | </div> | 304 | </div> | ||
| 312 | <div class="form-group"> | 305 | <div class="form-group"> | ||
| 313 | <label for="su-password">Mot de passe</label> | 306 | <label for="su-password">Mot de passe</label> | ||
| 314 | - | <input type="password" id="su-password" bind:value={suPassword} placeholder="Mot de passe" /> | 307 | + | <input |
| 308 | + | type="password" | |||
| 309 | + | id="su-password" | |||
| 310 | + | bind:value={suPassword} | |||
| 311 | + | placeholder="Mot de passe" | |||
| 312 | + | onfocus={() => showPasswordStrength = true} | |||
| 313 | + | /> | |||
| 314 | + | {#if showPasswordStrength} | |||
| 315 | + | <PasswordStrengthIndicator | |||
| 316 | + | password={suPassword} | |||
| 317 | + | constraints={passwordConstraints} | |||
| 318 | + | showDetails={true} | |||
| 319 | + | /> | |||
| 320 | + | {/if} | |||
| 315 | </div> | 321 | </div> | ||
| 316 | <div class="form-group"> | 322 | <div class="form-group"> | ||
| 317 | <label for="su-confirm-password">Confirmer le mot de passe</label> | 323 | <label for="su-confirm-password">Confirmer le mot de passe</label> | ||
| @@ -321,7 +327,6 @@ | |||||
| 321 | <label for="captcha">Quelle est la signification de PDR ?</label> | 327 | <label for="captcha">Quelle est la signification de PDR ?</label> | ||
| 322 | <input type="text" id="captcha" bind:value={captchaAnswer} placeholder="Entrez votre réponse" /> | 328 | <input type="text" id="captcha" bind:value={captchaAnswer} placeholder="Entrez votre réponse" /> | ||
| 323 | </div> | 329 | </div> | ||
| 324 | - | <Turnstile onVerify={(token: string) => suTurnstileToken = token} /> | |||
| 325 | {#if signupError} | 330 | {#if signupError} | ||
| 326 | <div class="error">{signupError}</div> | 331 | <div class="error">{signupError}</div> | ||
| 327 | {/if} | 332 | {/if} | ||
M src/lib/components/chat-room.svelte +82 -15
| @@ -11,8 +11,14 @@ import LoadingButton from './ui/button-loading.svelte'; | |||
| 11 | 11 | import Tooltip from './ui/tooltip.svelte'; | |
| 12 | 12 | import LoadingDots from './ui/loading-dots.svelte'; | |
| 13 | 13 | import AimLogin from './aim-login.svelte'; | |
| 14 | + | import FormattedMessage from './formatted-message.svelte'; | |
| 15 | + | import TextFormattingToolbar from './text-formatting-toolbar.svelte'; | |
| 16 | + | import { DEFAULT_TEXT_STYLE, type TextStyle, generateInputCSSStyle } from '../types/text-formatting'; | |
| 14 | 17 | import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format'; | |
| 15 | 18 | ||
| 19 | + | // Props destructuring must come first | |
| 20 | + | let { showChatRoom = $bindable(), initialTextStyle = DEFAULT_TEXT_STYLE } = $props(); | |
| 21 | + | ||
| 16 | 22 | // Initialize with default values for SSR | |
| 17 | 23 | let windowWidth = $state(400); | |
| 18 | 24 | let windowHeight = $state(500); | |
| @@ -26,6 +32,14 @@ let isInitialLoading = $state(true); | |||
| 26 | 32 | let showAuth = $state(false); | |
| 27 | 33 | let isMinimized = $state(false); | |
| 28 | 34 | ||
| 35 | + | // Text formatting state - initialize directly with merged initial style | |
| 36 | + | let currentTextStyle = $state<TextStyle>({ | |
| 37 | + | ...DEFAULT_TEXT_STYLE, | |
| 38 | + | ...initialTextStyle, | |
| 39 | + | color: initialTextStyle.color || '#000000' // Ensure we always have a valid color | |
| 40 | + | }); | |
| 41 | + | let showFormattingToolbar = $state(true); | |
| 42 | + | ||
| 29 | 43 | // Rate limiting state | |
| 30 | 44 | let cooldownEndTime = $state<number | null>(null); | |
| 31 | 45 | let cooldownProgress = $state(0); | |
| @@ -60,8 +74,6 @@ function startCooldownTimer(retryAfter: number) { | |||
| 60 | 74 | updateCooldownProgress(); | |
| 61 | 75 | } | |
| 62 | 76 | ||
| 63 | - | let { showChatRoom = $bindable() } = $props(); | |
| 64 | - | ||
| 65 | 77 | function handleClose() { | |
| 66 | 78 | showChatRoom = false; | |
| 67 | 79 | } | |
| @@ -239,7 +251,7 @@ async function handleSubmit() { | |||
| 239 | 251 | ||
| 240 | 252 | isSendingMessage = true; | |
| 241 | 253 | try { | |
| 242 | - | const response = await chatState.sendMessage(currentMessage); | |
| 254 | + | const response = await chatState.sendMessage(currentMessage, 'chat', currentTextStyle); | |
| 243 | 255 | if (!response.success && response.isRateLimited && response.retryAfter) { | |
| 244 | 256 | startCooldownTimer(response.retryAfter); | |
| 245 | 257 | rateLimitWarning = 'Whoa there! You\'re sending messages too quickly. Take a breather...'; | |
| @@ -459,7 +471,7 @@ function handleLoginSuccess() { | |||
| 459 | 471 | ondragmove={handleDragMove} | |
| 460 | 472 | > | |
| 461 | 473 | <div class="title-bar"> | |
| 462 | - | <div class="title-bar-text" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> | |
| 474 | + | <div class="title-bar-text" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: 'MS Sans Serif', 'Pixelated MS Sans Serif', sans-serif;"> | |
| 463 | 475 | Pdr Aim {#if currentUser} - {currentUser.nickname}{:else} - {totalUsers} membre{totalUsers > 1 ? 's' : ''}{/if} | |
| 464 | 476 | {#if sseError && !isMinimized} | |
| 465 | 477 | <span class="connection-error">⚠️ Erreur de connexion</span> | |
| @@ -563,21 +575,49 @@ function handleLoginSuccess() { | |||
| 563 | 575 | }}> | |
| 564 | 576 | <span class="nickname pointer-events-none">{message.user.nickname}:</span> | |
| 565 | 577 | </Tooltip> | |
| 566 | - | <span class="content">{message.content}</span> | |
| 578 | + | <FormattedMessage {message} allowFormatting={true} /> | |
| 567 | 579 | {/if} | |
| 568 | 580 | </div> | |
| 569 | 581 | {/each} | |
| 570 | 582 | </div> | |
| 571 | 583 | ||
| 584 | + | <!-- Text Formatting Toolbar --> | |
| 585 | + | {#if currentUser} | |
| 586 | + | <div style="margin-bottom: 0.25rem;"> | |
| 587 | + | <TextFormattingToolbar | |
| 588 | + | bind:style={currentTextStyle} | |
| 589 | + | compact={true} | |
| 590 | + | showFontSelector={true} | |
| 591 | + | showGradients={true} | |
| 592 | + | /> | |
| 593 | + | </div> | |
| 594 | + | {/if} | |
| 595 | + | ||
| 596 | + | ||
| 572 | 597 | <div class="field-row input-container" style="margin: 0;"> | |
| 573 | - | <input | |
| 574 | - | type="text" | |
| 575 | - | bind:value={currentMessage} | |
| 576 | - | style="flex: 1;" | |
| 577 | - | onkeydown={(e) => e.key === 'Enter' && handleSubmit()} | |
| 578 | - | placeholder={cooldownEndTime ? `Patientez ${cooldownProgress.toFixed(1)}s...` : "Écrivez un message..."} | |
| 579 | - | disabled={!currentUser || Boolean(cooldownEndTime)} | |
| 580 | - | /> | |
| 598 | + | {#if currentTextStyle.gradient && currentTextStyle.gradient.length > 1} | |
| 599 | + | <div class="gradient-input-wrapper" style="flex: 1; position: relative; background: white;"> | |
| 600 | + | <input | |
| 601 | + | type="text" | |
| 602 | + | bind:value={currentMessage} | |
| 603 | + | class="styled-input retro-font-{currentTextStyle.fontFamily}" | |
| 604 | + | style="width: 100%; background: transparent; {generateInputCSSStyle(currentTextStyle)}" | |
| 605 | + | onkeydown={(e) => e.key === 'Enter' && handleSubmit()} | |
| 606 | + | placeholder={cooldownEndTime ? `Patientez ${cooldownProgress.toFixed(1)}s...` : "Écrivez un message..."} | |
| 607 | + | disabled={!currentUser || Boolean(cooldownEndTime)} | |
| 608 | + | /> | |
| 609 | + | </div> | |
| 610 | + | {:else} | |
| 611 | + | <input | |
| 612 | + | type="text" | |
| 613 | + | bind:value={currentMessage} | |
| 614 | + | class="styled-input retro-font-{currentTextStyle.fontFamily}" | |
| 615 | + | style="flex: 1; {generateInputCSSStyle(currentTextStyle)}" | |
| 616 | + | onkeydown={(e) => e.key === 'Enter' && handleSubmit()} | |
| 617 | + | placeholder={cooldownEndTime ? `Patientez ${cooldownProgress.toFixed(1)}s...` : "Écrivez un message..."} | |
| 618 | + | disabled={!currentUser || Boolean(cooldownEndTime)} | |
| 619 | + | /> | |
| 620 | + | {/if} | |
| 581 | 621 | <LoadingButton | |
| 582 | 622 | onclick={handleSubmit} | |
| 583 | 623 | disabled={!currentUser || Boolean(cooldownEndTime)} | |
| @@ -632,9 +672,14 @@ function handleLoginSuccess() { | |||
| 632 | 672 | {/if} | |
| 633 | 673 | ||
| 634 | 674 | <style> | |
| 635 | - | .chat-area, .users-list, input { | |
| 675 | + | .chat-area, .users-list { | |
| 636 | 676 | font-size: 1rem; | |
| 637 | - | font-family: Arial, Verdana, Tahoma, sans-serif; | |
| 677 | + | font-family: "Pixelated MS Sans Serif", Arial, Verdana, Tahoma, sans-serif; | |
| 678 | + | } | |
| 679 | + | ||
| 680 | + | input:not(.styled-input) { | |
| 681 | + | font-size: 1rem; | |
| 682 | + | font-family: "Pixelated MS Sans Serif", Arial, Verdana, Tahoma, sans-serif; | |
| 638 | 683 | } | |
| 639 | 684 | ||
| 640 | 685 | .title-bar { | |
| @@ -674,6 +719,16 @@ function handleLoginSuccess() { | |||
| 674 | 719 | gap: 0.5rem; | |
| 675 | 720 | align-items: flex-start; | |
| 676 | 721 | } | |
| 722 | + | ||
| 723 | + | /* Fix gradient text spacing in messages */ | |
| 724 | + | .message :global(.gradient-text-static) { | |
| 725 | + | letter-spacing: normal !important; | |
| 726 | + | word-spacing: normal !important; | |
| 727 | + | } | |
| 728 | + | ||
| 729 | + | .message :global(.gradient-text-static span) { | |
| 730 | + | letter-spacing: 0 !important; | |
| 731 | + | } | |
| 677 | 732 | ||
| 678 | 733 | .message .nickname { | |
| 679 | 734 | font-weight: bold; | |
| @@ -1036,4 +1091,16 @@ function handleLoginSuccess() { | |||
| 1036 | 1091 | .pointer-events-none { | |
| 1037 | 1092 | pointer-events: none; | |
| 1038 | 1093 | } | |
| 1094 | + | ||
| 1095 | + | .gradient-input-wrapper { | |
| 1096 | + | border: 1px inset #dfdfdf; | |
| 1097 | + | padding: 2px; | |
| 1098 | + | } | |
| 1099 | + | ||
| 1100 | + | .gradient-input-wrapper input { | |
| 1101 | + | border: none !important; | |
| 1102 | + | outline: none !important; | |
| 1103 | + | padding: 1px 3px; | |
| 1104 | + | } | |
| 1105 | + | ||
| 1039 | 1106 | </style>⏎̸ | |
| @@ -11,8 +11,14 @@ import LoadingButton from './ui/button-loading.svelte'; | |||||
| 11 | import Tooltip from './ui/tooltip.svelte'; | 11 | import Tooltip from './ui/tooltip.svelte'; | ||
| 12 | import LoadingDots from './ui/loading-dots.svelte'; | 12 | import LoadingDots from './ui/loading-dots.svelte'; | ||
| 13 | import AimLogin from './aim-login.svelte'; | 13 | import AimLogin from './aim-login.svelte'; | ||
| 14 | + | import FormattedMessage from './formatted-message.svelte'; | |||
| 15 | + | import TextFormattingToolbar from './text-formatting-toolbar.svelte'; | |||
| 16 | + | import { DEFAULT_TEXT_STYLE, type TextStyle, generateInputCSSStyle } from '../types/text-formatting'; | |||
| 14 | import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format'; | 17 | import { formatFrenchDateTime, formatFrenchRelativeTimeSafe } from '$lib/utils/date-format'; | ||
| 15 | 18 | ||||
| 19 | + | // Props destructuring must come first | |||
| 20 | + | let { showChatRoom = $bindable(), initialTextStyle = DEFAULT_TEXT_STYLE } = $props(); | |||
| 21 | + | ||||
| 16 | // Initialize with default values for SSR | 22 | // Initialize with default values for SSR | ||
| 17 | let windowWidth = $state(400); | 23 | let windowWidth = $state(400); | ||
| 18 | let windowHeight = $state(500); | 24 | let windowHeight = $state(500); | ||
| @@ -26,6 +32,14 @@ let isInitialLoading = $state(true); | |||||
| 26 | let showAuth = $state(false); | 32 | let showAuth = $state(false); | ||
| 27 | let isMinimized = $state(false); | 33 | let isMinimized = $state(false); | ||
| 28 | 34 | ||||
| 35 | + | // Text formatting state - initialize directly with merged initial style | |||
| 36 | + | let currentTextStyle = $state<TextStyle>({ | |||
| 37 | + | ...DEFAULT_TEXT_STYLE, | |||
| 38 | + | ...initialTextStyle, | |||
| 39 | + | color: initialTextStyle.color || '#000000' // Ensure we always have a valid color | |||
| 40 | + | }); | |||
| 41 | + | let showFormattingToolbar = $state(true); | |||
| 42 | + | ||||
| 29 | // Rate limiting state | 43 | // Rate limiting state | ||
| 30 | let cooldownEndTime = $state<number | null>(null); | 44 | let cooldownEndTime = $state<number | null>(null); | ||
| 31 | let cooldownProgress = $state(0); | 45 | let cooldownProgress = $state(0); | ||
| @@ -60,8 +74,6 @@ function startCooldownTimer(retryAfter: number) { | |||||
| 60 | updateCooldownProgress(); | 74 | updateCooldownProgress(); | ||
| 61 | } | 75 | } | ||
| 62 | 76 | ||||
| 63 | - | let { showChatRoom = $bindable() } = $props(); | |||
| 64 | - | ||||
| 65 | function handleClose() { | 77 | function handleClose() { | ||
| 66 | showChatRoom = false; | 78 | showChatRoom = false; | ||
| 67 | } | 79 | } | ||
| @@ -239,7 +251,7 @@ async function handleSubmit() { | |||||
| 239 | 251 | ||||
| 240 | isSendingMessage = true; | 252 | isSendingMessage = true; | ||
| 241 | try { | 253 | try { | ||
| 242 | - | const response = await chatState.sendMessage(currentMessage); | 254 | + | const response = await chatState.sendMessage(currentMessage, 'chat', currentTextStyle); |
| 243 | if (!response.success && response.isRateLimited && response.retryAfter) { | 255 | if (!response.success && response.isRateLimited && response.retryAfter) { | ||
| 244 | startCooldownTimer(response.retryAfter); | 256 | startCooldownTimer(response.retryAfter); | ||
| 245 | rateLimitWarning = 'Whoa there! You\'re sending messages too quickly. Take a breather...'; | 257 | rateLimitWarning = 'Whoa there! You\'re sending messages too quickly. Take a breather...'; | ||
| @@ -459,7 +471,7 @@ function handleLoginSuccess() { | |||||
| 459 | ondragmove={handleDragMove} | 471 | ondragmove={handleDragMove} | ||
| 460 | > | 472 | > | ||
| 461 | <div class="title-bar"> | 473 | <div class="title-bar"> | ||
| 462 | - | <div class="title-bar-text" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> | 474 | + | <div class="title-bar-text" style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: 'MS Sans Serif', 'Pixelated MS Sans Serif', sans-serif;"> |
| 463 | Pdr Aim {#if currentUser} - {currentUser.nickname}{:else} - {totalUsers} membre{totalUsers > 1 ? 's' : ''}{/if} | 475 | Pdr Aim {#if currentUser} - {currentUser.nickname}{:else} - {totalUsers} membre{totalUsers > 1 ? 's' : ''}{/if} | ||
| 464 | {#if sseError && !isMinimized} | 476 | {#if sseError && !isMinimized} | ||
| 465 | <span class="connection-error">⚠️ Erreur de connexion</span> | 477 | <span class="connection-error">⚠️ Erreur de connexion</span> | ||
| @@ -563,21 +575,49 @@ function handleLoginSuccess() { | |||||
| 563 | }}> | 575 | }}> | ||
| 564 | <span class="nickname pointer-events-none">{message.user.nickname}:</span> | 576 | <span class="nickname pointer-events-none">{message.user.nickname}:</span> | ||
| 565 | </Tooltip> | 577 | </Tooltip> | ||
| 566 | - | <span class="content">{message.content}</span> | 578 | + | <FormattedMessage {message} allowFormatting={true} /> |
| 567 | {/if} | 579 | {/if} | ||
| 568 | </div> | 580 | </div> | ||
| 569 | {/each} | 581 | {/each} | ||
| 570 | </div> | 582 | </div> | ||
| 571 | 583 | ||||
| 584 | + | <!-- Text Formatting Toolbar --> | |||
| 585 | + | {#if currentUser} | |||
| 586 | + | <div style="margin-bottom: 0.25rem;"> | |||
| 587 | + | <TextFormattingToolbar | |||
| 588 | + | bind:style={currentTextStyle} | |||
| 589 | + | compact={true} | |||
| 590 | + | showFontSelector={true} | |||
| 591 | + | showGradients={true} | |||
| 592 | + | /> | |||
| 593 | + | </div> | |||
| 594 | + | {/if} | |||
| 595 | + | ||||
| 596 | + | ||||
| 572 | <div class="field-row input-container" style="margin: 0;"> | 597 | <div class="field-row input-container" style="margin: 0;"> | ||
| 573 | - | <input | 598 | + | {#if currentTextStyle.gradient && currentTextStyle.gradient.length > 1} |
| 574 | - | type="text" | 599 | + | <div class="gradient-input-wrapper" style="flex: 1; position: relative; background: white;"> |
| 575 | - | bind:value={currentMessage} | 600 | + | <input |
| 576 | - | style="flex: 1;" | 601 | + | type="text" |
| 577 | - | onkeydown={(e) => e.key === 'Enter' && handleSubmit()} | 602 | + | bind:value={currentMessage} |
| 578 | - | placeholder={cooldownEndTime ? `Patientez ${cooldownProgress.toFixed(1)}s...` : "Écrivez un message..."} | 603 | + | class="styled-input retro-font-{currentTextStyle.fontFamily}" |
| 579 | - | disabled={!currentUser || Boolean(cooldownEndTime)} | 604 | + | style="width: 100%; background: transparent; {generateInputCSSStyle(currentTextStyle)}" |
| 580 | - | /> | 605 | + | onkeydown={(e) => e.key === 'Enter' && handleSubmit()} |
| 606 | + | placeholder={cooldownEndTime ? `Patientez ${cooldownProgress.toFixed(1)}s...` : "Écrivez un message..."} | |||
| 607 | + | disabled={!currentUser || Boolean(cooldownEndTime)} | |||
| 608 | + | /> | |||
| 609 | + | </div> | |||
| 610 | + | {:else} | |||
| 611 | + | <input | |||
| 612 | + | type="text" | |||
| 613 | + | bind:value={currentMessage} | |||
| 614 | + | class="styled-input retro-font-{currentTextStyle.fontFamily}" | |||
| 615 | + | style="flex: 1; {generateInputCSSStyle(currentTextStyle)}" | |||
| 616 | + | onkeydown={(e) => e.key === 'Enter' && handleSubmit()} | |||
| 617 | + | placeholder={cooldownEndTime ? `Patientez ${cooldownProgress.toFixed(1)}s...` : "Écrivez un message..."} | |||
| 618 | + | disabled={!currentUser || Boolean(cooldownEndTime)} | |||
| 619 | + | /> | |||
| 620 | + | {/if} | |||
| 581 | <LoadingButton | 621 | <LoadingButton | ||
| 582 | onclick={handleSubmit} | 622 | onclick={handleSubmit} | ||
| 583 | disabled={!currentUser || Boolean(cooldownEndTime)} | 623 | disabled={!currentUser || Boolean(cooldownEndTime)} | ||
| @@ -632,9 +672,14 @@ function handleLoginSuccess() { | |||||
| 632 | {/if} | 672 | {/if} | ||
| 633 | 673 | ||||
| 634 | <style> | 674 | <style> | ||
| 635 | - | .chat-area, .users-list, input { | 675 | + | .chat-area, .users-list { |
| 636 | font-size: 1rem; | 676 | font-size: 1rem; | ||
| 637 | - | font-family: Arial, Verdana, Tahoma, sans-serif; | 677 | + | font-family: "Pixelated MS Sans Serif", Arial, Verdana, Tahoma, sans-serif; |
| 678 | + | } | |||
| 679 | + | ||||
| 680 | + | input:not(.styled-input) { | |||
| 681 | + | font-size: 1rem; | |||
| 682 | + | font-family: "Pixelated MS Sans Serif", Arial, Verdana, Tahoma, sans-serif; | |||
| 638 | } | 683 | } | ||
| 639 | 684 | ||||
| 640 | .title-bar { | 685 | .title-bar { | ||
| @@ -674,6 +719,16 @@ function handleLoginSuccess() { | |||||
| 674 | gap: 0.5rem; | 719 | gap: 0.5rem; | ||
| 675 | align-items: flex-start; | 720 | align-items: flex-start; | ||
| 676 | } | 721 | } | ||
| 722 | + | ||||
| 723 | + | /* Fix gradient text spacing in messages */ | |||
| 724 | + | .message :global(.gradient-text-static) { | |||
| 725 | + | letter-spacing: normal !important; | |||
| 726 | + | word-spacing: normal !important; | |||
| 727 | + | } | |||
| 728 | + | ||||
| 729 | + | .message :global(.gradient-text-static span) { | |||
| 730 | + | letter-spacing: 0 !important; | |||
| 731 | + | } | |||
| 677 | 732 | ||||
| 678 | .message .nickname { | 733 | .message .nickname { | ||
| 679 | font-weight: bold; | 734 | font-weight: bold; | ||
| @@ -1036,4 +1091,16 @@ function handleLoginSuccess() { | |||||
| 1036 | .pointer-events-none { | 1091 | .pointer-events-none { | ||
| 1037 | pointer-events: none; | 1092 | pointer-events: none; | ||
| 1038 | } | 1093 | } | ||
| 1094 | + | ||||
| 1095 | + | .gradient-input-wrapper { | |||
| 1096 | + | border: 1px inset #dfdfdf; | |||
| 1097 | + | padding: 2px; | |||
| 1098 | + | } | |||
| 1099 | + | ||||
| 1100 | + | .gradient-input-wrapper input { | |||
| 1101 | + | border: none !important; | |||
| 1102 | + | outline: none !important; | |||
| 1103 | + | padding: 1px 3px; | |||
| 1104 | + | } | |||
| 1105 | + | ||||
| 1039 | </style>⏎̸ | 1106 | </style>⏎̸ | ||
A src/lib/components/user-preferences-modal.svelte +492 -0
| @@ -0,0 +1,492 @@ | |||
| 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 { | |
| 13 | + | showModal = $bindable(false), | |
| 14 | + | onSave | |
| 15 | + | } = $props<{ | |
| 16 | + | showModal: boolean; | |
| 17 | + | onSave?: (preferences: UserTextPreferences) => void; | |
| 18 | + | }>(); | |
| 19 | + | ||
| 20 | + | // State | |
| 21 | + | let preferences = $state<UserTextPreferences>({ ...DEFAULT_USER_PREFERENCES }); | |
| 22 | + | let currentStyle = $state<TextStyle>({ ...DEFAULT_TEXT_STYLE }); | |
| 23 | + | let isSaving = $state(false); | |
| 24 | + | let hasChanges = $state(false); | |
| 25 | + | ||
| 26 | + | // Track changes | |
| 27 | + | $effect(() => { | |
| 28 | + | hasChanges = JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES); | |
| 29 | + | }); | |
| 30 | + | ||
| 31 | + | // Update preferences when style changes | |
| 32 | + | $effect(() => { | |
| 33 | + | preferences.defaultStyle = { ...currentStyle }; | |
| 34 | + | }); | |
| 35 | + | ||
| 36 | + | // Load user preferences on mount | |
| 37 | + | onMount(async () => { | |
| 38 | + | try { | |
| 39 | + | const response = await fetch('/api/user/text-preferences'); | |
| 40 | + | if (response.ok) { | |
| 41 | + | const data = await response.json(); | |
| 42 | + | if (data.success && data.preferences) { | |
| 43 | + | preferences = { ...DEFAULT_USER_PREFERENCES, ...data.preferences }; | |
| 44 | + | currentStyle = { ...DEFAULT_TEXT_STYLE, ...preferences.defaultStyle }; | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | } catch (error) { | |
| 48 | + | console.error('Failed to load user preferences:', error); | |
| 49 | + | } | |
| 50 | + | }); | |
| 51 | + | ||
| 52 | + | // Save preferences | |
| 53 | + | async function savePreferences() { | |
| 54 | + | if (!onSave) return; | |
| 55 | + | ||
| 56 | + | isSaving = true; | |
| 57 | + | try { | |
| 58 | + | await onSave(preferences); | |
| 59 | + | showModal = false; | |
| 60 | + | } catch (error) { | |
| 61 | + | console.error('Failed to save preferences:', error); | |
| 62 | + | alert('Failed to save preferences. Please try again.'); | |
| 63 | + | } finally { | |
| 64 | + | isSaving = false; | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // Reset to defaults | |
| 69 | + | function resetToDefaults() { | |
| 70 | + | preferences = { ...DEFAULT_USER_PREFERENCES }; | |
| 71 | + | currentStyle = { ...DEFAULT_TEXT_STYLE }; | |
| 72 | + | } | |
| 73 | + | ||
| 74 | + | // Cancel changes | |
| 75 | + | function cancel() { | |
| 76 | + | showModal = false; | |
| 77 | + | } | |
| 78 | + | ||
| 79 | + | // Handle keyboard shortcuts | |
| 80 | + | function handleKeydown(event: KeyboardEvent) { | |
| 81 | + | if (event.key === 'Escape') { | |
| 82 | + | cancel(); | |
| 83 | + | } else if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { | |
| 84 | + | savePreferences(); | |
| 85 | + | } | |
| 86 | + | } | |
| 87 | + | </script> | |
| 88 | + | ||
| 89 | + | {#if showModal} | |
| 90 | + | <!-- Modal backdrop --> | |
| 91 | + | <div | |
| 92 | + | class="modal-backdrop" | |
| 93 | + | onclick={cancel} | |
| 94 | + | onkeydown={handleKeydown} | |
| 95 | + | role="dialog" | |
| 96 | + | aria-modal="true" | |
| 97 | + | aria-labelledby="preferences-title" | |
| 98 | + | > | |
| 99 | + | <!-- Modal content --> | |
| 100 | + | <div class="modal-window window" onclick={(e) => e.stopPropagation()}> | |
| 101 | + | <div class="title-bar"> | |
| 102 | + | <div class="title-bar-text" id="preferences-title"> | |
| 103 | + | Text Formatting Preferences | |
| 104 | + | </div> | |
| 105 | + | <div class="title-bar-controls"> | |
| 106 | + | <button onclick={cancel} aria-label="Close"></button> | |
| 107 | + | </div> | |
| 108 | + | </div> | |
| 109 | + | ||
| 110 | + | <div class="window-body"> | |
| 111 | + | <div class="preferences-content"> | |
| 112 | + | ||
| 113 | + | <!-- Enable/Disable Formatting --> | |
| 114 | + | <div class="preference-group"> | |
| 115 | + | <label class="checkbox-label"> | |
| 116 | + | <input | |
| 117 | + | type="checkbox" | |
| 118 | + | bind:checked={preferences.allowFormatting} | |
| 119 | + | /> | |
| 120 | + | Enable text formatting in messages | |
| 121 | + | </label> | |
| 122 | + | <small class="help-text"> | |
| 123 | + | When enabled, you can use formatting options like bold, italic, colors, and gradients. | |
| 124 | + | </small> | |
| 125 | + | </div> | |
| 126 | + | ||
| 127 | + | <!-- Default Text Style --> | |
| 128 | + | <div class="preference-group"> | |
| 129 | + | <h3>Default Text Style</h3> | |
| 130 | + | <p class="description"> | |
| 131 | + | These settings will be used as defaults when you send new messages. | |
| 132 | + | </p> | |
| 133 | + | ||
| 134 | + | <TextStyleSelector | |
| 135 | + | bind:style={currentStyle} | |
| 136 | + | previewText="This is how your messages will look by default" | |
| 137 | + | showPreview={true} | |
| 138 | + | compact={false} | |
| 139 | + | /> | |
| 140 | + | </div> | |
| 141 | + | ||
| 142 | + | <!-- Message Settings --> | |
| 143 | + | <div class="preference-group"> | |
| 144 | + | <h3>Message Settings</h3> | |
| 145 | + | ||
| 146 | + | <div class="setting-row"> | |
| 147 | + | <label for="max-length">Maximum message length:</label> | |
| 148 | + | <input | |
| 149 | + | id="max-length" | |
| 150 | + | type="number" | |
| 151 | + | min="100" | |
| 152 | + | max="2000" | |
| 153 | + | bind:value={preferences.maxMessageLength} | |
| 154 | + | class="length-input" | |
| 155 | + | /> | |
| 156 | + | <span class="unit">characters</span> | |
| 157 | + | </div> | |
| 158 | + | ||
| 159 | + | <small class="help-text"> | |
| 160 | + | Longer messages may take more time to process and display. | |
| 161 | + | </small> | |
| 162 | + | </div> | |
| 163 | + | ||
| 164 | + | <!-- Format Help --> | |
| 165 | + | <div class="preference-group"> | |
| 166 | + | <details class="format-help"> | |
| 167 | + | <summary>Formatting Guide</summary> | |
| 168 | + | <div class="format-examples"> | |
| 169 | + | <h4>BBCode Style:</h4> | |
| 170 | + | <ul> | |
| 171 | + | <li><code>[b]bold text[/b]</code> → <strong>bold text</strong></li> | |
| 172 | + | <li><code>[i]italic text[/i]</code> → <em>italic text</em></li> | |
| 173 | + | <li><code>[u]underlined[/u]</code> → <u>underlined</u></li> | |
| 174 | + | <li><code>[s]strikethrough[/s]</code> → <s>strikethrough</s></li> | |
| 175 | + | <li><code>[color=red]red text[/color]</code> → <span style="color: red">red text</span></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 | + | ||
| 189 | + | </div> | |
| 190 | + | ||
| 191 | + | <!-- Modal buttons --> | |
| 192 | + | <div class="modal-buttons"> | |
| 193 | + | <button | |
| 194 | + | class="reset-button" | |
| 195 | + | onclick={resetToDefaults} | |
| 196 | + | disabled={isSaving} | |
| 197 | + | > | |
| 198 | + | Reset to Defaults | |
| 199 | + | </button> | |
| 200 | + | ||
| 201 | + | <div class="main-buttons"> | |
| 202 | + | <button | |
| 203 | + | onclick={cancel} | |
| 204 | + | disabled={isSaving} | |
| 205 | + | > | |
| 206 | + | Cancel | |
| 207 | + | </button> | |
| 208 | + | ||
| 209 | + | <button | |
| 210 | + | class="save-button" | |
| 211 | + | onclick={savePreferences} | |
| 212 | + | disabled={isSaving || !hasChanges} | |
| 213 | + | > | |
| 214 | + | {#if isSaving} | |
| 215 | + | Saving... | |
| 216 | + | {:else} | |
| 217 | + | Save Changes | |
| 218 | + | {/if} | |
| 219 | + | </button> | |
| 220 | + | </div> | |
| 221 | + | </div> | |
| 222 | + | </div> | |
| 223 | + | </div> | |
| 224 | + | </div> | |
| 225 | + | {/if} | |
| 226 | + | ||
| 227 | + | <style> | |
| 228 | + | .modal-backdrop { | |
| 229 | + | position: fixed; | |
| 230 | + | top: 0; | |
| 231 | + | left: 0; | |
| 232 | + | right: 0; | |
| 233 | + | bottom: 0; | |
| 234 | + | background: rgba(0, 0, 0, 0.5); | |
| 235 | + | display: flex; | |
| 236 | + | align-items: center; | |
| 237 | + | justify-content: center; | |
| 238 | + | z-index: 1000; | |
| 239 | + | padding: 20px; | |
| 240 | + | } | |
| 241 | + | ||
| 242 | + | .modal-window { | |
| 243 | + | width: 100%; | |
| 244 | + | max-width: 600px; | |
| 245 | + | max-height: 90vh; | |
| 246 | + | overflow: hidden; | |
| 247 | + | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.3); | |
| 248 | + | } | |
| 249 | + | ||
| 250 | + | .window-body { | |
| 251 | + | display: flex; | |
| 252 | + | flex-direction: column; | |
| 253 | + | height: 70vh; | |
| 254 | + | max-height: 600px; | |
| 255 | + | } | |
| 256 | + | ||
| 257 | + | .preferences-content { | |
| 258 | + | flex: 1; | |
| 259 | + | overflow-y: auto; | |
| 260 | + | padding: 16px; | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | .preference-group { | |
| 264 | + | margin-bottom: 24px; | |
| 265 | + | padding-bottom: 16px; | |
| 266 | + | border-bottom: 1px solid #ddd; | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | .preference-group:last-child { | |
| 270 | + | border-bottom: none; | |
| 271 | + | margin-bottom: 0; | |
| 272 | + | } | |
| 273 | + | ||
| 274 | + | .preference-group h3 { | |
| 275 | + | margin: 0 0 8px 0; | |
| 276 | + | font-size: 14px; | |
| 277 | + | font-weight: bold; | |
| 278 | + | color: #2d31a6; | |
| 279 | + | } | |
| 280 | + | ||
| 281 | + | .description { | |
| 282 | + | margin: 0 0 12px 0; | |
| 283 | + | font-size: 12px; | |
| 284 | + | color: #666; | |
| 285 | + | } | |
| 286 | + | ||
| 287 | + | .checkbox-label { | |
| 288 | + | display: flex; | |
| 289 | + | align-items: center; | |
| 290 | + | gap: 8px; | |
| 291 | + | font-size: 12px; | |
| 292 | + | cursor: pointer; | |
| 293 | + | } | |
| 294 | + | ||
| 295 | + | .help-text { | |
| 296 | + | display: block; | |
| 297 | + | margin-top: 4px; | |
| 298 | + | font-size: 11px; | |
| 299 | + | color: #666; | |
| 300 | + | font-style: italic; | |
| 301 | + | } | |
| 302 | + | ||
| 303 | + | .setting-row { | |
| 304 | + | display: flex; | |
| 305 | + | align-items: center; | |
| 306 | + | gap: 8px; | |
| 307 | + | margin-bottom: 8px; | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | .setting-row label { | |
| 311 | + | font-size: 12px; | |
| 312 | + | min-width: 140px; | |
| 313 | + | } | |
| 314 | + | ||
| 315 | + | .length-input { | |
| 316 | + | width: 80px; | |
| 317 | + | padding: 2px 4px; | |
| 318 | + | border: 1px inset #dfdfdf; | |
| 319 | + | background: white; | |
| 320 | + | font-size: 11px; | |
| 321 | + | } | |
| 322 | + | ||
| 323 | + | .unit { | |
| 324 | + | font-size: 11px; | |
| 325 | + | color: #666; | |
| 326 | + | } | |
| 327 | + | ||
| 328 | + | .format-help { | |
| 329 | + | margin-top: 8px; | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | .format-help summary { | |
| 333 | + | font-size: 12px; | |
| 334 | + | font-weight: bold; | |
| 335 | + | cursor: pointer; | |
| 336 | + | padding: 4px 0; | |
| 337 | + | } | |
| 338 | + | ||
| 339 | + | .format-examples { | |
| 340 | + | padding: 8px 0; | |
| 341 | + | font-size: 11px; | |
| 342 | + | } | |
| 343 | + | ||
| 344 | + | .format-examples h4 { | |
| 345 | + | margin: 12px 0 4px 0; | |
| 346 | + | font-size: 11px; | |
| 347 | + | color: #2d31a6; | |
| 348 | + | } | |
| 349 | + | ||
| 350 | + | .format-examples ul { | |
| 351 | + | margin: 0; | |
| 352 | + | padding-left: 16px; | |
| 353 | + | } | |
| 354 | + | ||
| 355 | + | .format-examples li { | |
| 356 | + | margin-bottom: 2px; | |
| 357 | + | } | |
| 358 | + | ||
| 359 | + | .format-examples code { | |
| 360 | + | background: #f0f0f0; | |
| 361 | + | padding: 1px 3px; | |
| 362 | + | border-radius: 2px; | |
| 363 | + | font-family: 'Courier New', monospace; | |
| 364 | + | font-size: 10px; | |
| 365 | + | } | |
| 366 | + | ||
| 367 | + | .modal-buttons { | |
| 368 | + | display: flex; | |
| 369 | + | justify-content: space-between; | |
| 370 | + | align-items: center; | |
| 371 | + | padding: 12px 16px; | |
| 372 | + | border-top: 1px solid #ddd; | |
| 373 | + | background: #f8f8f8; | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | .main-buttons { | |
| 377 | + | display: flex; | |
| 378 | + | gap: 8px; | |
| 379 | + | } | |
| 380 | + | ||
| 381 | + | .reset-button { | |
| 382 | + | padding: 4px 12px; | |
| 383 | + | font-size: 11px; | |
| 384 | + | border: 1px outset #dfdfdf; | |
| 385 | + | background: #f0f0f0; | |
| 386 | + | cursor: pointer; | |
| 387 | + | } | |
| 388 | + | ||
| 389 | + | .reset-button:hover { | |
| 390 | + | background: #e0e0e0; | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | .reset-button:disabled { | |
| 394 | + | opacity: 0.6; | |
| 395 | + | cursor: not-allowed; | |
| 396 | + | } | |
| 397 | + | ||
| 398 | + | .save-button { | |
| 399 | + | padding: 4px 16px; | |
| 400 | + | font-size: 11px; | |
| 401 | + | font-weight: bold; | |
| 402 | + | border: 1px outset #dfdfdf; | |
| 403 | + | background: #0078d4; | |
| 404 | + | color: white; | |
| 405 | + | cursor: pointer; | |
| 406 | + | } | |
| 407 | + | ||
| 408 | + | .save-button:hover:not(:disabled) { | |
| 409 | + | background: #106ebe; | |
| 410 | + | } | |
| 411 | + | ||
| 412 | + | .save-button:disabled { | |
| 413 | + | opacity: 0.6; | |
| 414 | + | cursor: not-allowed; | |
| 415 | + | background: #999; | |
| 416 | + | } | |
| 417 | + | ||
| 418 | + | button { | |
| 419 | + | padding: 4px 12px; | |
| 420 | + | font-size: 11px; | |
| 421 | + | border: 1px outset #dfdfdf; | |
| 422 | + | background: #f0f0f0; | |
| 423 | + | cursor: pointer; | |
| 424 | + | } | |
| 425 | + | ||
| 426 | + | button:hover:not(:disabled) { | |
| 427 | + | background: #e0e0e0; | |
| 428 | + | } | |
| 429 | + | ||
| 430 | + | button:disabled { | |
| 431 | + | opacity: 0.6; | |
| 432 | + | cursor: not-allowed; | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | /* Scrollbar styling */ | |
| 436 | + | .preferences-content::-webkit-scrollbar { | |
| 437 | + | width: 12px; | |
| 438 | + | } | |
| 439 | + | ||
| 440 | + | .preferences-content::-webkit-scrollbar-track { | |
| 441 | + | background: #f0f0f0; | |
| 442 | + | border: 1px inset #dfdfdf; | |
| 443 | + | } | |
| 444 | + | ||
| 445 | + | .preferences-content::-webkit-scrollbar-thumb { | |
| 446 | + | background: #c0c0c0; | |
| 447 | + | border: 1px outset #dfdfdf; | |
| 448 | + | } | |
| 449 | + | ||
| 450 | + | .preferences-content::-webkit-scrollbar-thumb:hover { | |
| 451 | + | background: #a0a0a0; | |
| 452 | + | } | |
| 453 | + | ||
| 454 | + | /* Mobile responsive */ | |
| 455 | + | @media (max-width: 768px) { | |
| 456 | + | .modal-backdrop { | |
| 457 | + | padding: 10px; | |
| 458 | + | } | |
| 459 | + | ||
| 460 | + | .modal-window { | |
| 461 | + | max-width: none; | |
| 462 | + | max-height: 95vh; | |
| 463 | + | } | |
| 464 | + | ||
| 465 | + | .window-body { | |
| 466 | + | height: 80vh; | |
| 467 | + | } | |
| 468 | + | ||
| 469 | + | .preferences-content { | |
| 470 | + | padding: 12px; | |
| 471 | + | } | |
| 472 | + | ||
| 473 | + | .setting-row { | |
| 474 | + | flex-direction: column; | |
| 475 | + | align-items: flex-start; | |
| 476 | + | gap: 4px; | |
| 477 | + | } | |
| 478 | + | ||
| 479 | + | .setting-row label { | |
| 480 | + | min-width: auto; | |
| 481 | + | } | |
| 482 | + | ||
| 483 | + | .modal-buttons { | |
| 484 | + | flex-direction: column; | |
| 485 | + | gap: 8px; | |
| 486 | + | } | |
| 487 | + | ||
| 488 | + | .main-buttons { | |
| 489 | + | order: -1; | |
| 490 | + | } | |
| 491 | + | } | |
| 492 | + | </style>⏎̸ | |
| @@ -0,0 +1,492 @@ | |||||
| 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 { | |||
| 13 | + | showModal = $bindable(false), | |||
| 14 | + | onSave | |||
| 15 | + | } = $props<{ | |||
| 16 | + | showModal: boolean; | |||
| 17 | + | onSave?: (preferences: UserTextPreferences) => void; | |||
| 18 | + | }>(); | |||
| 19 | + | ||||
| 20 | + | // State | |||
| 21 | + | let preferences = $state<UserTextPreferences>({ ...DEFAULT_USER_PREFERENCES }); | |||
| 22 | + | let currentStyle = $state<TextStyle>({ ...DEFAULT_TEXT_STYLE }); | |||
| 23 | + | let isSaving = $state(false); | |||
| 24 | + | let hasChanges = $state(false); | |||
| 25 | + | ||||
| 26 | + | // Track changes | |||
| 27 | + | $effect(() => { | |||
| 28 | + | hasChanges = JSON.stringify(preferences) !== JSON.stringify(DEFAULT_USER_PREFERENCES); | |||
| 29 | + | }); | |||
| 30 | + | ||||
| 31 | + | // Update preferences when style changes | |||
| 32 | + | $effect(() => { | |||
| 33 | + | preferences.defaultStyle = { ...currentStyle }; | |||
| 34 | + | }); | |||
| 35 | + | ||||
| 36 | + | // Load user preferences on mount | |||
| 37 | + | onMount(async () => { | |||
| 38 | + | try { | |||
| 39 | + | const response = await fetch('/api/user/text-preferences'); | |||
| 40 | + | if (response.ok) { | |||
| 41 | + | const data = await response.json(); | |||
| 42 | + | if (data.success && data.preferences) { | |||
| 43 | + | preferences = { ...DEFAULT_USER_PREFERENCES, ...data.preferences }; | |||
| 44 | + | currentStyle = { ...DEFAULT_TEXT_STYLE, ...preferences.defaultStyle }; | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | } catch (error) { | |||
| 48 | + | console.error('Failed to load user preferences:', error); | |||
| 49 | + | } | |||
| 50 | + | }); | |||
| 51 | + | ||||
| 52 | + | // Save preferences | |||
| 53 | + | async function savePreferences() { | |||
| 54 | + | if (!onSave) return; | |||
| 55 | + | ||||
| 56 | + | isSaving = true; | |||
| 57 | + | try { | |||
| 58 | + | await onSave(preferences); | |||
| 59 | + | showModal = false; | |||
| 60 | + | } catch (error) { | |||
| 61 | + | console.error('Failed to save preferences:', error); | |||
| 62 | + | alert('Failed to save preferences. Please try again.'); | |||
| 63 | + | } finally { | |||
| 64 | + | isSaving = false; | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // Reset to defaults | |||
| 69 | + | function resetToDefaults() { | |||
| 70 | + | preferences = { ...DEFAULT_USER_PREFERENCES }; | |||
| 71 | + | currentStyle = { ...DEFAULT_TEXT_STYLE }; | |||
| 72 | + | } | |||
| 73 | + | ||||
| 74 | + | // Cancel changes | |||
| 75 | + | function cancel() { | |||
| 76 | + | showModal = false; | |||
| 77 | + | } | |||
| 78 | + | ||||
| 79 | + | // Handle keyboard shortcuts | |||
| 80 | + | function handleKeydown(event: KeyboardEvent) { | |||
| 81 | + | if (event.key === 'Escape') { | |||
| 82 | + | cancel(); | |||
| 83 | + | } else if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) { | |||
| 84 | + | savePreferences(); | |||
| 85 | + | } | |||
| 86 | + | } | |||
| 87 | + | </script> | |||
| 88 | + | ||||
| 89 | + | {#if showModal} | |||
| 90 | + | <!-- Modal backdrop --> | |||
| 91 | + | <div | |||
| 92 | + | class="modal-backdrop" | |||
| 93 | + | onclick={cancel} | |||
| 94 | + | onkeydown={handleKeydown} | |||
| 95 | + | role="dialog" | |||
| 96 | + | aria-modal="true" | |||
| 97 | + | aria-labelledby="preferences-title" | |||
| 98 | + | > | |||
| 99 | + | <!-- Modal content --> | |||
| 100 | + | <div class="modal-window window" onclick={(e) => e.stopPropagation()}> | |||
| 101 | + | <div class="title-bar"> | |||
| 102 | + | <div class="title-bar-text" id="preferences-title"> | |||
| 103 | + | Text Formatting Preferences | |||
| 104 | + | </div> | |||
| 105 | + | <div class="title-bar-controls"> | |||
| 106 | + | <button onclick={cancel} aria-label="Close"></button> | |||
| 107 | + | </div> | |||
| 108 | + | </div> | |||
| 109 | + | ||||
| 110 | + | <div class="window-body"> | |||
| 111 | + | <div class="preferences-content"> | |||
| 112 | + | ||||
| 113 | + | <!-- Enable/Disable Formatting --> | |||
| 114 | + | <div class="preference-group"> | |||
| 115 | + | <label class="checkbox-label"> | |||
| 116 | + | <input | |||
| 117 | + | type="checkbox" | |||
| 118 | + | bind:checked={preferences.allowFormatting} | |||
| 119 | + | /> | |||
| 120 | + | Enable text formatting in messages | |||
| 121 | + | </label> | |||
| 122 | + | <small class="help-text"> | |||
| 123 | + | When enabled, you can use formatting options like bold, italic, colors, and gradients. | |||
| 124 | + | </small> | |||
| 125 | + | </div> | |||
| 126 | + | ||||
| 127 | + | <!-- Default Text Style --> | |||
| 128 | + | <div class="preference-group"> | |||
| 129 | + | <h3>Default Text Style</h3> | |||
| 130 | + | <p class="description"> | |||
| 131 | + | These settings will be used as defaults when you send new messages. | |||
| 132 | + | </p> | |||
| 133 | + | ||||
| 134 | + | <TextStyleSelector | |||
| 135 | + | bind:style={currentStyle} | |||
| 136 | + | previewText="This is how your messages will look by default" | |||
| 137 | + | showPreview={true} | |||
| 138 | + | compact={false} | |||
| 139 | + | /> | |||
| 140 | + | </div> | |||
| 141 | + | ||||
| 142 | + | <!-- Message Settings --> | |||
| 143 | + | <div class="preference-group"> | |||
| 144 | + | <h3>Message Settings</h3> | |||
| 145 | + | ||||
| 146 | + | <div class="setting-row"> | |||
| 147 | + | <label for="max-length">Maximum message length:</label> | |||
| 148 | + | <input | |||
| 149 | + | id="max-length" | |||
| 150 | + | type="number" | |||
| 151 | + | min="100" | |||
| 152 | + | max="2000" | |||
| 153 | + | bind:value={preferences.maxMessageLength} | |||
| 154 | + | class="length-input" | |||
| 155 | + | /> | |||
| 156 | + | <span class="unit">characters</span> | |||
| 157 | + | </div> | |||
| 158 | + | ||||
| 159 | + | <small class="help-text"> | |||
| 160 | + | Longer messages may take more time to process and display. | |||
| 161 | + | </small> | |||
| 162 | + | </div> | |||
| 163 | + | ||||
| 164 | + | <!-- Format Help --> | |||
| 165 | + | <div class="preference-group"> | |||
| 166 | + | <details class="format-help"> | |||
| 167 | + | <summary>Formatting Guide</summary> | |||
| 168 | + | <div class="format-examples"> | |||
| 169 | + | <h4>BBCode Style:</h4> | |||
| 170 | + | <ul> | |||
| 171 | + | <li><code>[b]bold text[/b]</code> → <strong>bold text</strong></li> | |||
| 172 | + | <li><code>[i]italic text[/i]</code> → <em>italic text</em></li> | |||
| 173 | + | <li><code>[u]underlined[/u]</code> → <u>underlined</u></li> | |||
| 174 | + | <li><code>[s]strikethrough[/s]</code> → <s>strikethrough</s></li> | |||
| 175 | + | <li><code>[color=red]red text[/color]</code> → <span style="color: red">red text</span></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 | + | ||||
| 189 | + | </div> | |||
| 190 | + | ||||
| 191 | + | <!-- Modal buttons --> | |||
| 192 | + | <div class="modal-buttons"> | |||
| 193 | + | <button | |||
| 194 | + | class="reset-button" | |||
| 195 | + | onclick={resetToDefaults} | |||
| 196 | + | disabled={isSaving} | |||
| 197 | + | > | |||
| 198 | + | Reset to Defaults | |||
| 199 | + | </button> | |||
| 200 | + | ||||
| 201 | + | <div class="main-buttons"> | |||
| 202 | + | <button | |||
| 203 | + | onclick={cancel} | |||
| 204 | + | disabled={isSaving} | |||
| 205 | + | > | |||
| 206 | + | Cancel | |||
| 207 | + | </button> | |||
| 208 | + | ||||
| 209 | + | <button | |||
| 210 | + | class="save-button" | |||
| 211 | + | onclick={savePreferences} | |||
| 212 | + | disabled={isSaving || !hasChanges} | |||
| 213 | + | > | |||
| 214 | + | {#if isSaving} | |||
| 215 | + | Saving... | |||
| 216 | + | {:else} | |||
| 217 | + | Save Changes | |||
| 218 | + | {/if} | |||
| 219 | + | </button> | |||
| 220 | + | </div> | |||
| 221 | + | </div> | |||
| 222 | + | </div> | |||
| 223 | + | </div> | |||
| 224 | + | </div> | |||
| 225 | + | {/if} | |||
| 226 | + | ||||
| 227 | + | <style> | |||
| 228 | + | .modal-backdrop { | |||
| 229 | + | position: fixed; | |||
| 230 | + | top: 0; | |||
| 231 | + | left: 0; | |||
| 232 | + | right: 0; | |||
| 233 | + | bottom: 0; | |||
| 234 | + | background: rgba(0, 0, 0, 0.5); | |||
| 235 | + | display: flex; | |||
| 236 | + | align-items: center; | |||
| 237 | + | justify-content: center; | |||
| 238 | + | z-index: 1000; | |||
| 239 | + | padding: 20px; | |||
| 240 | + | } | |||
| 241 | + | ||||
| 242 | + | .modal-window { | |||
| 243 | + | width: 100%; | |||
| 244 | + | max-width: 600px; | |||
| 245 | + | max-height: 90vh; | |||
| 246 | + | overflow: hidden; | |||
| 247 | + | box-shadow: 4px 4px 10px rgba(0, 0, 0, 0.3); | |||
| 248 | + | } | |||
| 249 | + | ||||
| 250 | + | .window-body { | |||
| 251 | + | display: flex; | |||
| 252 | + | flex-direction: column; | |||
| 253 | + | height: 70vh; | |||
| 254 | + | max-height: 600px; | |||
| 255 | + | } | |||
| 256 | + | ||||
| 257 | + | .preferences-content { | |||
| 258 | + | flex: 1; | |||
| 259 | + | overflow-y: auto; | |||
| 260 | + | padding: 16px; | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | .preference-group { | |||
| 264 | + | margin-bottom: 24px; | |||
| 265 | + | padding-bottom: 16px; | |||
| 266 | + | border-bottom: 1px solid #ddd; | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | .preference-group:last-child { | |||
| 270 | + | border-bottom: none; | |||
| 271 | + | margin-bottom: 0; | |||
| 272 | + | } | |||
| 273 | + | ||||
| 274 | + | .preference-group h3 { | |||
| 275 | + | margin: 0 0 8px 0; | |||
| 276 | + | font-size: 14px; | |||
| 277 | + | font-weight: bold; | |||
| 278 | + | color: #2d31a6; | |||
| 279 | + | } | |||
| 280 | + | ||||
| 281 | + | .description { | |||
| 282 | + | margin: 0 0 12px 0; | |||
| 283 | + | font-size: 12px; | |||
| 284 | + | color: #666; | |||
| 285 | + | } | |||
| 286 | + | ||||
| 287 | + | .checkbox-label { | |||
| 288 | + | display: flex; | |||
| 289 | + | align-items: center; | |||
| 290 | + | gap: 8px; | |||
| 291 | + | font-size: 12px; | |||
| 292 | + | cursor: pointer; | |||
| 293 | + | } | |||
| 294 | + | ||||
| 295 | + | .help-text { | |||
| 296 | + | display: block; | |||
| 297 | + | margin-top: 4px; | |||
| 298 | + | font-size: 11px; | |||
| 299 | + | color: #666; | |||
| 300 | + | font-style: italic; | |||
| 301 | + | } | |||
| 302 | + | ||||
| 303 | + | .setting-row { | |||
| 304 | + | display: flex; | |||
| 305 | + | align-items: center; | |||
| 306 | + | gap: 8px; | |||
| 307 | + | margin-bottom: 8px; | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | .setting-row label { | |||
| 311 | + | font-size: 12px; | |||
| 312 | + | min-width: 140px; | |||
| 313 | + | } | |||
| 314 | + | ||||
| 315 | + | .length-input { | |||
| 316 | + | width: 80px; | |||
| 317 | + | padding: 2px 4px; | |||
| 318 | + | border: 1px inset #dfdfdf; | |||
| 319 | + | background: white; | |||
| 320 | + | font-size: 11px; | |||
| 321 | + | } | |||
| 322 | + | ||||
| 323 | + | .unit { | |||
| 324 | + | font-size: 11px; | |||
| 325 | + | color: #666; | |||
| 326 | + | } | |||
| 327 | + | ||||
| 328 | + | .format-help { | |||
| 329 | + | margin-top: 8px; | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | .format-help summary { | |||
| 333 | + | font-size: 12px; | |||
| 334 | + | font-weight: bold; | |||
| 335 | + | cursor: pointer; | |||
| 336 | + | padding: 4px 0; | |||
| 337 | + | } | |||
| 338 | + | ||||
| 339 | + | .format-examples { | |||
| 340 | + | padding: 8px 0; | |||
| 341 | + | font-size: 11px; | |||
| 342 | + | } | |||
| 343 | + | ||||
| 344 | + | .format-examples h4 { | |||
| 345 | + | margin: 12px 0 4px 0; | |||
| 346 | + | font-size: 11px; | |||
| 347 | + | color: #2d31a6; | |||
| 348 | + | } | |||
| 349 | + | ||||
| 350 | + | .format-examples ul { | |||
| 351 | + | margin: 0; | |||
| 352 | + | padding-left: 16px; | |||
| 353 | + | } | |||
| 354 | + | ||||
| 355 | + | .format-examples li { | |||
| 356 | + | margin-bottom: 2px; | |||
| 357 | + | } | |||
| 358 | + | ||||
| 359 | + | .format-examples code { | |||
| 360 | + | background: #f0f0f0; | |||
| 361 | + | padding: 1px 3px; | |||
| 362 | + | border-radius: 2px; | |||
| 363 | + | font-family: 'Courier New', monospace; | |||
| 364 | + | font-size: 10px; | |||
| 365 | + | } | |||
| 366 | + | ||||
| 367 | + | .modal-buttons { | |||
| 368 | + | display: flex; | |||
| 369 | + | justify-content: space-between; | |||
| 370 | + | align-items: center; | |||
| 371 | + | padding: 12px 16px; | |||
| 372 | + | border-top: 1px solid #ddd; | |||
| 373 | + | background: #f8f8f8; | |||
| 374 | + | } | |||
| 375 | + | ||||
| 376 | + | .main-buttons { | |||
| 377 | + | display: flex; | |||
| 378 | + | gap: 8px; | |||
| 379 | + | } | |||
| 380 | + | ||||
| 381 | + | .reset-button { | |||
| 382 | + | padding: 4px 12px; | |||
| 383 | + | font-size: 11px; | |||
| 384 | + | border: 1px outset #dfdfdf; | |||
| 385 | + | background: #f0f0f0; | |||
| 386 | + | cursor: pointer; | |||
| 387 | + | } | |||
| 388 | + | ||||
| 389 | + | .reset-button:hover { | |||
| 390 | + | background: #e0e0e0; | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | .reset-button:disabled { | |||
| 394 | + | opacity: 0.6; | |||
| 395 | + | cursor: not-allowed; | |||
| 396 | + | } | |||
| 397 | + | ||||
| 398 | + | .save-button { | |||
| 399 | + | padding: 4px 16px; | |||
| 400 | + | font-size: 11px; | |||
| 401 | + | font-weight: bold; | |||
| 402 | + | border: 1px outset #dfdfdf; | |||
| 403 | + | background: #0078d4; | |||
| 404 | + | color: white; | |||
| 405 | + | cursor: pointer; | |||
| 406 | + | } | |||
| 407 | + | ||||
| 408 | + | .save-button:hover:not(:disabled) { | |||
| 409 | + | background: #106ebe; | |||
| 410 | + | } | |||
| 411 | + | ||||
| 412 | + | .save-button:disabled { | |||
| 413 | + | opacity: 0.6; | |||
| 414 | + | cursor: not-allowed; | |||
| 415 | + | background: #999; | |||
| 416 | + | } | |||
| 417 | + | ||||
| 418 | + | button { | |||
| 419 | + | padding: 4px 12px; | |||
| 420 | + | font-size: 11px; | |||
| 421 | + | border: 1px outset #dfdfdf; | |||
| 422 | + | background: #f0f0f0; | |||
| 423 | + | cursor: pointer; | |||
| 424 | + | } | |||
| 425 | + | ||||
| 426 | + | button:hover:not(:disabled) { | |||
| 427 | + | background: #e0e0e0; | |||
| 428 | + | } | |||
| 429 | + | ||||
| 430 | + | button:disabled { | |||
| 431 | + | opacity: 0.6; | |||
| 432 | + | cursor: not-allowed; | |||
| 433 | + | } | |||
| 434 | + | ||||
| 435 | + | /* Scrollbar styling */ | |||
| 436 | + | .preferences-content::-webkit-scrollbar { | |||
| 437 | + | width: 12px; | |||
| 438 | + | } | |||
| 439 | + | ||||
| 440 | + | .preferences-content::-webkit-scrollbar-track { | |||
| 441 | + | background: #f0f0f0; | |||
| 442 | + | border: 1px inset #dfdfdf; | |||
| 443 | + | } | |||
| 444 | + | ||||
| 445 | + | .preferences-content::-webkit-scrollbar-thumb { | |||
| 446 | + | background: #c0c0c0; | |||
| 447 | + | border: 1px outset #dfdfdf; | |||
| 448 | + | } | |||
| 449 | + | ||||
| 450 | + | .preferences-content::-webkit-scrollbar-thumb:hover { | |||
| 451 | + | background: #a0a0a0; | |||
| 452 | + | } | |||
| 453 | + | ||||
| 454 | + | /* Mobile responsive */ | |||
| 455 | + | @media (max-width: 768px) { | |||
| 456 | + | .modal-backdrop { | |||
| 457 | + | padding: 10px; | |||
| 458 | + | } | |||
| 459 | + | ||||
| 460 | + | .modal-window { | |||
| 461 | + | max-width: none; | |||
| 462 | + | max-height: 95vh; | |||
| 463 | + | } | |||
| 464 | + | ||||
| 465 | + | .window-body { | |||
| 466 | + | height: 80vh; | |||
| 467 | + | } | |||
| 468 | + | ||||
| 469 | + | .preferences-content { | |||
| 470 | + | padding: 12px; | |||
| 471 | + | } | |||
| 472 | + | ||||
| 473 | + | .setting-row { | |||
| 474 | + | flex-direction: column; | |||
| 475 | + | align-items: flex-start; | |||
| 476 | + | gap: 4px; | |||
| 477 | + | } | |||
| 478 | + | ||||
| 479 | + | .setting-row label { | |||
| 480 | + | min-width: auto; | |||
| 481 | + | } | |||
| 482 | + | ||||
| 483 | + | .modal-buttons { | |||
| 484 | + | flex-direction: column; | |||
| 485 | + | gap: 8px; | |||
| 486 | + | } | |||
| 487 | + | ||||
| 488 | + | .main-buttons { | |||
| 489 | + | order: -1; | |||
| 490 | + | } | |||
| 491 | + | } | |||
| 492 | + | </style>⏎̸ | |||
M src/routes/+page.svelte +4 -1
| @@ -2,7 +2,10 @@ | |||
| 2 | 2 | import ChatRoom from '$lib/components/chat-room.svelte'; | |
| 3 | 3 | import DesktopIcons from '$lib/components/desktop-icons.svelte'; | |
| 4 | 4 | import AimLogin from '$lib/components/aim-login.svelte'; | |
| 5 | + | import type { PageData } from './$types'; | |
| 5 | 6 | ||
| 7 | + | let { data }: { data: PageData } = $props(); | |
| 8 | + | ||
| 6 | 9 | let showChatRoom = $state(true); | |
| 7 | 10 | let showAuth = $state(false); | |
| 8 | 11 | ||
| @@ -15,7 +18,7 @@ | |||
| 15 | 18 | <DesktopIcons bind:showChatRoom bind:showAuth /> | |
| 16 | 19 | ||
| 17 | 20 | {#if showChatRoom} | |
| 18 | - | <ChatRoom bind:showChatRoom /> | |
| 21 | + | <ChatRoom bind:showChatRoom initialTextStyle={data.lastTextStyle} /> | |
| 19 | 22 | {/if} | |
| 20 | 23 | ||
| 21 | 24 | {#if showAuth} | |
| @@ -2,7 +2,10 @@ | |||||
| 2 | import ChatRoom from '$lib/components/chat-room.svelte'; | 2 | import ChatRoom from '$lib/components/chat-room.svelte'; | ||
| 3 | import DesktopIcons from '$lib/components/desktop-icons.svelte'; | 3 | import DesktopIcons from '$lib/components/desktop-icons.svelte'; | ||
| 4 | import AimLogin from '$lib/components/aim-login.svelte'; | 4 | import AimLogin from '$lib/components/aim-login.svelte'; | ||
| 5 | + | import type { PageData } from './$types'; | |||
| 5 | 6 | ||||
| 7 | + | let { data }: { data: PageData } = $props(); | |||
| 8 | + | ||||
| 6 | let showChatRoom = $state(true); | 9 | let showChatRoom = $state(true); | ||
| 7 | let showAuth = $state(false); | 10 | let showAuth = $state(false); | ||
| 8 | 11 | ||||
| @@ -15,7 +18,7 @@ | |||||
| 15 | <DesktopIcons bind:showChatRoom bind:showAuth /> | 18 | <DesktopIcons bind:showChatRoom bind:showAuth /> | ||
| 16 | 19 | ||||
| 17 | {#if showChatRoom} | 20 | {#if showChatRoom} | ||
| 18 | - | <ChatRoom bind:showChatRoom /> | 21 | + | <ChatRoom bind:showChatRoom initialTextStyle={data.lastTextStyle} /> |
| 19 | {/if} | 22 | {/if} | ||
| 20 | 23 | ||||
| 21 | {#if showAuth} | 24 | {#if showAuth} | ||