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.

Initial commit

Chris Kalos b26d0e48

+3025
+871
index.css
··· 1 + /* UXET - UX Testing Framework Styles */ 2 + 3 + :root { 4 + /* Colors */ 5 + --bg-primary: #0a0a0f; 6 + --bg-secondary: #12121a; 7 + --bg-tertiary: #1a1a24; 8 + --bg-glass: rgba(26, 26, 36, 0.8); 9 + 10 + --text-primary: #f0f0f5; 11 + --text-secondary: #a0a0b0; 12 + --text-muted: #606070; 13 + 14 + --accent-primary: #6366f1; 15 + --accent-primary-hover: #818cf8; 16 + --accent-secondary: #22d3ee; 17 + --accent-success: #22c55e; 18 + --accent-danger: #ef4444; 19 + --accent-warning: #f59e0b; 20 + 21 + --border-color: rgba(255, 255, 255, 0.08); 22 + --border-glow: rgba(99, 102, 241, 0.4); 23 + 24 + /* Typography */ 25 + --font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; 26 + 27 + /* Spacing */ 28 + --spacing-xs: 4px; 29 + --spacing-sm: 8px; 30 + --spacing-md: 16px; 31 + --spacing-lg: 24px; 32 + --spacing-xl: 32px; 33 + 34 + /* Borders */ 35 + --radius-sm: 6px; 36 + --radius-md: 10px; 37 + --radius-lg: 16px; 38 + 39 + /* Transitions */ 40 + --transition-fast: 150ms ease; 41 + --transition-normal: 250ms ease; 42 + 43 + /* Shadows */ 44 + --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.3); 45 + --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4); 46 + --shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.5); 47 + --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.3); 48 + } 49 + 50 + /* Reset & Base */ 51 + *, 52 + *::before, 53 + *::after { 54 + box-sizing: border-box; 55 + margin: 0; 56 + padding: 0; 57 + } 58 + 59 + html, 60 + body { 61 + height: 100%; 62 + overflow: hidden; 63 + } 64 + 65 + body { 66 + font-family: var(--font-family); 67 + background: var(--bg-primary); 68 + color: var(--text-primary); 69 + line-height: 1.5; 70 + } 71 + 72 + /* Utility Classes */ 73 + .hidden { 74 + display: none !important; 75 + } 76 + 77 + /* App Container */ 78 + .app-container { 79 + display: flex; 80 + flex-direction: column; 81 + height: 100vh; 82 + background: 83 + radial-gradient(ellipse at top left, rgba(99, 102, 241, 0.1) 0%, transparent 50%), 84 + radial-gradient(ellipse at bottom right, rgba(34, 211, 238, 0.08) 0%, transparent 50%), 85 + var(--bg-primary); 86 + } 87 + 88 + /* Testing mode - hide header, full screen app */ 89 + .app-container.testing-mode .header { 90 + display: none; 91 + } 92 + 93 + .app-container.testing-mode .iframe-header { 94 + display: none; 95 + } 96 + 97 + .app-container.testing-mode .main-content { 98 + padding: 0; 99 + } 100 + 101 + .app-container.testing-mode .iframe-wrapper { 102 + border-radius: 0; 103 + border: none; 104 + } 105 + 106 + /* Header */ 107 + .header { 108 + display: flex; 109 + align-items: center; 110 + justify-content: space-between; 111 + padding: var(--spacing-md) var(--spacing-lg); 112 + background: var(--bg-glass); 113 + backdrop-filter: blur(12px); 114 + border-bottom: 1px solid var(--border-color); 115 + gap: var(--spacing-lg); 116 + transition: all var(--transition-normal); 117 + position: relative; 118 + z-index: 100; 119 + } 120 + 121 + .header-left { 122 + display: flex; 123 + align-items: center; 124 + gap: var(--spacing-md); 125 + } 126 + 127 + .logo { 128 + display: flex; 129 + align-items: center; 130 + gap: var(--spacing-sm); 131 + font-size: 1.5rem; 132 + font-weight: 700; 133 + background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); 134 + -webkit-background-clip: text; 135 + -webkit-text-fill-color: transparent; 136 + background-clip: text; 137 + } 138 + 139 + .logo-icon { 140 + font-size: 1.8rem; 141 + animation: pulse 2s ease-in-out infinite; 142 + } 143 + 144 + @keyframes pulse { 145 + 146 + 0%, 147 + 100% { 148 + opacity: 1; 149 + transform: scale(1); 150 + } 151 + 152 + 50% { 153 + opacity: 0.7; 154 + transform: scale(1.05); 155 + } 156 + } 157 + 158 + .tagline { 159 + color: var(--text-muted); 160 + font-size: 0.875rem; 161 + font-weight: 400; 162 + } 163 + 164 + .header-center { 165 + flex: 1; 166 + display: flex; 167 + justify-content: center; 168 + align-items: center; 169 + gap: var(--spacing-sm); 170 + } 171 + 172 + .header-right { 173 + display: flex; 174 + align-items: center; 175 + } 176 + 177 + /* Custom Dropdown */ 178 + .custom-dropdown { 179 + position: relative; 180 + z-index: 1000; 181 + } 182 + 183 + .dropdown-trigger { 184 + display: flex; 185 + align-items: center; 186 + gap: var(--spacing-sm); 187 + background: var(--bg-tertiary); 188 + border: 1px solid var(--border-color); 189 + border-radius: var(--radius-md); 190 + padding: var(--spacing-sm) var(--spacing-md); 191 + cursor: pointer; 192 + transition: all var(--transition-fast); 193 + min-width: 280px; 194 + } 195 + 196 + .dropdown-trigger:hover { 197 + border-color: var(--accent-primary); 198 + } 199 + 200 + .dropdown-label { 201 + color: var(--text-secondary); 202 + font-size: 0.875rem; 203 + font-weight: 500; 204 + } 205 + 206 + .dropdown-value { 207 + flex: 1; 208 + color: var(--text-primary); 209 + font-size: 0.875rem; 210 + text-align: left; 211 + } 212 + 213 + .dropdown-arrow { 214 + color: var(--text-muted); 215 + font-size: 0.65rem; 216 + transition: transform var(--transition-fast); 217 + } 218 + 219 + .custom-dropdown.open .dropdown-arrow { 220 + transform: rotate(180deg); 221 + } 222 + 223 + .dropdown-menu { 224 + position: absolute; 225 + top: calc(100% + 4px); 226 + left: 0; 227 + right: 0; 228 + background: var(--bg-secondary); 229 + border: 1px solid var(--border-color); 230 + border-radius: var(--radius-md); 231 + box-shadow: var(--shadow-lg); 232 + z-index: 1001; 233 + opacity: 0; 234 + visibility: hidden; 235 + transform: translateY(-8px); 236 + transition: all var(--transition-fast); 237 + } 238 + 239 + .custom-dropdown.open .dropdown-menu { 240 + opacity: 1; 241 + visibility: visible; 242 + transform: translateY(0); 243 + } 244 + 245 + .dropdown-item { 246 + display: flex; 247 + flex-direction: column; 248 + gap: 2px; 249 + padding: var(--spacing-md); 250 + cursor: pointer; 251 + border-bottom: 1px solid var(--border-color); 252 + transition: background var(--transition-fast); 253 + } 254 + 255 + .dropdown-item:last-child { 256 + border-bottom: none; 257 + } 258 + 259 + .dropdown-item:hover { 260 + background: var(--bg-tertiary); 261 + } 262 + 263 + .dropdown-item.selected { 264 + background: rgba(99, 102, 241, 0.15); 265 + } 266 + 267 + .item-name { 268 + font-weight: 500; 269 + color: var(--text-primary); 270 + } 271 + 272 + .item-task { 273 + font-size: 0.8rem; 274 + color: var(--text-muted); 275 + } 276 + 277 + /* Buttons */ 278 + .btn { 279 + display: inline-flex; 280 + align-items: center; 281 + gap: var(--spacing-xs); 282 + padding: var(--spacing-sm) var(--spacing-md); 283 + font-family: inherit; 284 + font-size: 0.875rem; 285 + font-weight: 500; 286 + border: none; 287 + border-radius: var(--radius-sm); 288 + cursor: pointer; 289 + transition: all var(--transition-fast); 290 + } 291 + 292 + .btn:disabled { 293 + opacity: 0.5; 294 + cursor: not-allowed; 295 + } 296 + 297 + .btn-icon { 298 + font-size: 0.75rem; 299 + } 300 + 301 + .btn-primary { 302 + background: linear-gradient(135deg, var(--accent-primary), #4f46e5); 303 + color: white; 304 + box-shadow: var(--shadow-sm), 0 0 12px rgba(99, 102, 241, 0.3); 305 + } 306 + 307 + .btn-primary:hover:not(:disabled) { 308 + background: linear-gradient(135deg, var(--accent-primary-hover), var(--accent-primary)); 309 + transform: translateY(-1px); 310 + box-shadow: var(--shadow-md), 0 0 20px rgba(99, 102, 241, 0.4); 311 + } 312 + 313 + .btn-secondary { 314 + background: var(--bg-tertiary); 315 + color: var(--text-primary); 316 + border: 1px solid var(--border-color); 317 + } 318 + 319 + .btn-secondary:hover:not(:disabled) { 320 + background: var(--bg-secondary); 321 + border-color: var(--text-muted); 322 + } 323 + 324 + .btn-accent { 325 + background: linear-gradient(135deg, var(--accent-secondary), #06b6d4); 326 + color: var(--bg-primary); 327 + font-weight: 600; 328 + } 329 + 330 + .btn-accent:hover:not(:disabled) { 331 + background: linear-gradient(135deg, #67e8f9, var(--accent-secondary)); 332 + transform: translateY(-1px); 333 + } 334 + 335 + .btn-large { 336 + padding: var(--spacing-md) var(--spacing-xl); 337 + font-size: 1.1rem; 338 + } 339 + 340 + /* Main Content */ 341 + .main-content { 342 + display: flex; 343 + flex: 1; 344 + overflow: hidden; 345 + gap: var(--spacing-lg); 346 + padding: var(--spacing-lg); 347 + transition: padding var(--transition-normal); 348 + } 349 + 350 + /* Iframe Wrapper */ 351 + .iframe-wrapper { 352 + flex: 1; 353 + display: flex; 354 + flex-direction: column; 355 + background: var(--bg-glass); 356 + backdrop-filter: blur(12px); 357 + border-radius: var(--radius-lg); 358 + border: 1px solid var(--border-color); 359 + overflow: hidden; 360 + box-shadow: var(--shadow-lg); 361 + transition: all var(--transition-normal); 362 + } 363 + 364 + .iframe-header { 365 + display: flex; 366 + justify-content: space-between; 367 + align-items: center; 368 + padding: var(--spacing-sm) var(--spacing-md); 369 + background: var(--bg-tertiary); 370 + border-bottom: 1px solid var(--border-color); 371 + } 372 + 373 + .iframe-status { 374 + color: var(--text-secondary); 375 + font-size: 0.8rem; 376 + } 377 + 378 + .session-timer { 379 + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; 380 + font-size: 0.875rem; 381 + color: var(--accent-secondary); 382 + font-weight: 500; 383 + } 384 + 385 + .iframe-container { 386 + flex: 1; 387 + position: relative; 388 + background: var(--bg-primary); 389 + } 390 + 391 + .iframe-placeholder { 392 + position: absolute; 393 + inset: 0; 394 + display: flex; 395 + align-items: center; 396 + justify-content: center; 397 + background: 398 + radial-gradient(circle at center, rgba(99, 102, 241, 0.05) 0%, transparent 70%), 399 + var(--bg-secondary); 400 + z-index: 1; 401 + } 402 + 403 + .placeholder-content { 404 + text-align: center; 405 + padding: var(--spacing-xl); 406 + } 407 + 408 + .placeholder-icon { 409 + font-size: 4rem; 410 + display: block; 411 + margin-bottom: var(--spacing-md); 412 + opacity: 0.6; 413 + } 414 + 415 + .placeholder-content h2 { 416 + color: var(--text-primary); 417 + font-size: 1.5rem; 418 + margin-bottom: var(--spacing-sm); 419 + } 420 + 421 + .placeholder-content p { 422 + color: var(--text-muted); 423 + font-size: 0.95rem; 424 + } 425 + 426 + #test-iframe { 427 + width: 100%; 428 + height: 100%; 429 + border: none; 430 + background: white; 431 + } 432 + 433 + /* Task Briefing Screen */ 434 + .task-briefing { 435 + position: absolute; 436 + inset: 0; 437 + display: flex; 438 + align-items: center; 439 + justify-content: center; 440 + background: 441 + radial-gradient(circle at center, rgba(99, 102, 241, 0.1) 0%, transparent 60%), 442 + var(--bg-secondary); 443 + z-index: 2; 444 + padding: var(--spacing-xl); 445 + } 446 + 447 + .briefing-card { 448 + background: var(--bg-glass); 449 + backdrop-filter: blur(20px); 450 + border-radius: var(--radius-lg); 451 + border: 1px solid var(--border-color); 452 + box-shadow: var(--shadow-lg), var(--shadow-glow); 453 + max-width: 500px; 454 + width: 100%; 455 + overflow: hidden; 456 + animation: slideUp 0.4s ease-out; 457 + } 458 + 459 + @keyframes slideUp { 460 + from { 461 + opacity: 0; 462 + transform: translateY(20px); 463 + } 464 + 465 + to { 466 + opacity: 1; 467 + transform: translateY(0); 468 + } 469 + } 470 + 471 + .briefing-header { 472 + background: linear-gradient(135deg, var(--accent-primary), #4f46e5); 473 + padding: var(--spacing-lg); 474 + text-align: center; 475 + } 476 + 477 + .briefing-icon { 478 + font-size: 2.5rem; 479 + display: block; 480 + margin-bottom: var(--spacing-sm); 481 + } 482 + 483 + .briefing-header h2 { 484 + color: white; 485 + font-size: 1.5rem; 486 + font-weight: 600; 487 + } 488 + 489 + .briefing-body { 490 + padding: var(--spacing-lg); 491 + } 492 + 493 + .task-description { 494 + font-size: 1.25rem; 495 + color: var(--text-primary); 496 + text-align: center; 497 + padding: var(--spacing-md); 498 + background: var(--bg-tertiary); 499 + border-radius: var(--radius-md); 500 + border-left: 4px solid var(--accent-primary); 501 + margin-bottom: var(--spacing-lg); 502 + font-weight: 500; 503 + } 504 + 505 + .briefing-info { 506 + display: flex; 507 + flex-direction: column; 508 + gap: var(--spacing-sm); 509 + } 510 + 511 + .info-item { 512 + display: flex; 513 + align-items: center; 514 + gap: var(--spacing-sm); 515 + color: var(--text-secondary); 516 + font-size: 0.875rem; 517 + } 518 + 519 + .info-icon { 520 + font-size: 1rem; 521 + opacity: 0.7; 522 + } 523 + 524 + .briefing-footer { 525 + padding: var(--spacing-lg); 526 + background: var(--bg-tertiary); 527 + text-align: center; 528 + border-top: 1px solid var(--border-color); 529 + } 530 + 531 + .ready-text { 532 + color: var(--text-muted); 533 + font-size: 0.875rem; 534 + margin-bottom: var(--spacing-md); 535 + } 536 + 537 + /* Debrief Screen */ 538 + .debrief-screen { 539 + position: absolute; 540 + inset: 0; 541 + display: flex; 542 + align-items: center; 543 + justify-content: center; 544 + background: rgba(10, 10, 15, 0.95); 545 + z-index: 10; 546 + padding: var(--spacing-xl); 547 + } 548 + 549 + .debrief-card { 550 + background: var(--bg-glass); 551 + backdrop-filter: blur(20px); 552 + border-radius: var(--radius-lg); 553 + border: 1px solid var(--border-color); 554 + box-shadow: var(--shadow-lg), 0 0 40px rgba(34, 197, 94, 0.2); 555 + max-width: 600px; 556 + width: 100%; 557 + overflow: hidden; 558 + animation: slideUp 0.4s ease-out; 559 + } 560 + 561 + .debrief-header { 562 + background: linear-gradient(135deg, var(--accent-success), #16a34a); 563 + padding: var(--spacing-lg); 564 + text-align: center; 565 + } 566 + 567 + .debrief-icon { 568 + font-size: 3rem; 569 + display: block; 570 + margin-bottom: var(--spacing-sm); 571 + } 572 + 573 + .debrief-header h2 { 574 + color: white; 575 + font-size: 1.75rem; 576 + font-weight: 700; 577 + margin-bottom: var(--spacing-xs); 578 + } 579 + 580 + .debrief-subtitle { 581 + color: rgba(255, 255, 255, 0.8); 582 + font-size: 1rem; 583 + } 584 + 585 + .debrief-body { 586 + padding: var(--spacing-lg); 587 + } 588 + 589 + .debrief-stat-group { 590 + margin-bottom: var(--spacing-lg); 591 + } 592 + 593 + .debrief-stat-group:last-child { 594 + margin-bottom: 0; 595 + } 596 + 597 + .debrief-stat-group h3 { 598 + font-size: 0.9rem; 599 + color: var(--text-secondary); 600 + margin-bottom: var(--spacing-md); 601 + padding-bottom: var(--spacing-sm); 602 + border-bottom: 1px solid var(--border-color); 603 + } 604 + 605 + .debrief-stats { 606 + display: grid; 607 + grid-template-columns: repeat(3, 1fr); 608 + gap: var(--spacing-md); 609 + } 610 + 611 + .debrief-stat { 612 + background: var(--bg-tertiary); 613 + padding: var(--spacing-md); 614 + border-radius: var(--radius-md); 615 + text-align: center; 616 + } 617 + 618 + .debrief-stat-value { 619 + display: block; 620 + font-size: 1.5rem; 621 + font-weight: 700; 622 + color: var(--accent-secondary); 623 + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; 624 + margin-bottom: var(--spacing-xs); 625 + } 626 + 627 + .debrief-stat-label { 628 + font-size: 0.75rem; 629 + color: var(--text-muted); 630 + text-transform: uppercase; 631 + letter-spacing: 0.5px; 632 + } 633 + 634 + .debrief-footer { 635 + padding: var(--spacing-lg); 636 + background: var(--bg-tertiary); 637 + display: flex; 638 + gap: var(--spacing-md); 639 + justify-content: center; 640 + border-top: 1px solid var(--border-color); 641 + } 642 + 643 + /* Sidebar */ 644 + .sidebar { 645 + width: 320px; 646 + display: flex; 647 + flex-direction: column; 648 + gap: var(--spacing-md); 649 + overflow-y: auto; 650 + transition: all var(--transition-normal); 651 + } 652 + 653 + .sidebar-section { 654 + background: var(--bg-glass); 655 + backdrop-filter: blur(12px); 656 + border-radius: var(--radius-md); 657 + border: 1px solid var(--border-color); 658 + padding: var(--spacing-md); 659 + transition: border-color var(--transition-normal); 660 + } 661 + 662 + .sidebar-section:hover { 663 + border-color: rgba(255, 255, 255, 0.12); 664 + } 665 + 666 + .section-title { 667 + display: flex; 668 + align-items: center; 669 + gap: var(--spacing-sm); 670 + font-size: 0.9rem; 671 + font-weight: 600; 672 + color: var(--text-primary); 673 + margin-bottom: var(--spacing-md); 674 + padding-bottom: var(--spacing-sm); 675 + border-bottom: 1px solid var(--border-color); 676 + } 677 + 678 + .section-icon { 679 + font-size: 1rem; 680 + } 681 + 682 + .stats-grid { 683 + display: grid; 684 + grid-template-columns: repeat(2, 1fr); 685 + gap: var(--spacing-sm); 686 + } 687 + 688 + .stat-item { 689 + background: var(--bg-tertiary); 690 + padding: var(--spacing-sm) var(--spacing-md); 691 + border-radius: var(--radius-sm); 692 + display: flex; 693 + flex-direction: column; 694 + gap: 2px; 695 + } 696 + 697 + .stat-item.full-width { 698 + grid-column: span 2; 699 + flex-direction: row; 700 + justify-content: space-between; 701 + align-items: center; 702 + } 703 + 704 + .stat-label { 705 + font-size: 0.7rem; 706 + color: var(--text-muted); 707 + text-transform: uppercase; 708 + letter-spacing: 0.5px; 709 + } 710 + 711 + .stat-value { 712 + font-size: 1rem; 713 + font-weight: 600; 714 + color: var(--accent-secondary); 715 + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; 716 + } 717 + 718 + .session-status { 719 + padding: 2px 8px; 720 + border-radius: var(--radius-sm); 721 + font-size: 0.8rem; 722 + } 723 + 724 + .session-status.idle { 725 + background: var(--bg-secondary); 726 + color: var(--text-muted); 727 + } 728 + 729 + .session-status.recording { 730 + background: rgba(239, 68, 68, 0.2); 731 + color: var(--accent-danger); 732 + animation: blink 1s ease-in-out infinite; 733 + } 734 + 735 + .session-status.completed { 736 + background: rgba(34, 197, 94, 0.2); 737 + color: var(--accent-success); 738 + } 739 + 740 + @keyframes blink { 741 + 742 + 0%, 743 + 100% { 744 + opacity: 1; 745 + } 746 + 747 + 50% { 748 + opacity: 0.6; 749 + } 750 + } 751 + 752 + /* Task Card */ 753 + .task-card { 754 + border-color: var(--accent-primary); 755 + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(99, 102, 241, 0.05)); 756 + } 757 + 758 + .current-task-text { 759 + color: var(--text-primary); 760 + font-size: 0.9rem; 761 + line-height: 1.5; 762 + } 763 + 764 + /* Event Log */ 765 + .event-log { 766 + max-height: 200px; 767 + overflow-y: auto; 768 + background: var(--bg-tertiary); 769 + border-radius: var(--radius-sm); 770 + padding: var(--spacing-sm); 771 + } 772 + 773 + .log-empty { 774 + color: var(--text-muted); 775 + font-size: 0.8rem; 776 + text-align: center; 777 + padding: var(--spacing-md); 778 + } 779 + 780 + .log-entry { 781 + display: flex; 782 + align-items: flex-start; 783 + gap: var(--spacing-sm); 784 + padding: var(--spacing-xs) 0; 785 + font-size: 0.75rem; 786 + border-bottom: 1px solid var(--border-color); 787 + } 788 + 789 + .log-entry:last-child { 790 + border-bottom: none; 791 + } 792 + 793 + .log-time { 794 + color: var(--text-muted); 795 + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; 796 + white-space: nowrap; 797 + } 798 + 799 + .log-type { 800 + padding: 1px 6px; 801 + border-radius: 3px; 802 + font-size: 0.65rem; 803 + font-weight: 600; 804 + text-transform: uppercase; 805 + } 806 + 807 + .log-type.mouse { 808 + background: rgba(99, 102, 241, 0.2); 809 + color: var(--accent-primary); 810 + } 811 + 812 + .log-type.key { 813 + background: rgba(34, 211, 238, 0.2); 814 + color: var(--accent-secondary); 815 + } 816 + 817 + .log-type.click { 818 + background: rgba(34, 197, 94, 0.2); 819 + color: var(--accent-success); 820 + } 821 + 822 + .log-type.system { 823 + background: rgba(245, 158, 11, 0.2); 824 + color: var(--accent-warning); 825 + } 826 + 827 + .log-message { 828 + color: var(--text-secondary); 829 + flex: 1; 830 + } 831 + 832 + /* Scrollbar */ 833 + ::-webkit-scrollbar { 834 + width: 6px; 835 + height: 6px; 836 + } 837 + 838 + ::-webkit-scrollbar-track { 839 + background: var(--bg-tertiary); 840 + border-radius: 3px; 841 + } 842 + 843 + ::-webkit-scrollbar-thumb { 844 + background: var(--text-muted); 845 + border-radius: 3px; 846 + } 847 + 848 + ::-webkit-scrollbar-thumb:hover { 849 + background: var(--text-secondary); 850 + } 851 + 852 + /* Responsive */ 853 + @media (max-width: 768px) { 854 + .header { 855 + flex-wrap: wrap; 856 + gap: var(--spacing-md); 857 + } 858 + 859 + .header-center { 860 + order: 3; 861 + flex-basis: 100%; 862 + } 863 + 864 + .dropdown-trigger { 865 + width: 100%; 866 + } 867 + 868 + .debrief-stats { 869 + grid-template-columns: 1fr; 870 + } 871 + }
+260
index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>UXET - UX Testing Framework</title> 8 + <link rel="stylesheet" href="index.css"> 9 + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 10 + </head> 11 + 12 + <body> 13 + <div class="app-container"> 14 + <!-- Header - Hidden during testing --> 15 + <header class="header" id="header"> 16 + <div class="header-left"> 17 + <h1 class="logo"> 18 + <span class="logo-icon">◎</span> 19 + UXET 20 + </h1> 21 + <span class="tagline">UX Testing Framework</span> 22 + </div> 23 + <div class="header-center"> 24 + <!-- Custom Dropdown --> 25 + <div class="custom-dropdown" id="app-dropdown"> 26 + <button class="dropdown-trigger" id="dropdown-trigger"> 27 + <span class="dropdown-label">Test App:</span> 28 + <span class="dropdown-value" id="dropdown-value">Select an app...</span> 29 + <span class="dropdown-arrow">▼</span> 30 + </button> 31 + <div class="dropdown-menu" id="dropdown-menu"> 32 + <div class="dropdown-item" data-value="testable-apps/shop-app/index.html" 33 + data-task="Find and purchase a blue t-shirt"> 34 + <span class="item-name">ShopEasy Store</span> 35 + <span class="item-task">Buy a t-shirt</span> 36 + </div> 37 + <div class="dropdown-item" data-value="testable-apps/example-app/index.html" 38 + data-task="Fill out the contact form with your details"> 39 + <span class="item-name">Example Form App</span> 40 + <span class="item-task">Complete form</span> 41 + </div> 42 + </div> 43 + </div> 44 + <button id="load-app-btn" class="btn btn-secondary">Load</button> 45 + </div> 46 + <div class="header-right"> 47 + <button id="reset-btn" class="btn btn-secondary"> 48 + <span class="btn-icon">↻</span> Reset 49 + </button> 50 + </div> 51 + </header> 52 + 53 + <!-- Main Content --> 54 + <main class="main-content" id="main-content"> 55 + <!-- Iframe Container --> 56 + <div class="iframe-wrapper" id="iframe-wrapper"> 57 + <div class="iframe-header" id="iframe-header"> 58 + <span class="iframe-status" id="iframe-status">No app loaded</span> 59 + <span class="session-timer" id="session-timer">00:00:00</span> 60 + </div> 61 + <div class="iframe-container" id="iframe-container"> 62 + <!-- Initial placeholder --> 63 + <div class="iframe-placeholder" id="iframe-placeholder"> 64 + <div class="placeholder-content"> 65 + <span class="placeholder-icon">🖥️</span> 66 + <h2>Select an App to Test</h2> 67 + <p>Choose an app from the dropdown above and click "Load" to begin</p> 68 + </div> 69 + </div> 70 + 71 + <!-- Task Briefing Screen --> 72 + <div class="task-briefing hidden" id="task-briefing"> 73 + <div class="briefing-card"> 74 + <div class="briefing-header"> 75 + <span class="briefing-icon">📋</span> 76 + <h2>Your Task</h2> 77 + </div> 78 + <div class="briefing-body"> 79 + <p class="task-description" id="task-description"></p> 80 + <div class="briefing-info"> 81 + <div class="info-item"> 82 + <span class="info-icon">⏱️</span> 83 + <span>Complete the task at your own pace</span> 84 + </div> 85 + <div class="info-item"> 86 + <span class="info-icon">✓</span> 87 + <span>The test will end automatically when done</span> 88 + </div> 89 + </div> 90 + </div> 91 + <div class="briefing-footer"> 92 + <p class="ready-text">When you're ready, click below to start</p> 93 + <button id="begin-test-btn" class="btn btn-primary btn-large"> 94 + <span class="btn-icon">▶</span> Begin Testing 95 + </button> 96 + </div> 97 + </div> 98 + </div> 99 + 100 + <!-- Debrief Screen --> 101 + <div class="debrief-screen hidden" id="debrief-screen"> 102 + <div class="debrief-card"> 103 + <div class="debrief-header"> 104 + <span class="debrief-icon">✅</span> 105 + <h2>Task Complete!</h2> 106 + <p class="debrief-subtitle">Here's how you did</p> 107 + </div> 108 + <div class="debrief-body"> 109 + <div class="debrief-stat-group"> 110 + <h3>📊 Session Summary</h3> 111 + <div class="debrief-stats"> 112 + <div class="debrief-stat"> 113 + <span class="debrief-stat-value" id="debrief-time">00:00</span> 114 + <span class="debrief-stat-label">Time to Complete</span> 115 + </div> 116 + <div class="debrief-stat"> 117 + <span class="debrief-stat-value" id="debrief-clicks">0</span> 118 + <span class="debrief-stat-label">Total Clicks</span> 119 + </div> 120 + <div class="debrief-stat"> 121 + <span class="debrief-stat-value" id="debrief-keys">0</span> 122 + <span class="debrief-stat-label">Keystrokes</span> 123 + </div> 124 + </div> 125 + </div> 126 + <div class="debrief-stat-group"> 127 + <h3>🖱️ Mouse Activity</h3> 128 + <div class="debrief-stats"> 129 + <div class="debrief-stat"> 130 + <span class="debrief-stat-value" id="debrief-distance">0</span> 131 + <span class="debrief-stat-label">Distance (px)</span> 132 + </div> 133 + <div class="debrief-stat"> 134 + <span class="debrief-stat-value" id="debrief-scrolls">0</span> 135 + <span class="debrief-stat-label">Scroll Events</span> 136 + </div> 137 + <div class="debrief-stat"> 138 + <span class="debrief-stat-value" id="debrief-velocity">0</span> 139 + <span class="debrief-stat-label">Avg Speed (px/s)</span> 140 + </div> 141 + </div> 142 + </div> 143 + </div> 144 + <div class="debrief-footer"> 145 + <button id="export-btn" class="btn btn-accent"> 146 + <span class="btn-icon">↓</span> Export Data 147 + </button> 148 + <button id="new-test-btn" class="btn btn-secondary"> 149 + Start New Test 150 + </button> 151 + </div> 152 + </div> 153 + </div> 154 + 155 + <iframe id="test-iframe" src="" title="Test Application"></iframe> 156 + </div> 157 + </div> 158 + 159 + <!-- Tracking Sidebar - Hidden during testing, shown in setup/debrief --> 160 + <aside class="sidebar hidden" id="sidebar"> 161 + <div class="sidebar-section task-card" id="task-card"> 162 + <h3 class="section-title"> 163 + <span class="section-icon">🎯</span> 164 + Current Task 165 + </h3> 166 + <p class="current-task-text" id="current-task-text">No task loaded</p> 167 + </div> 168 + 169 + <div class="sidebar-section"> 170 + <h3 class="section-title"> 171 + <span class="section-icon">🖱️</span> 172 + Mouse Tracking 173 + </h3> 174 + <div class="stats-grid"> 175 + <div class="stat-item"> 176 + <span class="stat-label">Position</span> 177 + <span class="stat-value" id="mouse-position">--, --</span> 178 + </div> 179 + <div class="stat-item"> 180 + <span class="stat-label">Movements</span> 181 + <span class="stat-value" id="mouse-movements">0</span> 182 + </div> 183 + <div class="stat-item"> 184 + <span class="stat-label">Clicks</span> 185 + <span class="stat-value" id="mouse-clicks">0</span> 186 + </div> 187 + <div class="stat-item"> 188 + <span class="stat-label">Distance (px)</span> 189 + <span class="stat-value" id="mouse-distance">0</span> 190 + </div> 191 + <div class="stat-item"> 192 + <span class="stat-label">Avg Velocity</span> 193 + <span class="stat-value" id="mouse-velocity">0 px/s</span> 194 + </div> 195 + <div class="stat-item"> 196 + <span class="stat-label">Scroll Events</span> 197 + <span class="stat-value" id="scroll-events">0</span> 198 + </div> 199 + </div> 200 + </div> 201 + 202 + <div class="sidebar-section"> 203 + <h3 class="section-title"> 204 + <span class="section-icon">⌨️</span> 205 + Keystroke Tracking 206 + </h3> 207 + <div class="stats-grid"> 208 + <div class="stat-item"> 209 + <span class="stat-label">Total Keys</span> 210 + <span class="stat-value" id="total-keys">0</span> 211 + </div> 212 + <div class="stat-item"> 213 + <span class="stat-label">Keys/Min</span> 214 + <span class="stat-value" id="keys-per-minute">0</span> 215 + </div> 216 + <div class="stat-item"> 217 + <span class="stat-label">Last Key</span> 218 + <span class="stat-value" id="last-key">--</span> 219 + </div> 220 + <div class="stat-item"> 221 + <span class="stat-label">Backspaces</span> 222 + <span class="stat-value" id="backspace-count">0</span> 223 + </div> 224 + </div> 225 + </div> 226 + 227 + <div class="sidebar-section"> 228 + <h3 class="section-title"> 229 + <span class="section-icon">📊</span> 230 + Session Info 231 + </h3> 232 + <div class="stats-grid"> 233 + <div class="stat-item full-width"> 234 + <span class="stat-label">Status</span> 235 + <span class="stat-value session-status" id="session-status">Idle</span> 236 + </div> 237 + <div class="stat-item full-width"> 238 + <span class="stat-label">Total Events</span> 239 + <span class="stat-value" id="total-events">0</span> 240 + </div> 241 + </div> 242 + </div> 243 + 244 + <div class="sidebar-section"> 245 + <h3 class="section-title"> 246 + <span class="section-icon">📝</span> 247 + Event Log 248 + </h3> 249 + <div class="event-log" id="event-log"> 250 + <div class="log-empty">No events recorded</div> 251 + </div> 252 + </div> 253 + </aside> 254 + </main> 255 + </div> 256 + 257 + <script type="module" src="js/main.js"></script> 258 + </body> 259 + 260 + </html>
+317
js/main.js
··· 1 + /** 2 + * UXET Main - Application entry point 3 + */ 4 + 5 + import { Tracker } from './tracker.js'; 6 + import { Session } from './session.js'; 7 + 8 + class UXETApp { 9 + constructor() { 10 + this.tracker = new Tracker(); 11 + this.session = new Session(); 12 + this.currentTask = ''; 13 + this.selectedApp = null; 14 + 15 + this.elements = { 16 + appContainer: document.querySelector('.app-container'), 17 + header: document.getElementById('header'), 18 + // Custom dropdown 19 + dropdown: document.getElementById('app-dropdown'), 20 + dropdownTrigger: document.getElementById('dropdown-trigger'), 21 + dropdownValue: document.getElementById('dropdown-value'), 22 + dropdownMenu: document.getElementById('dropdown-menu'), 23 + // Buttons 24 + loadAppBtn: document.getElementById('load-app-btn'), 25 + resetBtn: document.getElementById('reset-btn'), 26 + exportBtn: document.getElementById('export-btn'), 27 + newTestBtn: document.getElementById('new-test-btn'), 28 + beginTestBtn: document.getElementById('begin-test-btn'), 29 + // Iframe 30 + iframe: document.getElementById('test-iframe'), 31 + iframePlaceholder: document.getElementById('iframe-placeholder'), 32 + iframeHeader: document.getElementById('iframe-header'), 33 + iframeStatus: document.getElementById('iframe-status'), 34 + // Screens 35 + taskBriefing: document.getElementById('task-briefing'), 36 + debriefScreen: document.getElementById('debrief-screen'), 37 + taskDescription: document.getElementById('task-description'), 38 + // Timer 39 + sessionTimer: document.getElementById('session-timer'), 40 + // Sidebar (for internal tracking, hidden during test) 41 + sidebar: document.getElementById('sidebar'), 42 + currentTaskText: document.getElementById('current-task-text'), 43 + sessionStatus: document.getElementById('session-status'), 44 + // Debrief stats 45 + debriefTime: document.getElementById('debrief-time'), 46 + debriefClicks: document.getElementById('debrief-clicks'), 47 + debriefKeys: document.getElementById('debrief-keys'), 48 + debriefDistance: document.getElementById('debrief-distance'), 49 + debriefScrolls: document.getElementById('debrief-scrolls'), 50 + debriefVelocity: document.getElementById('debrief-velocity'), 51 + // Hidden stats (still tracked internally) 52 + mousePosition: document.getElementById('mouse-position'), 53 + mouseMovements: document.getElementById('mouse-movements'), 54 + mouseClicks: document.getElementById('mouse-clicks'), 55 + mouseDistance: document.getElementById('mouse-distance'), 56 + mouseVelocity: document.getElementById('mouse-velocity'), 57 + scrollEvents: document.getElementById('scroll-events'), 58 + totalKeys: document.getElementById('total-keys'), 59 + keysPerMinute: document.getElementById('keys-per-minute'), 60 + lastKey: document.getElementById('last-key'), 61 + backspaceCount: document.getElementById('backspace-count'), 62 + totalEvents: document.getElementById('total-events'), 63 + eventLog: document.getElementById('event-log') 64 + }; 65 + 66 + this.init(); 67 + } 68 + 69 + init() { 70 + this.bindEvents(); 71 + this.setupCallbacks(); 72 + this.setupCompletionAPI(); 73 + } 74 + 75 + bindEvents() { 76 + // Custom dropdown 77 + this.elements.dropdownTrigger.addEventListener('click', (e) => { 78 + e.stopPropagation(); 79 + this.toggleDropdown(); 80 + }); 81 + 82 + document.querySelectorAll('.dropdown-item').forEach(item => { 83 + item.addEventListener('click', () => this.selectApp(item)); 84 + }); 85 + 86 + // Close dropdown when clicking outside 87 + document.addEventListener('click', () => this.closeDropdown()); 88 + 89 + // Buttons 90 + this.elements.loadAppBtn.addEventListener('click', () => this.loadApp()); 91 + this.elements.beginTestBtn.addEventListener('click', () => this.beginTesting()); 92 + this.elements.resetBtn.addEventListener('click', () => this.resetSession()); 93 + this.elements.exportBtn.addEventListener('click', () => this.exportData()); 94 + this.elements.newTestBtn.addEventListener('click', () => this.resetSession()); 95 + 96 + this.elements.iframe.addEventListener('load', () => this.onIframeLoad()); 97 + } 98 + 99 + setupCallbacks() { 100 + this.tracker.onStatsUpdate = (stats) => this.updateStats(stats); 101 + this.tracker.onEvent = (event) => this.addEventToLog(event); 102 + 103 + this.session.onStatusChange = (status) => this.updateSessionStatus(status); 104 + this.session.onTimerUpdate = (time) => { 105 + this.elements.sessionTimer.textContent = time; 106 + }; 107 + } 108 + 109 + setupCompletionAPI() { 110 + // Listen for postMessage from iframe 111 + window.addEventListener('message', (e) => { 112 + if (e.data?.type === 'UXET_TASK_COMPLETE') { 113 + this.onTaskComplete(e.data.details || {}); 114 + } 115 + }); 116 + } 117 + 118 + // Custom dropdown methods 119 + toggleDropdown() { 120 + this.elements.dropdown.classList.toggle('open'); 121 + } 122 + 123 + closeDropdown() { 124 + this.elements.dropdown.classList.remove('open'); 125 + } 126 + 127 + selectApp(item) { 128 + // Update selection UI 129 + document.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected')); 130 + item.classList.add('selected'); 131 + 132 + // Store selection 133 + this.selectedApp = { 134 + value: item.dataset.value, 135 + task: item.dataset.task, 136 + name: item.querySelector('.item-name').textContent 137 + }; 138 + 139 + this.elements.dropdownValue.textContent = this.selectedApp.name; 140 + this.closeDropdown(); 141 + } 142 + 143 + loadApp() { 144 + if (!this.selectedApp) { 145 + alert('Please select an app to test'); 146 + return; 147 + } 148 + 149 + this.currentTask = this.selectedApp.task; 150 + 151 + // Update task displays 152 + this.elements.taskDescription.textContent = this.currentTask; 153 + this.elements.currentTaskText.textContent = this.currentTask; 154 + 155 + // Load the iframe but keep it hidden 156 + this.elements.iframe.src = this.selectedApp.value; 157 + this.elements.iframeStatus.textContent = 'Loading app...'; 158 + this.session.setAppName(this.selectedApp.value); 159 + this.session.setTask(this.currentTask); 160 + } 161 + 162 + onIframeLoad() { 163 + const src = this.elements.iframe.src; 164 + if (!src || src === 'about:blank') return; 165 + 166 + // Hide placeholder, show task briefing 167 + this.elements.iframePlaceholder.classList.add('hidden'); 168 + this.elements.taskBriefing.classList.remove('hidden'); 169 + this.elements.iframeStatus.textContent = 'Ready to test'; 170 + 171 + // Hide the iframe until testing begins 172 + this.elements.iframe.style.visibility = 'hidden'; 173 + } 174 + 175 + beginTesting() { 176 + // Hide task briefing, show iframe 177 + this.elements.taskBriefing.classList.add('hidden'); 178 + this.elements.iframe.style.visibility = 'visible'; 179 + 180 + // Enter testing mode - hide header and sidebar 181 + this.elements.appContainer.classList.add('testing-mode'); 182 + 183 + // Attach tracker and start session 184 + const attached = this.tracker.attachToIframe(this.elements.iframe); 185 + if (!attached) { 186 + console.warn('Could not attach tracker to iframe'); 187 + } 188 + 189 + this.session.start(); 190 + this.tracker.start(); 191 + } 192 + 193 + onTaskComplete(details) { 194 + if (this.session.status !== 'recording') return; 195 + 196 + // Stop tracking 197 + this.session.stop(); 198 + this.tracker.stop(); 199 + 200 + // Log completion 201 + this.tracker.logEvent('system', 'Task completed automatically'); 202 + 203 + // Exit testing mode 204 + this.elements.appContainer.classList.remove('testing-mode'); 205 + 206 + // Populate debrief stats 207 + const stats = this.tracker.getStats(); 208 + const timeFormatted = this.formatTime(this.session.elapsed); 209 + 210 + this.elements.debriefTime.textContent = timeFormatted; 211 + this.elements.debriefClicks.textContent = stats.mouse.clicks.toLocaleString(); 212 + this.elements.debriefKeys.textContent = stats.keyboard.totalKeys.toLocaleString(); 213 + this.elements.debriefDistance.textContent = Math.round(stats.mouse.distance).toLocaleString(); 214 + this.elements.debriefScrolls.textContent = stats.mouse.scrollEvents.toLocaleString(); 215 + this.elements.debriefVelocity.textContent = stats.mouse.avgVelocity.toLocaleString(); 216 + 217 + // Show debrief screen 218 + this.elements.debriefScreen.classList.remove('hidden'); 219 + } 220 + 221 + formatTime(ms) { 222 + const totalSeconds = Math.floor(ms / 1000); 223 + const minutes = Math.floor(totalSeconds / 60); 224 + const seconds = totalSeconds % 60; 225 + return `${minutes}:${seconds.toString().padStart(2, '0')}`; 226 + } 227 + 228 + resetSession() { 229 + this.session.reset(); 230 + this.tracker.reset(); 231 + this.clearEventLog(); 232 + 233 + // Reset UI state 234 + this.elements.appContainer.classList.remove('testing-mode'); 235 + this.elements.iframe.src = ''; 236 + this.elements.iframe.style.visibility = 'visible'; 237 + this.elements.iframePlaceholder.classList.remove('hidden'); 238 + this.elements.taskBriefing.classList.add('hidden'); 239 + this.elements.debriefScreen.classList.add('hidden'); 240 + this.elements.iframeStatus.textContent = 'No app loaded'; 241 + this.elements.currentTaskText.textContent = 'No task loaded'; 242 + this.elements.dropdownValue.textContent = 'Select an app...'; 243 + this.selectedApp = null; 244 + this.currentTask = ''; 245 + 246 + // Clear dropdown selection 247 + document.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected')); 248 + 249 + this.updateStats(this.tracker.createInitialStats()); 250 + } 251 + 252 + exportData() { 253 + const trackerData = this.tracker.exportData(); 254 + this.session.exportSession(trackerData); 255 + } 256 + 257 + updateSessionStatus(status) { 258 + const statusEl = this.elements.sessionStatus; 259 + statusEl.textContent = status.charAt(0).toUpperCase() + status.slice(1); 260 + statusEl.className = 'stat-value session-status ' + status; 261 + } 262 + 263 + updateStats(stats) { 264 + // Update internal stats (hidden from user during testing) 265 + this.elements.mousePosition.textContent = `${stats.mouse.x}, ${stats.mouse.y}`; 266 + this.elements.mouseMovements.textContent = stats.mouse.movements.toLocaleString(); 267 + this.elements.mouseClicks.textContent = stats.mouse.clicks.toLocaleString(); 268 + this.elements.mouseDistance.textContent = Math.round(stats.mouse.distance).toLocaleString(); 269 + this.elements.mouseVelocity.textContent = `${stats.mouse.avgVelocity} px/s`; 270 + this.elements.scrollEvents.textContent = stats.mouse.scrollEvents.toLocaleString(); 271 + 272 + this.elements.totalKeys.textContent = stats.keyboard.totalKeys.toLocaleString(); 273 + this.elements.keysPerMinute.textContent = stats.keyboard.keysPerMinute.toLocaleString(); 274 + this.elements.lastKey.textContent = stats.keyboard.lastKey || '--'; 275 + this.elements.backspaceCount.textContent = stats.keyboard.backspaces.toLocaleString(); 276 + 277 + this.elements.totalEvents.textContent = stats.totalEvents.toLocaleString(); 278 + } 279 + 280 + addEventToLog(event) { 281 + const log = this.elements.eventLog; 282 + 283 + const empty = log.querySelector('.log-empty'); 284 + if (empty) empty.remove(); 285 + 286 + const entry = document.createElement('div'); 287 + entry.className = 'log-entry'; 288 + 289 + const time = new Date(event.timestamp).toLocaleTimeString('en-US', { 290 + hour12: false, 291 + hour: '2-digit', 292 + minute: '2-digit', 293 + second: '2-digit' 294 + }); 295 + 296 + entry.innerHTML = ` 297 + <span class="log-time">${time}</span> 298 + <span class="log-type ${event.type}">${event.type}</span> 299 + <span class="log-message">${event.message}</span> 300 + `; 301 + 302 + log.insertBefore(entry, log.firstChild); 303 + 304 + while (log.children.length > 100) { 305 + log.removeChild(log.lastChild); 306 + } 307 + } 308 + 309 + clearEventLog() { 310 + this.elements.eventLog.innerHTML = '<div class="log-empty">No events recorded</div>'; 311 + } 312 + } 313 + 314 + // Initialize app when DOM is ready 315 + document.addEventListener('DOMContentLoaded', () => { 316 + window.uxetApp = new UXETApp(); 317 + });
+117
js/session.js
··· 1 + /** 2 + * UXET Session - Session management for testing sessions 3 + */ 4 + 5 + export class Session { 6 + constructor() { 7 + this.status = 'idle'; // idle, recording, stopped 8 + this.startTime = null; 9 + this.elapsed = 0; 10 + this.timerInterval = null; 11 + this.appName = ''; 12 + this.task = ''; 13 + this.onStatusChange = null; 14 + this.onTimerUpdate = null; 15 + } 16 + 17 + start() { 18 + if (this.status === 'recording') return; 19 + 20 + this.status = 'recording'; 21 + this.startTime = Date.now() - this.elapsed; 22 + 23 + this.timerInterval = setInterval(() => { 24 + this.elapsed = Date.now() - this.startTime; 25 + if (this.onTimerUpdate) { 26 + this.onTimerUpdate(this.formatTime(this.elapsed)); 27 + } 28 + }, 100); 29 + 30 + this.notifyStatusChange(); 31 + } 32 + 33 + stop() { 34 + if (this.status !== 'recording') return; 35 + 36 + this.status = 'stopped'; 37 + this.elapsed = Date.now() - this.startTime; 38 + 39 + if (this.timerInterval) { 40 + clearInterval(this.timerInterval); 41 + this.timerInterval = null; 42 + } 43 + 44 + this.notifyStatusChange(); 45 + } 46 + 47 + reset() { 48 + this.status = 'idle'; 49 + this.startTime = null; 50 + this.elapsed = 0; 51 + this.task = ''; 52 + 53 + if (this.timerInterval) { 54 + clearInterval(this.timerInterval); 55 + this.timerInterval = null; 56 + } 57 + 58 + if (this.onTimerUpdate) { 59 + this.onTimerUpdate('00:00:00'); 60 + } 61 + 62 + this.notifyStatusChange(); 63 + } 64 + 65 + setAppName(name) { 66 + this.appName = name; 67 + } 68 + 69 + setTask(task) { 70 + this.task = task; 71 + } 72 + 73 + formatTime(ms) { 74 + const totalSeconds = Math.floor(ms / 1000); 75 + const hours = Math.floor(totalSeconds / 3600); 76 + const minutes = Math.floor((totalSeconds % 3600) / 60); 77 + const seconds = totalSeconds % 60; 78 + 79 + return [hours, minutes, seconds] 80 + .map(n => n.toString().padStart(2, '0')) 81 + .join(':'); 82 + } 83 + 84 + notifyStatusChange() { 85 + if (this.onStatusChange) { 86 + this.onStatusChange(this.status); 87 + } 88 + } 89 + 90 + getMetadata() { 91 + return { 92 + appName: this.appName, 93 + task: this.task, 94 + status: this.status, 95 + startTime: this.startTime ? new Date(this.startTime).toISOString() : null, 96 + duration: this.elapsed 97 + }; 98 + } 99 + 100 + exportSession(trackerData) { 101 + const data = { 102 + session: this.getMetadata(), 103 + ...trackerData 104 + }; 105 + 106 + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); 107 + const url = URL.createObjectURL(blob); 108 + 109 + const a = document.createElement('a'); 110 + a.href = url; 111 + a.download = `uxet-session-${Date.now()}.json`; 112 + document.body.appendChild(a); 113 + a.click(); 114 + document.body.removeChild(a); 115 + URL.revokeObjectURL(url); 116 + } 117 + }
+248
js/tracker.js
··· 1 + /** 2 + * UXET Tracker - Core tracking engine for mouse and keyboard events 3 + */ 4 + 5 + export class Tracker { 6 + constructor() { 7 + this.isRecording = false; 8 + this.events = []; 9 + this.stats = this.createInitialStats(); 10 + this.iframeDoc = null; 11 + this.lastMousePos = null; 12 + this.lastMouseTime = null; 13 + this.velocities = []; 14 + this.listeners = new Map(); 15 + this.onStatsUpdate = null; 16 + this.onEvent = null; 17 + } 18 + 19 + createInitialStats() { 20 + return { 21 + mouse: { 22 + x: 0, 23 + y: 0, 24 + movements: 0, 25 + clicks: 0, 26 + distance: 0, 27 + avgVelocity: 0, 28 + scrollEvents: 0 29 + }, 30 + keyboard: { 31 + totalKeys: 0, 32 + keysPerMinute: 0, 33 + lastKey: '', 34 + backspaces: 0 35 + }, 36 + totalEvents: 0 37 + }; 38 + } 39 + 40 + reset() { 41 + this.events = []; 42 + this.stats = this.createInitialStats(); 43 + this.lastMousePos = null; 44 + this.lastMouseTime = null; 45 + this.velocities = []; 46 + this.notifyStatsUpdate(); 47 + } 48 + 49 + attachToIframe(iframe) { 50 + this.detach(); 51 + 52 + try { 53 + this.iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 54 + if (!this.iframeDoc) { 55 + console.warn('Cannot access iframe document - same-origin policy'); 56 + return false; 57 + } 58 + 59 + // Attach event listeners 60 + this.addListener(this.iframeDoc, 'mousemove', this.handleMouseMove.bind(this)); 61 + this.addListener(this.iframeDoc, 'click', this.handleClick.bind(this)); 62 + this.addListener(this.iframeDoc, 'scroll', this.handleScroll.bind(this), true); 63 + this.addListener(this.iframeDoc, 'keydown', this.handleKeyDown.bind(this)); 64 + this.addListener(this.iframeDoc, 'wheel', this.handleScroll.bind(this)); 65 + 66 + // Also track on scrollable elements within the iframe 67 + const scrollables = this.iframeDoc.querySelectorAll('*'); 68 + scrollables.forEach(el => { 69 + if (el.scrollHeight > el.clientHeight || el.scrollWidth > el.clientWidth) { 70 + this.addListener(el, 'scroll', this.handleScroll.bind(this)); 71 + } 72 + }); 73 + 74 + return true; 75 + } catch (e) { 76 + console.error('Error attaching to iframe:', e); 77 + return false; 78 + } 79 + } 80 + 81 + addListener(target, event, handler, options = false) { 82 + target.addEventListener(event, handler, options); 83 + const key = `${target.nodeName || 'doc'}-${event}`; 84 + if (!this.listeners.has(key)) { 85 + this.listeners.set(key, []); 86 + } 87 + this.listeners.get(key).push({ target, event, handler, options }); 88 + } 89 + 90 + detach() { 91 + this.listeners.forEach(entries => { 92 + entries.forEach(({ target, event, handler, options }) => { 93 + try { 94 + target.removeEventListener(event, handler, options); 95 + } catch (e) { 96 + // Target may no longer exist 97 + } 98 + }); 99 + }); 100 + this.listeners.clear(); 101 + this.iframeDoc = null; 102 + } 103 + 104 + start() { 105 + this.isRecording = true; 106 + this.startTime = Date.now(); 107 + } 108 + 109 + stop() { 110 + this.isRecording = false; 111 + } 112 + 113 + handleMouseMove(e) { 114 + if (!this.isRecording) return; 115 + 116 + const now = Date.now(); 117 + const x = e.clientX; 118 + const y = e.clientY; 119 + 120 + // Calculate distance and velocity 121 + if (this.lastMousePos) { 122 + const dx = x - this.lastMousePos.x; 123 + const dy = y - this.lastMousePos.y; 124 + const distance = Math.sqrt(dx * dx + dy * dy); 125 + this.stats.mouse.distance += distance; 126 + 127 + if (this.lastMouseTime) { 128 + const dt = (now - this.lastMouseTime) / 1000; // seconds 129 + if (dt > 0) { 130 + const velocity = distance / dt; 131 + this.velocities.push(velocity); 132 + // Keep last 50 velocities for average 133 + if (this.velocities.length > 50) { 134 + this.velocities.shift(); 135 + } 136 + this.stats.mouse.avgVelocity = Math.round( 137 + this.velocities.reduce((a, b) => a + b, 0) / this.velocities.length 138 + ); 139 + } 140 + } 141 + } 142 + 143 + this.lastMousePos = { x, y }; 144 + this.lastMouseTime = now; 145 + 146 + this.stats.mouse.x = x; 147 + this.stats.mouse.y = y; 148 + this.stats.mouse.movements++; 149 + this.stats.totalEvents++; 150 + 151 + // Throttle event logging for mouse moves (every 10th move) 152 + if (this.stats.mouse.movements % 10 === 0) { 153 + this.logEvent('mouse', `Move to (${x}, ${y})`); 154 + } 155 + 156 + this.notifyStatsUpdate(); 157 + } 158 + 159 + handleClick(e) { 160 + if (!this.isRecording) return; 161 + 162 + this.stats.mouse.clicks++; 163 + this.stats.totalEvents++; 164 + 165 + const target = e.target; 166 + const tagName = target.tagName.toLowerCase(); 167 + const id = target.id ? `#${target.id}` : ''; 168 + const className = target.className ? `.${target.className.split(' ')[0]}` : ''; 169 + 170 + this.logEvent('click', `Click on ${tagName}${id}${className} at (${e.clientX}, ${e.clientY})`); 171 + this.notifyStatsUpdate(); 172 + } 173 + 174 + handleScroll(e) { 175 + if (!this.isRecording) return; 176 + 177 + this.stats.mouse.scrollEvents++; 178 + this.stats.totalEvents++; 179 + 180 + this.logEvent('mouse', 'Scroll event'); 181 + this.notifyStatsUpdate(); 182 + } 183 + 184 + handleKeyDown(e) { 185 + if (!this.isRecording) return; 186 + 187 + this.stats.keyboard.totalKeys++; 188 + this.stats.totalEvents++; 189 + 190 + // Calculate keys per minute 191 + const elapsed = (Date.now() - this.startTime) / 60000; // minutes 192 + if (elapsed > 0) { 193 + this.stats.keyboard.keysPerMinute = Math.round(this.stats.keyboard.totalKeys / elapsed); 194 + } 195 + 196 + // Track backspaces 197 + if (e.key === 'Backspace') { 198 + this.stats.keyboard.backspaces++; 199 + } 200 + 201 + // Display key (handle special keys) 202 + let keyDisplay = e.key; 203 + if (e.key === ' ') keyDisplay = 'Space'; 204 + if (e.key.length > 1) keyDisplay = `[${e.key}]`; 205 + 206 + this.stats.keyboard.lastKey = keyDisplay; 207 + 208 + this.logEvent('key', `Key: ${keyDisplay}`); 209 + this.notifyStatsUpdate(); 210 + } 211 + 212 + logEvent(type, message) { 213 + const event = { 214 + timestamp: Date.now(), 215 + type, 216 + message, 217 + stats: { ...this.stats } 218 + }; 219 + this.events.push(event); 220 + 221 + if (this.onEvent) { 222 + this.onEvent(event); 223 + } 224 + } 225 + 226 + notifyStatsUpdate() { 227 + if (this.onStatsUpdate) { 228 + this.onStatsUpdate({ ...this.stats }); 229 + } 230 + } 231 + 232 + getEvents() { 233 + return [...this.events]; 234 + } 235 + 236 + getStats() { 237 + return { ...this.stats }; 238 + } 239 + 240 + exportData() { 241 + return { 242 + exportedAt: new Date().toISOString(), 243 + duration: this.startTime ? Date.now() - this.startTime : 0, 244 + stats: this.getStats(), 245 + events: this.getEvents() 246 + }; 247 + } 248 + }
+328
testable-apps/example-app/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>Example App - UXET Test</title> 8 + <style> 9 + * { 10 + box-sizing: border-box; 11 + margin: 0; 12 + padding: 0; 13 + } 14 + 15 + body { 16 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 17 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 18 + min-height: 100vh; 19 + padding: 40px 20px; 20 + color: #333; 21 + } 22 + 23 + .container { 24 + max-width: 600px; 25 + margin: 0 auto; 26 + } 27 + 28 + .card { 29 + background: white; 30 + border-radius: 16px; 31 + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); 32 + padding: 40px; 33 + margin-bottom: 24px; 34 + } 35 + 36 + h1 { 37 + font-size: 2rem; 38 + margin-bottom: 8px; 39 + color: #1a1a2e; 40 + } 41 + 42 + .subtitle { 43 + color: #666; 44 + margin-bottom: 32px; 45 + } 46 + 47 + .form-group { 48 + margin-bottom: 24px; 49 + } 50 + 51 + label { 52 + display: block; 53 + font-weight: 600; 54 + margin-bottom: 8px; 55 + color: #444; 56 + } 57 + 58 + input[type="text"], 59 + input[type="email"], 60 + textarea, 61 + select { 62 + width: 100%; 63 + padding: 12px 16px; 64 + border: 2px solid #e0e0e0; 65 + border-radius: 8px; 66 + font-size: 1rem; 67 + transition: border-color 0.2s, box-shadow 0.2s; 68 + } 69 + 70 + input:focus, 71 + textarea:focus, 72 + select:focus { 73 + outline: none; 74 + border-color: #667eea; 75 + box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1); 76 + } 77 + 78 + textarea { 79 + min-height: 120px; 80 + resize: vertical; 81 + } 82 + 83 + .button-group { 84 + display: flex; 85 + gap: 12px; 86 + flex-wrap: wrap; 87 + } 88 + 89 + button { 90 + padding: 14px 28px; 91 + border: none; 92 + border-radius: 8px; 93 + font-size: 1rem; 94 + font-weight: 600; 95 + cursor: pointer; 96 + transition: transform 0.2s, box-shadow 0.2s; 97 + } 98 + 99 + button:hover { 100 + transform: translateY(-2px); 101 + } 102 + 103 + button:active { 104 + transform: translateY(0); 105 + } 106 + 107 + .btn-primary { 108 + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 109 + color: white; 110 + box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); 111 + } 112 + 113 + .btn-primary:hover { 114 + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); 115 + } 116 + 117 + .btn-secondary { 118 + background: #f0f0f0; 119 + color: #333; 120 + } 121 + 122 + .btn-secondary:hover { 123 + background: #e0e0e0; 124 + } 125 + 126 + .checkbox-group { 127 + display: flex; 128 + flex-direction: column; 129 + gap: 12px; 130 + } 131 + 132 + .checkbox-item { 133 + display: flex; 134 + align-items: center; 135 + gap: 10px; 136 + cursor: pointer; 137 + } 138 + 139 + .checkbox-item input { 140 + width: 20px; 141 + height: 20px; 142 + cursor: pointer; 143 + } 144 + 145 + .interactive-section { 146 + background: #f8f9fa; 147 + border-radius: 12px; 148 + padding: 24px; 149 + margin-top: 24px; 150 + } 151 + 152 + .interactive-section h3 { 153 + margin-bottom: 16px; 154 + color: #444; 155 + } 156 + 157 + .counter { 158 + display: flex; 159 + align-items: center; 160 + gap: 16px; 161 + } 162 + 163 + .counter-value { 164 + font-size: 2rem; 165 + font-weight: 700; 166 + color: #667eea; 167 + min-width: 60px; 168 + text-align: center; 169 + } 170 + 171 + .counter button { 172 + width: 48px; 173 + height: 48px; 174 + border-radius: 50%; 175 + font-size: 1.5rem; 176 + padding: 0; 177 + display: flex; 178 + align-items: center; 179 + justify-content: center; 180 + } 181 + 182 + .scrollable-list { 183 + max-height: 200px; 184 + overflow-y: auto; 185 + border: 1px solid #e0e0e0; 186 + border-radius: 8px; 187 + margin-top: 16px; 188 + } 189 + 190 + .list-item { 191 + padding: 12px 16px; 192 + border-bottom: 1px solid #e0e0e0; 193 + cursor: pointer; 194 + transition: background 0.2s; 195 + } 196 + 197 + .list-item:last-child { 198 + border-bottom: none; 199 + } 200 + 201 + .list-item:hover { 202 + background: #f0f4ff; 203 + } 204 + 205 + .list-item.selected { 206 + background: #667eea; 207 + color: white; 208 + } 209 + </style> 210 + </head> 211 + 212 + <body> 213 + <div class="container"> 214 + <div class="card"> 215 + <h1>Example Test App</h1> 216 + <p class="subtitle">Interact with this form to test UXET tracking</p> 217 + 218 + <form id="test-form"> 219 + <div class="form-group"> 220 + <label for="name">Your Name</label> 221 + <input type="text" id="name" placeholder="Enter your name"> 222 + </div> 223 + 224 + <div class="form-group"> 225 + <label for="email">Email Address</label> 226 + <input type="email" id="email" placeholder="you@example.com"> 227 + </div> 228 + 229 + <div class="form-group"> 230 + <label for="category">Category</label> 231 + <select id="category"> 232 + <option value="">Select a category...</option> 233 + <option value="feedback">Feedback</option> 234 + <option value="bug">Bug Report</option> 235 + <option value="feature">Feature Request</option> 236 + <option value="other">Other</option> 237 + </select> 238 + </div> 239 + 240 + <div class="form-group"> 241 + <label for="message">Message</label> 242 + <textarea id="message" placeholder="Type your message here..."></textarea> 243 + </div> 244 + 245 + <div class="form-group"> 246 + <label>Preferences</label> 247 + <div class="checkbox-group"> 248 + <label class="checkbox-item"> 249 + <input type="checkbox" id="pref1"> 250 + <span>Send me email updates</span> 251 + </label> 252 + <label class="checkbox-item"> 253 + <input type="checkbox" id="pref2"> 254 + <span>Enable notifications</span> 255 + </label> 256 + <label class="checkbox-item"> 257 + <input type="checkbox" id="pref3"> 258 + <span>Remember my preferences</span> 259 + </label> 260 + </div> 261 + </div> 262 + 263 + <div class="button-group"> 264 + <button type="submit" class="btn-primary">Submit Form</button> 265 + <button type="reset" class="btn-secondary">Reset</button> 266 + </div> 267 + </form> 268 + 269 + <div class="interactive-section"> 270 + <h3>Interactive Counter</h3> 271 + <div class="counter"> 272 + <button class="btn-secondary" id="decrease">−</button> 273 + <span class="counter-value" id="counter-value">0</span> 274 + <button class="btn-primary" id="increase">+</button> 275 + </div> 276 + </div> 277 + 278 + <div class="interactive-section"> 279 + <h3>Scrollable List</h3> 280 + <div class="scrollable-list" id="scrollable-list"> 281 + <div class="list-item">Item 1 - Click to select</div> 282 + <div class="list-item">Item 2 - Click to select</div> 283 + <div class="list-item">Item 3 - Click to select</div> 284 + <div class="list-item">Item 4 - Click to select</div> 285 + <div class="list-item">Item 5 - Click to select</div> 286 + <div class="list-item">Item 6 - Click to select</div> 287 + <div class="list-item">Item 7 - Click to select</div> 288 + <div class="list-item">Item 8 - Click to select</div> 289 + <div class="list-item">Item 9 - Click to select</div> 290 + <div class="list-item">Item 10 - Click to select</div> 291 + </div> 292 + </div> 293 + </div> 294 + </div> 295 + 296 + <script> 297 + // Counter functionality 298 + let count = 0; 299 + const counterValue = document.getElementById('counter-value'); 300 + 301 + document.getElementById('increase').addEventListener('click', () => { 302 + count++; 303 + counterValue.textContent = count; 304 + }); 305 + 306 + document.getElementById('decrease').addEventListener('click', () => { 307 + count--; 308 + counterValue.textContent = count; 309 + }); 310 + 311 + // List selection 312 + const listItems = document.querySelectorAll('.list-item'); 313 + listItems.forEach(item => { 314 + item.addEventListener('click', () => { 315 + listItems.forEach(i => i.classList.remove('selected')); 316 + item.classList.add('selected'); 317 + }); 318 + }); 319 + 320 + // Form submission 321 + document.getElementById('test-form').addEventListener('submit', (e) => { 322 + e.preventDefault(); 323 + alert('Form submitted! (This is just a demo)'); 324 + }); 325 + </script> 326 + </body> 327 + 328 + </html>
+884
testable-apps/shop-app/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + 4 + <head> 5 + <meta charset="UTF-8"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 + <title>ShopEasy - Your Style, Delivered</title> 8 + <style> 9 + * { 10 + box-sizing: border-box; 11 + margin: 0; 12 + padding: 0; 13 + } 14 + 15 + body { 16 + font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif; 17 + background: #f5f5f7; 18 + color: #1d1d1f; 19 + line-height: 1.5; 20 + } 21 + 22 + /* Header */ 23 + .header { 24 + background: white; 25 + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08); 26 + position: sticky; 27 + top: 0; 28 + z-index: 100; 29 + } 30 + 31 + .header-inner { 32 + max-width: 1200px; 33 + margin: 0 auto; 34 + padding: 16px 24px; 35 + display: flex; 36 + align-items: center; 37 + justify-content: space-between; 38 + } 39 + 40 + .logo { 41 + font-size: 1.5rem; 42 + font-weight: 700; 43 + color: #6366f1; 44 + text-decoration: none; 45 + } 46 + 47 + .nav { 48 + display: flex; 49 + gap: 32px; 50 + } 51 + 52 + .nav a { 53 + color: #555; 54 + text-decoration: none; 55 + font-weight: 500; 56 + transition: color 0.2s; 57 + } 58 + 59 + .nav a:hover { 60 + color: #6366f1; 61 + } 62 + 63 + .nav a.active { 64 + color: #6366f1; 65 + } 66 + 67 + .header-actions { 68 + display: flex; 69 + align-items: center; 70 + gap: 16px; 71 + } 72 + 73 + .search-box { 74 + display: flex; 75 + align-items: center; 76 + background: #f0f0f0; 77 + border-radius: 8px; 78 + padding: 8px 12px; 79 + } 80 + 81 + .search-box input { 82 + border: none; 83 + background: transparent; 84 + outline: none; 85 + width: 200px; 86 + font-size: 0.9rem; 87 + } 88 + 89 + .cart-btn { 90 + position: relative; 91 + background: none; 92 + border: none; 93 + font-size: 1.5rem; 94 + cursor: pointer; 95 + padding: 8px; 96 + } 97 + 98 + .cart-count { 99 + position: absolute; 100 + top: 0; 101 + right: 0; 102 + background: #ef4444; 103 + color: white; 104 + font-size: 0.7rem; 105 + width: 18px; 106 + height: 18px; 107 + border-radius: 50%; 108 + display: flex; 109 + align-items: center; 110 + justify-content: center; 111 + font-weight: 600; 112 + } 113 + 114 + /* Hero */ 115 + .hero { 116 + background: linear-gradient(135deg, #6366f1, #8b5cf6); 117 + color: white; 118 + padding: 60px 24px; 119 + text-align: center; 120 + } 121 + 122 + .hero h1 { 123 + font-size: 2.5rem; 124 + margin-bottom: 12px; 125 + } 126 + 127 + .hero p { 128 + font-size: 1.1rem; 129 + opacity: 0.9; 130 + } 131 + 132 + /* Category Filters */ 133 + .filters { 134 + max-width: 1200px; 135 + margin: 32px auto 0; 136 + padding: 0 24px; 137 + } 138 + 139 + .filter-tabs { 140 + display: flex; 141 + gap: 12px; 142 + flex-wrap: wrap; 143 + } 144 + 145 + .filter-tab { 146 + padding: 10px 20px; 147 + background: white; 148 + border: 2px solid #e0e0e0; 149 + border-radius: 24px; 150 + font-size: 0.9rem; 151 + font-weight: 500; 152 + cursor: pointer; 153 + transition: all 0.2s; 154 + } 155 + 156 + .filter-tab:hover { 157 + border-color: #6366f1; 158 + color: #6366f1; 159 + } 160 + 161 + .filter-tab.active { 162 + background: #6366f1; 163 + border-color: #6366f1; 164 + color: white; 165 + } 166 + 167 + /* Product Grid */ 168 + .products { 169 + max-width: 1200px; 170 + margin: 32px auto; 171 + padding: 0 24px; 172 + } 173 + 174 + .products h2 { 175 + font-size: 1.5rem; 176 + margin-bottom: 24px; 177 + } 178 + 179 + .product-grid { 180 + display: grid; 181 + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); 182 + gap: 24px; 183 + } 184 + 185 + .product-card { 186 + background: white; 187 + border-radius: 16px; 188 + overflow: hidden; 189 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); 190 + transition: transform 0.2s, box-shadow 0.2s; 191 + cursor: pointer; 192 + } 193 + 194 + .product-card:hover { 195 + transform: translateY(-4px); 196 + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); 197 + } 198 + 199 + .product-image { 200 + height: 200px; 201 + display: flex; 202 + align-items: center; 203 + justify-content: center; 204 + font-size: 4rem; 205 + background: linear-gradient(135deg, #f0f0ff, #e8e8ff); 206 + } 207 + 208 + .product-info { 209 + padding: 16px; 210 + } 211 + 212 + .product-name { 213 + font-weight: 600; 214 + font-size: 1rem; 215 + margin-bottom: 4px; 216 + } 217 + 218 + .product-color { 219 + color: #888; 220 + font-size: 0.85rem; 221 + margin-bottom: 8px; 222 + } 223 + 224 + .product-price { 225 + font-size: 1.25rem; 226 + font-weight: 700; 227 + color: #6366f1; 228 + } 229 + 230 + .product-rating { 231 + display: flex; 232 + align-items: center; 233 + gap: 4px; 234 + margin-top: 8px; 235 + color: #fbbf24; 236 + font-size: 0.85rem; 237 + } 238 + 239 + /* Product Modal */ 240 + .modal-overlay { 241 + display: none; 242 + position: fixed; 243 + inset: 0; 244 + background: rgba(0, 0, 0, 0.5); 245 + z-index: 200; 246 + align-items: center; 247 + justify-content: center; 248 + padding: 24px; 249 + } 250 + 251 + .modal-overlay.active { 252 + display: flex; 253 + } 254 + 255 + .modal { 256 + background: white; 257 + border-radius: 20px; 258 + max-width: 600px; 259 + width: 100%; 260 + max-height: 90vh; 261 + overflow-y: auto; 262 + animation: modalSlide 0.3s ease; 263 + } 264 + 265 + @keyframes modalSlide { 266 + from { 267 + opacity: 0; 268 + transform: scale(0.95); 269 + } 270 + 271 + to { 272 + opacity: 1; 273 + transform: scale(1); 274 + } 275 + } 276 + 277 + .modal-header { 278 + display: flex; 279 + justify-content: space-between; 280 + align-items: center; 281 + padding: 20px 24px; 282 + border-bottom: 1px solid #eee; 283 + } 284 + 285 + .modal-close { 286 + background: none; 287 + border: none; 288 + font-size: 1.5rem; 289 + cursor: pointer; 290 + color: #888; 291 + } 292 + 293 + .modal-body { 294 + padding: 24px; 295 + } 296 + 297 + .modal-product-image { 298 + height: 250px; 299 + display: flex; 300 + align-items: center; 301 + justify-content: center; 302 + font-size: 6rem; 303 + background: linear-gradient(135deg, #f0f0ff, #e8e8ff); 304 + border-radius: 12px; 305 + margin-bottom: 24px; 306 + } 307 + 308 + .modal-product-info h2 { 309 + font-size: 1.5rem; 310 + margin-bottom: 8px; 311 + } 312 + 313 + .modal-product-price { 314 + font-size: 2rem; 315 + font-weight: 700; 316 + color: #6366f1; 317 + margin-bottom: 16px; 318 + } 319 + 320 + .size-selector { 321 + margin-bottom: 24px; 322 + } 323 + 324 + .size-selector label { 325 + display: block; 326 + font-weight: 600; 327 + margin-bottom: 8px; 328 + } 329 + 330 + .size-options { 331 + display: flex; 332 + gap: 8px; 333 + } 334 + 335 + .size-option { 336 + width: 44px; 337 + height: 44px; 338 + border: 2px solid #e0e0e0; 339 + border-radius: 8px; 340 + display: flex; 341 + align-items: center; 342 + justify-content: center; 343 + cursor: pointer; 344 + font-weight: 500; 345 + transition: all 0.2s; 346 + } 347 + 348 + .size-option:hover { 349 + border-color: #6366f1; 350 + } 351 + 352 + .size-option.selected { 353 + border-color: #6366f1; 354 + background: #6366f1; 355 + color: white; 356 + } 357 + 358 + .add-to-cart-btn { 359 + width: 100%; 360 + padding: 16px; 361 + background: linear-gradient(135deg, #6366f1, #8b5cf6); 362 + color: white; 363 + border: none; 364 + border-radius: 12px; 365 + font-size: 1.1rem; 366 + font-weight: 600; 367 + cursor: pointer; 368 + transition: transform 0.2s, box-shadow 0.2s; 369 + } 370 + 371 + .add-to-cart-btn:hover { 372 + transform: translateY(-2px); 373 + box-shadow: 0 8px 20px rgba(99, 102, 241, 0.4); 374 + } 375 + 376 + /* Cart Sidebar */ 377 + .cart-sidebar { 378 + position: fixed; 379 + top: 0; 380 + right: -400px; 381 + width: 400px; 382 + height: 100vh; 383 + background: white; 384 + box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15); 385 + z-index: 300; 386 + transition: right 0.3s ease; 387 + display: flex; 388 + flex-direction: column; 389 + } 390 + 391 + .cart-sidebar.open { 392 + right: 0; 393 + } 394 + 395 + .cart-header { 396 + padding: 20px 24px; 397 + border-bottom: 1px solid #eee; 398 + display: flex; 399 + justify-content: space-between; 400 + align-items: center; 401 + } 402 + 403 + .cart-header h2 { 404 + font-size: 1.25rem; 405 + } 406 + 407 + .cart-close { 408 + background: none; 409 + border: none; 410 + font-size: 1.5rem; 411 + cursor: pointer; 412 + color: #888; 413 + } 414 + 415 + .cart-items { 416 + flex: 1; 417 + overflow-y: auto; 418 + padding: 16px 24px; 419 + } 420 + 421 + .cart-empty { 422 + text-align: center; 423 + color: #888; 424 + padding: 40px; 425 + } 426 + 427 + .cart-item { 428 + display: flex; 429 + gap: 16px; 430 + padding: 16px 0; 431 + border-bottom: 1px solid #eee; 432 + } 433 + 434 + .cart-item-image { 435 + width: 80px; 436 + height: 80px; 437 + background: #f0f0ff; 438 + border-radius: 8px; 439 + display: flex; 440 + align-items: center; 441 + justify-content: center; 442 + font-size: 2rem; 443 + } 444 + 445 + .cart-item-info { 446 + flex: 1; 447 + } 448 + 449 + .cart-item-name { 450 + font-weight: 600; 451 + margin-bottom: 4px; 452 + } 453 + 454 + .cart-item-details { 455 + color: #888; 456 + font-size: 0.85rem; 457 + margin-bottom: 8px; 458 + } 459 + 460 + .cart-item-price { 461 + font-weight: 600; 462 + color: #6366f1; 463 + } 464 + 465 + .cart-item-remove { 466 + background: none; 467 + border: none; 468 + color: #ef4444; 469 + cursor: pointer; 470 + font-size: 0.85rem; 471 + } 472 + 473 + .cart-footer { 474 + padding: 20px 24px; 475 + border-top: 1px solid #eee; 476 + background: #fafafa; 477 + } 478 + 479 + .cart-total { 480 + display: flex; 481 + justify-content: space-between; 482 + font-size: 1.25rem; 483 + font-weight: 600; 484 + margin-bottom: 16px; 485 + } 486 + 487 + .checkout-btn { 488 + width: 100%; 489 + padding: 16px; 490 + background: linear-gradient(135deg, #22c55e, #16a34a); 491 + color: white; 492 + border: none; 493 + border-radius: 12px; 494 + font-size: 1.1rem; 495 + font-weight: 600; 496 + cursor: pointer; 497 + transition: transform 0.2s; 498 + } 499 + 500 + .checkout-btn:hover { 501 + transform: translateY(-2px); 502 + } 503 + 504 + .cart-overlay { 505 + display: none; 506 + position: fixed; 507 + inset: 0; 508 + background: rgba(0, 0, 0, 0.3); 509 + z-index: 250; 510 + } 511 + 512 + .cart-overlay.active { 513 + display: block; 514 + } 515 + 516 + /* Checkout Success */ 517 + .checkout-success { 518 + display: none; 519 + position: fixed; 520 + inset: 0; 521 + background: rgba(0, 0, 0, 0.5); 522 + z-index: 400; 523 + align-items: center; 524 + justify-content: center; 525 + } 526 + 527 + .checkout-success.active { 528 + display: flex; 529 + } 530 + 531 + .success-card { 532 + background: white; 533 + border-radius: 20px; 534 + padding: 48px; 535 + text-align: center; 536 + max-width: 400px; 537 + animation: modalSlide 0.3s ease; 538 + } 539 + 540 + .success-icon { 541 + font-size: 4rem; 542 + margin-bottom: 16px; 543 + } 544 + 545 + .success-card h2 { 546 + color: #22c55e; 547 + margin-bottom: 8px; 548 + } 549 + 550 + .success-card p { 551 + color: #666; 552 + margin-bottom: 24px; 553 + } 554 + 555 + .continue-btn { 556 + padding: 12px 32px; 557 + background: #6366f1; 558 + color: white; 559 + border: none; 560 + border-radius: 8px; 561 + font-size: 1rem; 562 + font-weight: 600; 563 + cursor: pointer; 564 + } 565 + </style> 566 + </head> 567 + 568 + <body> 569 + <!-- Header --> 570 + <header class="header"> 571 + <div class="header-inner"> 572 + <a href="#" class="logo">ShopEasy</a> 573 + <nav class="nav"> 574 + <a href="#" class="active">Shop</a> 575 + <a href="#">Men</a> 576 + <a href="#">Women</a> 577 + <a href="#">Sale</a> 578 + </nav> 579 + <div class="header-actions"> 580 + <div class="search-box"> 581 + <span>🔍</span> 582 + <input type="text" placeholder="Search products..."> 583 + </div> 584 + <button class="cart-btn" id="cart-btn"> 585 + 🛒 586 + <span class="cart-count" id="cart-count">0</span> 587 + </button> 588 + </div> 589 + </div> 590 + </header> 591 + 592 + <!-- Hero --> 593 + <section class="hero"> 594 + <h1>Summer Collection 2026</h1> 595 + <p>Discover the latest trends in fashion. Free shipping on orders over $50!</p> 596 + </section> 597 + 598 + <!-- Filters --> 599 + <div class="filters"> 600 + <div class="filter-tabs"> 601 + <button class="filter-tab active" data-filter="all">All</button> 602 + <button class="filter-tab" data-filter="t-shirts">T-Shirts</button> 603 + <button class="filter-tab" data-filter="jeans">Jeans</button> 604 + <button class="filter-tab" data-filter="dresses">Dresses</button> 605 + <button class="filter-tab" data-filter="jackets">Jackets</button> 606 + </div> 607 + </div> 608 + 609 + <!-- Products --> 610 + <section class="products"> 611 + <h2>Featured Products</h2> 612 + <div class="product-grid" id="product-grid"> 613 + <!-- Products will be inserted here --> 614 + </div> 615 + </section> 616 + 617 + <!-- Product Modal --> 618 + <div class="modal-overlay" id="product-modal"> 619 + <div class="modal"> 620 + <div class="modal-header"> 621 + <h3>Product Details</h3> 622 + <button class="modal-close" id="modal-close">×</button> 623 + </div> 624 + <div class="modal-body"> 625 + <div class="modal-product-image" id="modal-image">👕</div> 626 + <div class="modal-product-info"> 627 + <h2 id="modal-name">Classic Blue T-Shirt</h2> 628 + <p id="modal-color" style="color: #888; margin-bottom: 8px;">Blue</p> 629 + <div class="modal-product-price" id="modal-price">$29.99</div> 630 + 631 + <div class="size-selector"> 632 + <label>Select Size</label> 633 + <div class="size-options"> 634 + <div class="size-option" data-size="XS">XS</div> 635 + <div class="size-option" data-size="S">S</div> 636 + <div class="size-option selected" data-size="M">M</div> 637 + <div class="size-option" data-size="L">L</div> 638 + <div class="size-option" data-size="XL">XL</div> 639 + </div> 640 + </div> 641 + 642 + <button class="add-to-cart-btn" id="add-to-cart">Add to Cart</button> 643 + </div> 644 + </div> 645 + </div> 646 + </div> 647 + 648 + <!-- Cart Sidebar --> 649 + <div class="cart-overlay" id="cart-overlay"></div> 650 + <div class="cart-sidebar" id="cart-sidebar"> 651 + <div class="cart-header"> 652 + <h2>Your Cart</h2> 653 + <button class="cart-close" id="cart-close">×</button> 654 + </div> 655 + <div class="cart-items" id="cart-items"> 656 + <div class="cart-empty">Your cart is empty</div> 657 + </div> 658 + <div class="cart-footer"> 659 + <div class="cart-total"> 660 + <span>Total</span> 661 + <span id="cart-total">$0.00</span> 662 + </div> 663 + <button class="checkout-btn" id="checkout-btn">Proceed to Checkout</button> 664 + </div> 665 + </div> 666 + 667 + <!-- Checkout Success --> 668 + <div class="checkout-success" id="checkout-success"> 669 + <div class="success-card"> 670 + <div class="success-icon">✅</div> 671 + <h2>Purchase Complete!</h2> 672 + <p>Thank you for your order. Your items will be shipped soon.</p> 673 + <button class="continue-btn" id="continue-btn">Continue Shopping</button> 674 + </div> 675 + </div> 676 + 677 + <script> 678 + // Product data 679 + const products = [ 680 + { id: 1, name: 'Classic T-Shirt', color: 'Blue', price: 29.99, category: 't-shirts', emoji: '👕', rating: 4.5 }, 681 + { id: 2, name: 'Classic T-Shirt', color: 'Red', price: 29.99, category: 't-shirts', emoji: '👕', rating: 4.3 }, 682 + { id: 3, name: 'Classic T-Shirt', color: 'Black', price: 29.99, category: 't-shirts', emoji: '👕', rating: 4.8 }, 683 + { id: 4, name: 'Premium T-Shirt', color: 'White', price: 39.99, category: 't-shirts', emoji: '👕', rating: 4.6 }, 684 + { id: 5, name: 'Slim Fit Jeans', color: 'Dark Blue', price: 59.99, category: 'jeans', emoji: '👖', rating: 4.4 }, 685 + { id: 6, name: 'Relaxed Jeans', color: 'Light Blue', price: 54.99, category: 'jeans', emoji: '👖', rating: 4.2 }, 686 + { id: 7, name: 'Summer Dress', color: 'Floral', price: 49.99, category: 'dresses', emoji: '👗', rating: 4.7 }, 687 + { id: 8, name: 'Evening Dress', color: 'Black', price: 79.99, category: 'dresses', emoji: '👗', rating: 4.9 }, 688 + { id: 9, name: 'Denim Jacket', color: 'Blue', price: 89.99, category: 'jackets', emoji: '🧥', rating: 4.5 }, 689 + { id: 10, name: 'Leather Jacket', color: 'Brown', price: 149.99, category: 'jackets', emoji: '🧥', rating: 4.8 }, 690 + { id: 11, name: 'Graphic T-Shirt', color: 'Green', price: 34.99, category: 't-shirts', emoji: '👕', rating: 4.1 }, 691 + { id: 12, name: 'V-Neck T-Shirt', color: 'Navy', price: 32.99, category: 't-shirts', emoji: '👕', rating: 4.4 }, 692 + ]; 693 + 694 + let cart = []; 695 + let selectedProduct = null; 696 + let selectedSize = 'M'; 697 + let currentFilter = 'all'; 698 + 699 + // DOM Elements 700 + const productGrid = document.getElementById('product-grid'); 701 + const productModal = document.getElementById('product-modal'); 702 + const cartSidebar = document.getElementById('cart-sidebar'); 703 + const cartOverlay = document.getElementById('cart-overlay'); 704 + const cartItems = document.getElementById('cart-items'); 705 + const cartCount = document.getElementById('cart-count'); 706 + const cartTotal = document.getElementById('cart-total'); 707 + const checkoutSuccess = document.getElementById('checkout-success'); 708 + 709 + // Render products 710 + function renderProducts(filter = 'all') { 711 + const filtered = filter === 'all' 712 + ? products 713 + : products.filter(p => p.category === filter); 714 + 715 + productGrid.innerHTML = filtered.map(product => ` 716 + <div class="product-card" data-id="${product.id}"> 717 + <div class="product-image">${product.emoji}</div> 718 + <div class="product-info"> 719 + <div class="product-name">${product.name}</div> 720 + <div class="product-color">${product.color}</div> 721 + <div class="product-price">$${product.price.toFixed(2)}</div> 722 + <div class="product-rating"> 723 + ${'★'.repeat(Math.floor(product.rating))}${'☆'.repeat(5 - Math.floor(product.rating))} 724 + <span style="color: #888; margin-left: 4px;">(${product.rating})</span> 725 + </div> 726 + </div> 727 + </div> 728 + `).join(''); 729 + 730 + // Add click listeners 731 + document.querySelectorAll('.product-card').forEach(card => { 732 + card.addEventListener('click', () => openProductModal(parseInt(card.dataset.id))); 733 + }); 734 + } 735 + 736 + // Open product modal 737 + function openProductModal(productId) { 738 + selectedProduct = products.find(p => p.id === productId); 739 + if (!selectedProduct) return; 740 + 741 + document.getElementById('modal-image').textContent = selectedProduct.emoji; 742 + document.getElementById('modal-name').textContent = `${selectedProduct.name}`; 743 + document.getElementById('modal-color').textContent = selectedProduct.color; 744 + document.getElementById('modal-price').textContent = `$${selectedProduct.price.toFixed(2)}`; 745 + 746 + productModal.classList.add('active'); 747 + } 748 + 749 + // Close modal 750 + document.getElementById('modal-close').addEventListener('click', () => { 751 + productModal.classList.remove('active'); 752 + }); 753 + 754 + productModal.addEventListener('click', (e) => { 755 + if (e.target === productModal) { 756 + productModal.classList.remove('active'); 757 + } 758 + }); 759 + 760 + // Size selection 761 + document.querySelectorAll('.size-option').forEach(option => { 762 + option.addEventListener('click', () => { 763 + document.querySelectorAll('.size-option').forEach(o => o.classList.remove('selected')); 764 + option.classList.add('selected'); 765 + selectedSize = option.dataset.size; 766 + }); 767 + }); 768 + 769 + // Add to cart 770 + document.getElementById('add-to-cart').addEventListener('click', () => { 771 + if (!selectedProduct) return; 772 + 773 + const existingItem = cart.find(item => 774 + item.product.id === selectedProduct.id && item.size === selectedSize 775 + ); 776 + 777 + if (existingItem) { 778 + existingItem.quantity++; 779 + } else { 780 + cart.push({ 781 + product: selectedProduct, 782 + size: selectedSize, 783 + quantity: 1 784 + }); 785 + } 786 + 787 + updateCart(); 788 + productModal.classList.remove('active'); 789 + openCart(); 790 + }); 791 + 792 + // Update cart display 793 + function updateCart() { 794 + const totalItems = cart.reduce((sum, item) => sum + item.quantity, 0); 795 + cartCount.textContent = totalItems; 796 + 797 + if (cart.length === 0) { 798 + cartItems.innerHTML = '<div class="cart-empty">Your cart is empty</div>'; 799 + } else { 800 + cartItems.innerHTML = cart.map((item, index) => ` 801 + <div class="cart-item"> 802 + <div class="cart-item-image">${item.product.emoji}</div> 803 + <div class="cart-item-info"> 804 + <div class="cart-item-name">${item.product.name}</div> 805 + <div class="cart-item-details">${item.product.color} · Size ${item.size} · Qty ${item.quantity}</div> 806 + <div class="cart-item-price">$${(item.product.price * item.quantity).toFixed(2)}</div> 807 + </div> 808 + <button class="cart-item-remove" data-index="${index}">Remove</button> 809 + </div> 810 + `).join(''); 811 + 812 + // Add remove listeners 813 + document.querySelectorAll('.cart-item-remove').forEach(btn => { 814 + btn.addEventListener('click', () => { 815 + cart.splice(parseInt(btn.dataset.index), 1); 816 + updateCart(); 817 + }); 818 + }); 819 + } 820 + 821 + const total = cart.reduce((sum, item) => sum + item.product.price * item.quantity, 0); 822 + cartTotal.textContent = `$${total.toFixed(2)}`; 823 + } 824 + 825 + // Cart sidebar 826 + function openCart() { 827 + cartSidebar.classList.add('open'); 828 + cartOverlay.classList.add('active'); 829 + } 830 + 831 + function closeCart() { 832 + cartSidebar.classList.remove('open'); 833 + cartOverlay.classList.remove('active'); 834 + } 835 + 836 + document.getElementById('cart-btn').addEventListener('click', openCart); 837 + document.getElementById('cart-close').addEventListener('click', closeCart); 838 + cartOverlay.addEventListener('click', closeCart); 839 + 840 + // Filter tabs 841 + document.querySelectorAll('.filter-tab').forEach(tab => { 842 + tab.addEventListener('click', () => { 843 + document.querySelectorAll('.filter-tab').forEach(t => t.classList.remove('active')); 844 + tab.classList.add('active'); 845 + currentFilter = tab.dataset.filter; 846 + renderProducts(currentFilter); 847 + }); 848 + }); 849 + 850 + // Checkout 851 + document.getElementById('checkout-btn').addEventListener('click', () => { 852 + if (cart.length === 0) { 853 + alert('Your cart is empty!'); 854 + return; 855 + } 856 + closeCart(); 857 + checkoutSuccess.classList.add('active'); 858 + 859 + // Signal UXET that task is complete 860 + if (window.parent !== window) { 861 + window.parent.postMessage({ 862 + type: 'UXET_TASK_COMPLETE', 863 + details: { 864 + taskId: 'purchase', 865 + itemCount: cart.length, 866 + total: cart.reduce((sum, item) => sum + item.product.price * item.quantity, 0) 867 + } 868 + }, '*'); 869 + } 870 + 871 + cart = []; 872 + updateCart(); 873 + }); 874 + 875 + document.getElementById('continue-btn').addEventListener('click', () => { 876 + checkoutSuccess.classList.remove('active'); 877 + }); 878 + 879 + // Initialize 880 + renderProducts(); 881 + </script> 882 + </body> 883 + 884 + </html>