Capstone project. I'm ngl it's vibe-coded and it's only here so I can mess around with it
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add theme toggles and polish the UXET shell

- Introduce persisted light and dark themes
- Refresh setup, calibration, and debrief layouts
- Rename debug copy and improve session status messaging

+461 -256
+13 -12
.uxet-server.log
··· 1 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET / HTTP/1.1" 200 - 2 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /index.css HTTP/1.1" 200 - 3 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/main.js HTTP/1.1" 200 - 4 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/session.js HTTP/1.1" 200 - 5 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/tracker.js HTTP/1.1" 200 - 6 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/gazeTracker.js HTTP/1.1" 200 - 7 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/iframeBridge.js HTTP/1.1" 200 - 8 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/calibration.js HTTP/1.1" 200 - 9 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/winConditions.js HTTP/1.1" 200 - 10 - 127.0.0.1 - - [23/Mar/2026 23:27:04] "GET /js/debriefRenderer.js HTTP/1.1" 200 - 11 - 127.0.0.1 - - [23/Mar/2026 23:27:05] code 404, message File not found 12 - 127.0.0.1 - - [23/Mar/2026 23:27:05] "GET /favicon.ico HTTP/1.1" 404 - 1 + 127.0.0.1 - - [29/Mar/2026 14:21:51] "GET / HTTP/1.1" 200 - 2 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /index.css HTTP/1.1" 200 - 3 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/main.js HTTP/1.1" 200 - 4 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/session.js HTTP/1.1" 200 - 5 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/gazeTracker.js HTTP/1.1" 200 - 6 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/tracker.js HTTP/1.1" 200 - 7 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/calibration.js HTTP/1.1" 200 - 8 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/iframeBridge.js HTTP/1.1" 200 - 9 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/winConditions.js HTTP/1.1" 200 - 10 + 127.0.0.1 - - [29/Mar/2026 14:21:52] "GET /js/debriefRenderer.js HTTP/1.1" 200 - 11 + 127.0.0.1 - - [29/Mar/2026 14:21:53] code 404, message File not found 12 + 127.0.0.1 - - [29/Mar/2026 14:21:53] "GET /favicon.ico HTTP/1.1" 404 - 13 + 127.0.0.1 - - [29/Mar/2026 14:22:07] "GET /testable-apps/shop-app/index.html?theme=dark HTTP/1.1" 200 -
-1
.uxet-server.pid
··· 1 - 46419
+325 -193
index.css
··· 1 1 :root { 2 - --bg: #f2efe8; 3 - --panel: rgba(255, 251, 245, 0.92); 4 - --panel-solid: #fffaf3; 5 - --ink: #1b1a17; 6 - --muted: #6b675f; 7 - --line: rgba(27, 26, 23, 0.12); 8 - --accent: #a33b24; 9 - --accent-strong: #7e2817; 10 - --shadow: 0 24px 60px rgba(41, 31, 22, 0.12); 2 + --font-sans: "IBM Plex Sans", "Avenir Next", "Helvetica Neue", sans-serif; 3 + --space-1: 4px; 4 + --space-2: 8px; 5 + --space-3: 12px; 6 + --space-4: 16px; 7 + --space-5: 24px; 8 + --space-6: 32px; 9 + --radius-sm: 8px; 10 + --radius-md: 10px; 11 + --radius-lg: 12px; 12 + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.08); 13 + --shadow-md: 0 10px 28px rgba(0, 0, 0, 0.14); 14 + } 15 + 16 + html[data-theme="light"] { 17 + --bg: #f3f5f4; 18 + --surface: #fbfcfb; 19 + --surface-2: #eef1ef; 20 + --surface-3: #e6ebe8; 21 + --text: #171a19; 22 + --muted: #5f6764; 23 + --border: #d7ddda; 24 + --primary: #7a4b2a; 25 + --secondary: #566b5d; 26 + --success: #2f6a4e; 27 + --danger: #93453c; 28 + --overlay: rgba(243, 245, 244, 0.96); 29 + --overlay-strong: rgba(243, 245, 244, 0.985); 30 + --placeholder: #c9d0cc; 31 + } 32 + 33 + html[data-theme="dark"] { 34 + --bg: #111315; 35 + --surface: #171a1c; 36 + --surface-2: #202427; 37 + --surface-3: #282d31; 38 + --text: #eef1ef; 39 + --muted: #a1aaa6; 40 + --border: #31383c; 41 + --primary: #b8845f; 42 + --secondary: #7e9585; 43 + --success: #6ca27f; 44 + --danger: #c7897a; 45 + --overlay: rgba(17, 19, 21, 0.96); 46 + --overlay-strong: rgba(17, 19, 21, 0.99); 47 + --placeholder: #3a4348; 11 48 } 12 49 13 50 * { 14 51 box-sizing: border-box; 15 52 } 16 53 54 + html, 55 + body { 56 + min-height: 100%; 57 + } 58 + 17 59 body { 18 60 margin: 0; 19 - min-height: 100vh; 20 - background: 21 - radial-gradient(circle at top left, rgba(222, 177, 120, 0.22), transparent 28%), 22 - radial-gradient(circle at bottom right, rgba(163, 59, 36, 0.16), transparent 26%), 23 - linear-gradient(180deg, #f8f4ec 0%, #efe8dd 100%); 24 - color: var(--ink); 25 - font-family: Georgia, 'Times New Roman', serif; 61 + background: var(--bg); 62 + color: var(--text); 63 + font-family: var(--font-sans); 26 64 line-height: 1.45; 27 65 } 28 66 ··· 38 76 } 39 77 40 78 button, 79 + select, 80 + input, 81 + textarea { 82 + border: 1px solid var(--border); 83 + border-radius: var(--radius-sm); 84 + } 85 + 86 + button, 41 87 select { 42 - border-radius: 14px; 43 - border: 1px solid var(--line); 44 - padding: 12px 16px; 45 - background: var(--panel-solid); 46 - color: var(--ink); 88 + min-height: 40px; 89 + padding: 0 var(--space-4); 90 + background: var(--surface); 91 + color: var(--text); 47 92 } 48 93 49 94 button { 50 95 cursor: pointer; 51 - transition: transform 0.16s ease, background 0.16s ease, border-color 0.16s ease; 96 + transition: background-color 160ms ease, border-color 160ms ease, color 160ms ease, opacity 160ms ease; 52 97 } 53 98 54 - button:hover:not(:disabled) { 55 - transform: translateY(-1px); 56 - border-color: rgba(163, 59, 36, 0.35); 99 + button:hover:not(:disabled), 100 + select:hover:not(:disabled), 101 + input:hover:not(:disabled), 102 + textarea:hover:not(:disabled) { 103 + border-color: var(--secondary); 57 104 } 58 105 59 106 button:disabled { ··· 61 108 cursor: not-allowed; 62 109 } 63 110 111 + button:focus-visible, 112 + select:focus-visible, 113 + input:focus-visible, 114 + textarea:focus-visible { 115 + outline: 2px solid color-mix(in srgb, var(--secondary) 78%, white 22%); 116 + outline-offset: 2px; 117 + } 118 + 119 + code { 120 + font-family: "IBM Plex Mono", "SFMono-Regular", Consolas, monospace; 121 + } 122 + 64 123 #load-app-btn, 65 - #export-btn, 66 - #start-test-btn { 67 - background: linear-gradient(135deg, var(--accent), var(--accent-strong)); 68 - color: #fff8f3; 69 - border: none; 124 + #start-test-btn, 125 + #retry-calibration-btn, 126 + .checkout-btn, 127 + .add-to-cart-btn, 128 + .btn-primary, 129 + .subscription-form button, 130 + .action-row button[type="submit"] { 131 + background: var(--primary); 132 + border-color: var(--primary); 133 + color: #f7f5f2; 70 134 } 71 135 72 - #app-container { 136 + #app-container, 137 + #setup-shell, 138 + #debrief-shell { 73 139 min-height: 100vh; 74 140 } 75 141 76 142 #setup-shell, 77 143 #debrief-shell { 78 - min-height: 100vh; 79 - padding: 24px; 144 + padding: var(--space-5); 80 145 } 81 146 82 147 #setup-shell { 83 148 display: grid; 84 149 grid-template-rows: auto auto 1fr; 85 - gap: 16px; 150 + gap: var(--space-4); 151 + } 152 + 153 + #controls, 154 + #status-bar, 155 + #shell-main, 156 + .overlay-card, 157 + .screen-card, 158 + #session-debug-drawer, 159 + .start-card, 160 + #calibration-hud { 161 + background: var(--surface); 162 + border: 1px solid var(--border); 163 + box-shadow: var(--shadow-sm); 86 164 } 87 165 88 166 #controls, 89 167 #status-bar, 168 + #shell-main, 90 169 .overlay-card, 91 170 .screen-card { 92 - border: 1px solid var(--line); 93 - background: var(--panel); 94 - backdrop-filter: blur(12px); 95 - box-shadow: var(--shadow); 171 + border-radius: var(--radius-lg); 96 172 } 97 173 98 174 #controls { 99 - padding: 20px; 100 - border-radius: 22px; 175 + padding: var(--space-5); 176 + display: grid; 177 + gap: var(--space-4); 178 + } 179 + 180 + .toolbar, 181 + .control-row { 182 + display: flex; 183 + align-items: start; 184 + justify-content: space-between; 185 + gap: var(--space-4); 186 + flex-wrap: wrap; 101 187 } 102 188 103 - .brand-block h1, 104 - .panel-header h2, 189 + .toolbar-copy h1, 190 + .workspace-header h2, 191 + .debrief-header h2, 105 192 .start-card h2 { 106 193 margin: 0; 107 - font-size: clamp(1.8rem, 2vw, 2.5rem); 108 - letter-spacing: -0.03em; 194 + font-size: 1.45rem; 195 + line-height: 1.2; 109 196 } 110 197 111 - .brand-block p, 112 - .panel-header p, 113 - #screen-gallery-label, 198 + .toolbar-copy p, 199 + .workspace-header p, 200 + .debrief-header p, 114 201 .start-card p, 115 202 .debug-copy { 116 - margin: 6px 0 0; 203 + margin: var(--space-2) 0 0; 117 204 color: var(--muted); 118 205 } 119 206 120 - .control-row { 121 - margin-top: 16px; 207 + .toolbar-actions, 208 + .primary-actions { 122 209 display: flex; 210 + align-items: center; 211 + gap: var(--space-3); 123 212 flex-wrap: wrap; 124 - gap: 16px; 125 - align-items: end; 126 - justify-content: space-between; 127 213 } 128 214 129 215 .field { 130 216 display: grid; 131 - gap: 6px; 132 - min-width: min(520px, 100%); 217 + gap: var(--space-2); 218 + min-width: min(480px, 100%); 133 219 } 134 220 135 221 .field span, 136 - .label, 137 - .start-label { 138 - font-size: 0.78rem; 139 - text-transform: uppercase; 140 - letter-spacing: 0.08em; 222 + .label { 223 + font-size: 0.82rem; 141 224 color: var(--muted); 142 225 } 143 226 144 - .primary-actions, 145 - .debug-shell, 146 - #debug-panel { 147 - display: flex; 148 - align-items: center; 149 - gap: 10px; 150 - flex-wrap: wrap; 227 + #app-select { 228 + width: 100%; 229 + background: var(--surface); 230 + color: var(--text); 151 231 } 152 232 153 - .debug-toggle { 233 + .theme-toggle { 234 + display: inline-flex; 235 + padding: 2px; 236 + border: 1px solid var(--border); 237 + border-radius: var(--radius-sm); 238 + background: var(--surface-2); 239 + } 240 + 241 + .theme-option { 242 + min-width: 72px; 243 + border: none; 244 + border-radius: 6px; 154 245 background: transparent; 246 + color: var(--muted); 247 + } 248 + 249 + .theme-option.active { 250 + background: var(--surface); 251 + color: var(--text); 252 + } 253 + 254 + .debug-shell { 255 + display: grid; 256 + gap: var(--space-3); 257 + } 258 + 259 + .debug-toggle { 260 + justify-self: start; 261 + } 262 + 263 + #debug-panel { 264 + display: flex; 265 + flex-wrap: wrap; 266 + gap: var(--space-3); 267 + align-items: center; 268 + padding-top: var(--space-1); 155 269 } 156 270 157 271 .debug-checkbox { 158 272 display: inline-flex; 159 273 align-items: center; 160 - gap: 8px; 161 - color: var(--ink); 274 + gap: var(--space-2); 275 + color: var(--text); 162 276 } 163 277 164 278 .debug-checkbox input { ··· 167 281 } 168 282 169 283 #status-bar { 170 - border-radius: 18px; 171 - padding: 14px 18px; 284 + padding: var(--space-4) var(--space-5); 172 285 display: grid; 173 286 grid-template-columns: repeat(4, minmax(0, 1fr)); 174 - gap: 12px; 287 + gap: var(--space-4); 175 288 } 176 289 177 290 #status-bar > div, 178 - #stats-container li { 291 + .stat-block { 179 292 display: grid; 180 - gap: 4px; 293 + gap: var(--space-1); 294 + } 295 + 296 + #status-bar strong, 297 + .stat-block strong { 298 + font-size: 0.97rem; 181 299 } 182 300 183 301 #shell-main { 184 - border-radius: 24px; 185 - border: 1px solid var(--line); 186 - background: rgba(255, 249, 240, 0.72); 187 - min-height: 48vh; 302 + padding: var(--space-5); 188 303 display: grid; 304 + grid-template-rows: auto 1fr; 305 + gap: var(--space-5); 306 + } 307 + 308 + .workspace-header { 309 + display: flex; 310 + justify-content: space-between; 311 + align-items: end; 312 + gap: var(--space-4); 189 313 } 190 314 191 315 #iframe-placeholder { 192 316 display: grid; 193 317 place-items: center; 318 + min-height: 380px; 319 + padding: var(--space-6); 320 + border: 1px dashed var(--placeholder); 321 + border-radius: var(--radius-md); 194 322 color: var(--muted); 195 - font-size: 1.1rem; 196 - padding: 32px; 197 323 text-align: center; 324 + background: 325 + linear-gradient(180deg, transparent 0, transparent calc(100% - 1px), var(--border) calc(100% - 1px)), 326 + linear-gradient(90deg, transparent 0, transparent calc(100% - 1px), var(--border) calc(100% - 1px)); 327 + background-size: 100% 48px, 48px 100%; 198 328 } 199 329 200 330 #session-stage { ··· 204 334 background: #000; 205 335 } 206 336 337 + #test-iframe, 338 + #calibration-screen, 339 + #start-curtain, 340 + #start-overlay, 341 + #calibration-failure-overlay { 342 + position: absolute; 343 + inset: 0; 344 + } 345 + 346 + #test-iframe { 347 + width: 100vw; 348 + height: 100vh; 349 + border: none; 350 + background: #fff; 351 + } 352 + 207 353 .session-debug-toggle { 208 354 position: absolute; 209 - top: 16px; 210 - right: 16px; 211 - z-index: 5; 212 - background: rgba(255, 250, 243, 0.92); 355 + top: var(--space-4); 356 + right: var(--space-4); 357 + z-index: 7; 358 + min-height: 36px; 359 + padding: 0 var(--space-3); 360 + background: var(--overlay); 213 361 } 214 362 215 363 #session-debug-drawer { 216 364 position: absolute; 217 - top: 64px; 218 - right: 16px; 219 - z-index: 5; 365 + top: 56px; 366 + right: var(--space-4); 367 + z-index: 7; 220 368 width: min(320px, calc(100vw - 32px)); 221 - padding: 14px; 222 - border-radius: 16px; 223 - background: rgba(255, 250, 243, 0.94); 224 - border: 1px solid rgba(27, 26, 23, 0.12); 225 - box-shadow: 0 16px 36px rgba(17, 12, 10, 0.18); 226 - backdrop-filter: blur(10px); 369 + padding: var(--space-4); 370 + border-radius: var(--radius-md); 371 + background: var(--overlay); 227 372 } 228 373 229 374 .session-debug-header { 230 - margin-bottom: 10px; 375 + margin-bottom: var(--space-3); 231 376 } 232 377 233 378 .session-debug-actions { 234 379 display: grid; 235 - gap: 10px; 236 - } 237 - 238 - #test-iframe, 239 - #calibration-screen, 240 - #start-overlay, 241 - #calibration-failure-overlay { 242 - position: absolute; 243 - inset: 0; 244 - } 245 - 246 - #test-iframe { 247 - width: 100vw; 248 - height: 100vh; 249 - border: none; 250 - background: #fff; 380 + gap: var(--space-3); 251 381 } 252 382 253 383 #calibration-screen { ··· 257 387 #calibration-veil { 258 388 position: absolute; 259 389 inset: 0; 260 - background: rgba(14, 12, 10, 0.16); 390 + background: rgba(0, 0, 0, 0.28); 261 391 } 262 392 263 393 #calibration-hud { 264 394 position: absolute; 265 - top: 14px; 266 - left: 14px; 267 - right: 14px; 395 + top: var(--space-4); 396 + left: var(--space-4); 397 + right: var(--space-4); 398 + z-index: 3; 399 + min-height: 52px; 268 400 display: flex; 269 401 flex-wrap: wrap; 270 - gap: 10px 16px; 402 + gap: var(--space-2) var(--space-4); 271 403 align-items: center; 272 - min-height: 56px; 273 - padding: 12px 14px; 274 - border-radius: 16px; 275 - background: rgba(255, 250, 243, 0.9); 276 - border: 1px solid rgba(27, 26, 23, 0.12); 277 - box-shadow: 0 16px 36px rgba(17, 12, 10, 0.14); 278 - backdrop-filter: blur(10px); 279 - pointer-events: none; 404 + padding: var(--space-3) var(--space-4); 405 + border-radius: var(--radius-md); 406 + background: var(--overlay); 280 407 } 281 408 282 409 #calibration-stage { ··· 286 413 287 414 #calibration-target { 288 415 position: absolute; 289 - width: 64px; 290 - height: 64px; 291 - border-radius: 999px; 416 + width: 58px; 417 + height: 58px; 292 418 transform: translate(-50%, -50%); 293 419 display: flex; 294 420 align-items: center; 295 421 justify-content: center; 296 - border: none; 297 - color: white; 298 - font-size: 1.15rem; 299 - background: radial-gradient(circle at 30% 30%, #ffcf70, #b03922 65%, #7e2817); 300 - box-shadow: 0 18px 36px rgba(126, 40, 23, 0.35); 422 + border: 2px solid color-mix(in srgb, var(--primary) 72%, white 28%); 423 + border-radius: 999px; 424 + background: color-mix(in srgb, var(--primary) 86%, black 14%); 425 + color: #f7f5f2; 426 + font-weight: 600; 427 + box-shadow: var(--shadow-md); 428 + } 429 + 430 + #start-curtain { 431 + z-index: 3; 432 + background: var(--overlay-strong); 301 433 } 302 434 303 435 #start-overlay, 304 436 #calibration-failure-overlay { 305 - z-index: 3; 437 + z-index: 4; 306 438 display: grid; 307 439 place-items: center; 308 - background: rgba(14, 12, 10, 0.2); 309 - padding: 24px; 440 + padding: var(--space-5); 310 441 } 311 442 312 - .failure-card { 313 - border: 1px solid rgba(126, 40, 23, 0.18); 443 + .start-card { 444 + width: min(440px, calc(100vw - 32px)); 445 + padding: var(--space-5); 446 + border-radius: var(--radius-lg); 447 + background: var(--surface); 314 448 } 315 449 316 - .start-card { 317 - width: min(420px, calc(100vw - 32px)); 318 - padding: 22px; 319 - border-radius: 20px; 320 - background: rgba(255, 250, 243, 0.96); 321 - border: 1px solid rgba(27, 26, 23, 0.12); 322 - box-shadow: var(--shadow); 450 + .start-note { 451 + font-size: 0.95rem; 323 452 } 324 453 325 - #debrief-shell { 326 - display: block; 454 + .failure-card { 455 + border-color: color-mix(in srgb, var(--danger) 45%, var(--border) 55%); 327 456 } 328 457 329 458 #debrief-screen { 330 - background: rgba(247, 242, 234, 0.95); 331 - border-radius: 22px; 332 - padding: 20px; 333 - min-height: calc(100vh - 48px); 334 - border: 1px solid var(--line); 459 + display: grid; 460 + gap: var(--space-5); 335 461 } 336 462 337 - .overlay-card { 338 - border-radius: 22px; 463 + .debrief-header { 464 + display: flex; 465 + justify-content: space-between; 466 + gap: var(--space-4); 467 + align-items: end; 468 + flex-wrap: wrap; 339 469 } 340 470 341 471 .debrief-panel { 342 - padding: 20px; 472 + padding: var(--space-5); 343 473 } 344 474 345 475 #stats-container { 346 - margin-top: 18px; 347 476 display: grid; 348 - grid-template-columns: repeat(2, minmax(0, 1fr)); 349 - gap: 20px; 477 + grid-template-columns: repeat(3, minmax(0, 1fr)); 478 + gap: var(--space-4); 350 479 } 351 480 352 - #stats-container ul { 353 - list-style: none; 354 - margin: 0; 355 - padding: 0; 356 - display: grid; 357 - gap: 10px; 481 + .stat-block { 482 + min-height: 80px; 483 + padding: var(--space-4); 484 + border: 1px solid var(--border); 485 + border-radius: var(--radius-md); 486 + background: var(--surface-2); 358 487 } 359 488 360 - #stats-container li span { 361 - font-size: 0.85rem; 489 + .stat-block span, 490 + .screen-card-header p, 491 + .screen-card-stats span { 362 492 color: var(--muted); 493 + font-size: 0.9rem; 363 494 } 364 495 365 496 #screen-gallery { 366 - margin-top: 20px; 367 497 display: grid; 368 - gap: 18px; 498 + gap: var(--space-4); 369 499 } 370 500 371 501 .screen-card { 372 - border-radius: 22px; 373 - padding: 18px; 502 + padding: var(--space-4); 374 503 } 375 504 376 505 .screen-card-header { 377 506 display: flex; 378 507 justify-content: space-between; 379 - gap: 16px; 508 + gap: var(--space-4); 380 509 align-items: start; 381 - margin-bottom: 14px; 510 + margin-bottom: var(--space-4); 382 511 } 383 512 384 513 .screen-card-header h4 { 385 514 margin: 0; 386 - font-size: 1.15rem; 387 - } 388 - 389 - .screen-card-header p, 390 - .screen-card-stats span { 391 - margin: 4px 0 0; 392 - color: var(--muted); 393 - font-size: 0.9rem; 515 + font-size: 1rem; 394 516 } 395 517 396 518 .screen-card-stats { 397 519 display: grid; 398 520 justify-items: end; 399 - gap: 4px; 521 + gap: var(--space-1); 400 522 } 401 523 402 524 .screen-card-canvas-wrap { 403 - border-radius: 16px; 404 525 overflow: hidden; 405 - border: 1px solid var(--line); 526 + border: 1px solid var(--border); 527 + border-radius: var(--radius-md); 406 528 background: #fff; 407 529 } 408 530 ··· 413 535 } 414 536 415 537 .screen-card-fallback { 416 - min-height: 140px; 538 + min-height: 160px; 417 539 display: grid; 418 540 place-items: center; 419 - padding: 24px; 420 - color: var(--muted); 421 541 text-align: center; 542 + padding: var(--space-5); 543 + color: var(--muted); 422 544 } 423 545 424 546 .hidden { 425 547 display: none !important; 426 548 } 427 549 428 - @media (max-width: 900px) { 550 + @media (max-width: 980px) { 551 + #status-bar, 552 + #stats-container { 553 + grid-template-columns: repeat(2, minmax(0, 1fr)); 554 + } 555 + } 556 + 557 + @media (max-width: 720px) { 429 558 #setup-shell, 430 559 #debrief-shell { 431 - padding: 16px; 560 + padding: var(--space-4); 432 561 } 433 562 434 563 #status-bar, ··· 436 565 grid-template-columns: 1fr; 437 566 } 438 567 568 + #controls, 569 + #shell-main, 570 + .debrief-panel { 571 + padding: var(--space-4); 572 + } 573 + 439 574 #calibration-hud { 440 - top: 10px; 441 - left: 10px; 442 - right: 10px; 443 - gap: 8px 12px; 444 - padding: 10px 12px; 445 - min-height: 64px; 575 + top: var(--space-3); 576 + left: var(--space-3); 577 + right: var(--space-3); 446 578 } 447 579 448 580 .screen-card-header {
+66 -43
index.html
··· 1 1 <!DOCTYPE html> 2 - <html lang="en"> 2 + <html lang="en" data-theme="dark"> 3 3 4 4 <head> 5 5 <meta charset="UTF-8"> ··· 14 14 <div id="app-container"> 15 15 <section id="setup-shell"> 16 16 <header id="controls"> 17 - <div class="brand-block"> 18 - <h1>UXET</h1> 19 - <p>Operator shell for selecting apps, exporting sessions, and reviewing page-level gaze coverage.</p> 17 + <div class="toolbar"> 18 + <div class="toolbar-copy"> 19 + <h1>UXET</h1> 20 + <p>Prepare a session, preload the app, and capture gaze data without changing the test flow.</p> 21 + </div> 22 + 23 + <div class="toolbar-actions"> 24 + <div class="theme-toggle" role="group" aria-label="Theme selection"> 25 + <button id="theme-dark-btn" class="theme-option" type="button">Dark</button> 26 + <button id="theme-light-btn" class="theme-option" type="button">Light</button> 27 + </div> 28 + </div> 20 29 </div> 21 30 22 31 <div class="control-row"> 23 - <label class="field"> 24 - <span>App Under Test</span> 32 + <label class="field field-app" for="app-select"> 33 + <span>App under test</span> 25 34 <select id="app-select"> 26 35 <option value="">Select an app...</option> 27 36 <option value="testable-apps/shop-app/index.html" data-task="Find and purchase a blue t-shirt" 28 37 data-win="selector:.checkout-success.active">ShopEasy Store</option> 29 38 <option value="testable-apps/example-app/index.html" 30 - data-task="Fill out the contact form with your details" data-win="text:Form submitted successfully"> 39 + data-task="Fill out the contact form with your details" 40 + data-win="text:Form submitted successfully"> 31 41 Example Form App 32 42 </option> 33 43 <option value="testable-apps/long-page-app/index.html" ··· 37 47 </label> 38 48 39 49 <div class="primary-actions"> 40 - <button id="load-app-btn">Load App</button> 41 - <button id="export-btn" disabled>Export Data</button> 42 - <button id="reset-btn">Reset</button> 50 + <button id="load-app-btn" type="button">Load App</button> 51 + <button id="export-btn" type="button" disabled>Export Data</button> 52 + <button id="reset-btn" type="button">Reset</button> 43 53 </div> 44 54 </div> 45 55 46 56 <div class="debug-shell"> 47 - <button id="debug-toggle-btn" class="debug-toggle" type="button">Debug Controls</button> 57 + <button id="debug-toggle-btn" class="debug-toggle" type="button" aria-expanded="false">Debug Controls</button> 48 58 <div id="debug-panel" class="hidden"> 49 59 <button id="debug-skip-calibration" type="button">Skip Calibration</button> 50 60 <label class="debug-checkbox"> 51 61 <input id="debug-mouse-gaze-toggle" type="checkbox"> 52 - <span>Skip Eye Tracking (use mouse as gaze)</span> 62 + <span>Use mouse as gaze</span> 53 63 </label> 54 - <button id="debug-exit-test" type="button" disabled>Manually Exit Test</button> 55 - <p class="debug-copy">Debug end test shortcut: <code>Shift+Escape</code></p> 64 + <button id="debug-exit-test" type="button" disabled>End Test</button> 65 + <p class="debug-copy">Session exit shortcut: <code>Shift+Escape</code></p> 56 66 </div> 57 67 </div> 58 68 </header> 59 69 60 - <section id="status-bar"> 70 + <section id="status-bar" aria-live="polite"> 61 71 <div><span class="label">State</span><strong id="session-status">idle</strong></div> 62 72 <div><span class="label">Time</span><strong id="session-timer">00:00:00</strong></div> 63 73 <div><span class="label">Task</span><strong id="current-task-text">None</strong></div> ··· 65 75 </section> 66 76 67 77 <main id="shell-main"> 68 - <div id="iframe-placeholder">Select an app and load it to begin calibration.</div> 78 + <div class="workspace-header"> 79 + <div> 80 + <h2>Preflight Workspace</h2> 81 + <p>The selected app loads in the background, then stays hidden until recording begins.</p> 82 + </div> 83 + </div> 84 + 85 + <div id="iframe-placeholder"> 86 + Select an app and load it to initialize calibration and prepare the session stage. 87 + </div> 69 88 </main> 70 89 </section> 71 90 ··· 75 94 <button id="session-debug-toggle-btn" class="session-debug-toggle hidden" type="button">Debug</button> 76 95 <aside id="session-debug-drawer" class="hidden"> 77 96 <div class="session-debug-header"> 78 - <strong>Debug</strong> 97 + <strong>Debug Controls</strong> 79 98 </div> 80 99 <div class="session-debug-actions"> 81 100 <button id="session-debug-skip-calibration" type="button">Skip Calibration</button> 82 101 <label class="debug-checkbox"> 83 102 <input id="session-debug-mouse-gaze-toggle" type="checkbox"> 84 - <span>Skip Eye Tracking</span> 103 + <span>Use mouse as gaze</span> 85 104 </label> 86 - <button id="session-debug-exit-test" type="button">Manually Exit Test</button> 105 + <button id="session-debug-exit-test" type="button">End Test</button> 87 106 </div> 88 107 </aside> 89 108 ··· 94 113 <span><strong id="calibration-clicks">0 / 3</strong> clicks</span> 95 114 <span id="calibration-quality">Pending</span> 96 115 <span id="calibration-feedback">Waiting for first point</span> 97 - <span id="calibration-instruction">Look at the target and click it three times.</span> 116 + <span id="calibration-instruction">Look at point 1 and click it 3 times.</span> 98 117 </div> 99 118 <div id="calibration-stage"> 100 119 <button id="calibration-target" type="button">1</button> 101 120 </div> 102 121 </section> 103 122 123 + <section id="start-curtain" class="hidden"></section> 124 + 104 125 <section id="start-overlay" class="hidden"> 105 126 <div class="start-card"> 106 - <p class="start-label">Calibration complete</p> 107 - <h2>Ready to start the task</h2> 127 + <h2>Task Ready</h2> 108 128 <p id="start-overlay-task">Task will appear here.</p> 109 - <button id="start-test-btn">Start Test</button> 129 + <p class="start-note">The app is loaded and hidden. It will appear when recording starts.</p> 130 + <button id="start-test-btn" type="button">Start Test</button> 110 131 </div> 111 132 </section> 112 133 113 134 <section id="calibration-failure-overlay" class="hidden"> 114 135 <div class="start-card failure-card"> 115 - <p class="start-label">Calibration result</p> 116 - <h2>Calibration didn't pass</h2> 136 + <h2>Calibration Retry Required</h2> 117 137 <p id="calibration-failure-summary">Average error summary will appear here.</p> 118 - <p>Try keeping your head still and looking directly at each point.</p> 138 + <p class="start-note">Keep your head still, look directly at each target, then retry.</p> 119 139 <button id="retry-calibration-btn" type="button">Retry Calibration</button> 120 140 </div> 121 141 </section> ··· 123 143 124 144 <section id="debrief-shell" class="hidden"> 125 145 <section id="debrief-screen"> 126 - <div class="overlay-card debrief-panel"> 127 - <div class="panel-header"> 128 - <h2>Test Debrief</h2> 146 + <header class="debrief-header"> 147 + <div> 148 + <h2>Session Debrief</h2> 129 149 <p id="screen-gallery-label">Heatmaps will appear here after a test completes.</p> 130 150 </div> 131 151 152 + <div class="theme-toggle" role="group" aria-label="Theme selection"> 153 + <button id="debrief-theme-dark-btn" class="theme-option" type="button">Dark</button> 154 + <button id="debrief-theme-light-btn" class="theme-option" type="button">Light</button> 155 + </div> 156 + </header> 157 + 158 + <section class="overlay-card debrief-panel"> 132 159 <div id="stats-container"> 133 - <ul> 134 - <li><span>Time</span><strong id="debrief-time">00:00:00</strong></li> 135 - <li><span>Clicks</span><strong id="debrief-clicks">0</strong></li> 136 - <li><span>Keys</span><strong id="debrief-keys">0</strong></li> 137 - <li><span>Mouse Distance</span><strong id="debrief-distance">0</strong></li> 138 - <li><span>Scroll Events</span><strong id="debrief-scrolls">0</strong></li> 139 - <li><span>Avg Velocity</span><strong id="debrief-velocity">0</strong></li> 140 - </ul> 141 - <ul> 142 - <li><span>Gaze Points</span><strong id="debrief-gaze-points">0</strong></li> 143 - <li><span>Fixations</span><strong id="debrief-fixations">0</strong></li> 144 - <li><span>Avg Fixation</span><strong id="debrief-fixation-duration">0</strong></li> 145 - </ul> 160 + <div class="stat-block"><span>Time</span><strong id="debrief-time">00:00:00</strong></div> 161 + <div class="stat-block"><span>Clicks</span><strong id="debrief-clicks">0</strong></div> 162 + <div class="stat-block"><span>Keys</span><strong id="debrief-keys">0</strong></div> 163 + <div class="stat-block"><span>Mouse Distance</span><strong id="debrief-distance">0</strong></div> 164 + <div class="stat-block"><span>Scroll Events</span><strong id="debrief-scrolls">0</strong></div> 165 + <div class="stat-block"><span>Avg Velocity</span><strong id="debrief-velocity">0</strong></div> 166 + <div class="stat-block"><span>Gaze Points</span><strong id="debrief-gaze-points">0</strong></div> 167 + <div class="stat-block"><span>Fixations</span><strong id="debrief-fixations">0</strong></div> 168 + <div class="stat-block"><span>Avg Fixation</span><strong id="debrief-fixation-duration">0</strong></div> 146 169 </div> 147 - </div> 170 + </section> 148 171 149 172 <div id="screen-gallery"></div> 150 173 </section>
+57 -7
js/main.js
··· 8 8 9 9 class UXETApp { 10 10 constructor() { 11 + this.themeStorageKey = 'uxet-theme'; 12 + this.theme = this.getStoredTheme(); 11 13 this.session = new Session(); 12 14 this.tracker = new Tracker(); 13 15 this.gazeTracker = new GazeTracker(); ··· 35 37 resetBtn: document.getElementById('reset-btn'), 36 38 exportBtn: document.getElementById('export-btn'), 37 39 startTestBtn: document.getElementById('start-test-btn'), 40 + startCurtain: document.getElementById('start-curtain'), 41 + themeDarkBtn: document.getElementById('theme-dark-btn'), 42 + themeLightBtn: document.getElementById('theme-light-btn'), 43 + debriefThemeDarkBtn: document.getElementById('debrief-theme-dark-btn'), 44 + debriefThemeLightBtn: document.getElementById('debrief-theme-light-btn'), 38 45 39 46 debugToggleBtn: document.getElementById('debug-toggle-btn'), 40 47 debugPanel: document.getElementById('debug-panel'), ··· 123 130 } 124 131 125 132 init() { 133 + this.applyTheme(this.theme); 126 134 this.bindEvents(); 127 135 this.bindCallbacks(); 128 136 this.syncDebugUi(); ··· 136 144 this.elements.exportBtn.addEventListener('click', () => this.exportData()); 137 145 this.elements.startTestBtn.addEventListener('click', () => this.beginTesting()); 138 146 this.elements.retryCalibrationBtn.addEventListener('click', () => this.retryCalibration()); 147 + this.elements.themeDarkBtn.addEventListener('click', () => this.setTheme('dark')); 148 + this.elements.themeLightBtn.addEventListener('click', () => this.setTheme('light')); 149 + this.elements.debriefThemeDarkBtn.addEventListener('click', () => this.setTheme('dark')); 150 + this.elements.debriefThemeLightBtn.addEventListener('click', () => this.setTheme('light')); 139 151 140 152 this.elements.debugToggleBtn.addEventListener('click', () => { 141 153 this.debugState.setupPanelOpen = !this.debugState.setupPanelOpen; ··· 281 293 const drawerVisible = sessionDrawerStates.has(this.session.state); 282 294 283 295 this.elements.debugPanel.classList.toggle('hidden', !this.debugState.setupPanelOpen); 296 + this.elements.debugToggleBtn.setAttribute('aria-expanded', String(this.debugState.setupPanelOpen)); 284 297 this.elements.sessionDebugToggleBtn.classList.toggle('hidden', !drawerVisible); 285 298 this.elements.sessionDebugDrawer.classList.toggle('hidden', !drawerVisible || !this.debugState.sessionDrawerOpen); 286 299 ··· 314 327 this.elements.startOverlayTask.textContent = this.selectedApp.task; 315 328 this.elements.iframePlaceholder.classList.add('hidden'); 316 329 this.elements.iframe.classList.remove('hidden'); 317 - this.elements.iframe.src = this.selectedApp.value; 330 + this.elements.iframe.src = this.withThemeQuery(this.selectedApp.value); 318 331 this.session.setState('loading_app'); 319 332 } 320 333 ··· 517 530 }); 518 531 } 519 532 533 + getStoredTheme() { 534 + const storedTheme = window.localStorage.getItem(this.themeStorageKey); 535 + return storedTheme === 'light' ? 'light' : 'dark'; 536 + } 537 + 538 + applyTheme(theme) { 539 + document.documentElement.dataset.theme = theme; 540 + this.theme = theme; 541 + const darkActive = theme === 'dark'; 542 + 543 + [ 544 + this.elements.themeDarkBtn, 545 + this.elements.debriefThemeDarkBtn 546 + ].forEach((button) => button.classList.toggle('active', darkActive)); 547 + [ 548 + this.elements.themeLightBtn, 549 + this.elements.debriefThemeLightBtn 550 + ].forEach((button) => button.classList.toggle('active', !darkActive)); 551 + } 552 + 553 + setTheme(theme) { 554 + if (!['dark', 'light'].includes(theme)) { 555 + return; 556 + } 557 + 558 + window.localStorage.setItem(this.themeStorageKey, theme); 559 + this.applyTheme(theme); 560 + } 561 + 562 + withThemeQuery(path) { 563 + const url = new URL(path, window.location.href); 564 + url.searchParams.set('theme', this.theme); 565 + return `${url.pathname}${url.search}${url.hash}`; 566 + } 567 + 520 568 async resetRuntimeOnly() { 521 569 this.winConditions.stop(); 522 570 this.tracker.detach(); ··· 528 576 this.elements.gallery.innerHTML = ''; 529 577 this.elements.galleryLabel.textContent = 'Heatmaps will appear here after a test completes.'; 530 578 this.elements.calibrationScreen.classList.add('hidden'); 579 + this.elements.startCurtain.classList.add('hidden'); 531 580 this.elements.startOverlay.classList.add('hidden'); 532 581 this.elements.calibrationFailureOverlay.classList.add('hidden'); 533 582 this.elements.debriefShell.classList.add('hidden'); ··· 595 644 this.elements.sessionStage.classList.toggle('hidden', !sessionActive); 596 645 this.elements.debriefShell.classList.toggle('hidden', state !== 'complete'); 597 646 this.elements.calibrationScreen.classList.toggle('hidden', state !== 'calibrating'); 647 + this.elements.startCurtain.classList.toggle('hidden', state !== 'ready_to_start'); 598 648 this.elements.startOverlay.classList.toggle('hidden', state !== 'ready_to_start'); 599 649 this.elements.calibrationFailureOverlay.classList.toggle('hidden', state !== 'calibration_failed'); 600 650 document.body.classList.toggle('session-active', sessionActive); ··· 605 655 this.elements.iframe.classList.add('hidden'); 606 656 break; 607 657 case 'loading_app': 608 - this.elements.sessionMessage.textContent = 'Loading app and attaching instrumentation bridge...'; 658 + this.elements.sessionMessage.textContent = 'Loading the app and attaching instrumentation.'; 609 659 this.elements.iframe.classList.remove('hidden'); 610 660 break; 611 661 case 'calibrating': 612 - this.elements.sessionMessage.textContent = 'Complete fullscreen calibration before recording starts.'; 662 + this.elements.sessionMessage.textContent = 'Complete fullscreen calibration before the task begins.'; 613 663 this.elements.iframe.classList.remove('hidden'); 614 664 this.calibration.updateUi(); 615 665 break; ··· 619 669 ? `Average error: ${averageError}px.` 620 670 : 'Calibration could not be scored accurately.'; 621 671 this.elements.calibrationFailureSummary.textContent = summary; 622 - this.elements.sessionMessage.textContent = 'Calibration failed. Review the result and retry explicitly.'; 672 + this.elements.sessionMessage.textContent = 'Calibration failed. Review the result and retry.'; 623 673 break; 624 674 } 625 675 case 'ready_to_start': 626 676 this.elements.sessionMessage.textContent = this.debugState.mouseGazeMode 627 - ? 'Eye tracking disabled in debug mode. Mouse pointer will be used as gaze.' 677 + ? 'Debug mode is armed. The app stays hidden until recording starts.' 628 678 : this.calibrationSkippedByDebug 629 - ? 'Calibration skipped in debug mode. Recording can start.' 630 - : 'Calibration passed. Start the fullscreen test when ready.'; 679 + ? 'Calibration was skipped in debug mode. The app is loaded and still hidden.' 680 + : 'Calibration passed. Start the task to reveal the app and begin recording.'; 631 681 this.elements.startOverlayTask.textContent = this.selectedApp?.task || 'Complete the assigned task.'; 632 682 break; 633 683 case 'recording':