|
10 | 10 | <meta charset="UTF-8" /> |
11 | 11 | <title>ToolboxAid - Sample Launcher</title> |
12 | 12 | <link rel="stylesheet" href="../src/engine/ui/hubCommon.css" /> |
| 13 | + <style> |
| 14 | + .samples-filters { |
| 15 | + display: grid; |
| 16 | + gap: 0.75rem; |
| 17 | + margin: 1rem 0 1.5rem; |
| 18 | + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); |
| 19 | + align-items: end; |
| 20 | + } |
| 21 | + .samples-filters label { |
| 22 | + display: block; |
| 23 | + margin-bottom: 0.25rem; |
| 24 | + font-weight: 600; |
| 25 | + } |
| 26 | + .samples-filters select, |
| 27 | + .samples-filters input { |
| 28 | + width: 100%; |
| 29 | + padding: 0.45rem 0.55rem; |
| 30 | + border: 1px solid #9ca3af; |
| 31 | + border-radius: 6px; |
| 32 | + font: inherit; |
| 33 | + box-sizing: border-box; |
| 34 | + } |
| 35 | + .samples-filters .status { |
| 36 | + margin: 0; |
| 37 | + font-size: 0.95rem; |
| 38 | + align-self: center; |
| 39 | + } |
| 40 | + </style> |
13 | 41 | </head> |
14 | 42 | <body class="hub-page-samples"> |
15 | 43 | <div class="wrap"> |
|
22 | 50 | <h1>ToolboxAid - Sample Launcher</h1> |
23 | 51 | <p class="subtitle">Samples are regrouped into phase folders and renumbered as LLSS (Level + Sample).</p> |
24 | 52 | </section> |
| 53 | + <section class="samples-filters" aria-label="Sample filters"> |
| 54 | + <div> |
| 55 | + <label for="sample-phase-filter">Phase</label> |
| 56 | + <select id="sample-phase-filter"> |
| 57 | + <option value="">All phases</option> |
| 58 | + </select> |
| 59 | + </div> |
| 60 | + <div> |
| 61 | + <label for="sample-tag-filter">Tag</label> |
| 62 | + <select id="sample-tag-filter"> |
| 63 | + <option value="">All tags</option> |
| 64 | + <option value="__untagged">Untagged</option> |
| 65 | + </select> |
| 66 | + </div> |
| 67 | + <div> |
| 68 | + <label for="sample-search">Search</label> |
| 69 | + <input id="sample-search" type="search" placeholder="Sample ID, title, description, or tag" /> |
| 70 | + </div> |
| 71 | + <p class="status" id="samples-filter-status" role="status" aria-live="polite"></p> |
| 72 | + </section> |
25 | 73 | <!-- AUTO-GENERATED SAMPLE SECTIONS START --> |
26 | 74 | <section> |
27 | 75 | <h2>Phase 01 - Core Engine (0101-0124)</h2> |
@@ -326,6 +374,96 @@ <h2>Phase 16 - 3D Games (160x-160x)</h2> |
326 | 374 | </div> |
327 | 375 | </section> |
328 | 376 | </div> |
| 377 | + <script> |
| 378 | + (function () { |
| 379 | + const phaseFilter = document.getElementById('sample-phase-filter'); |
| 380 | + const tagFilter = document.getElementById('sample-tag-filter'); |
| 381 | + const searchInput = document.getElementById('sample-search'); |
| 382 | + const statusNode = document.getElementById('samples-filter-status'); |
| 383 | + const linkNodes = Array.from(document.querySelectorAll('section .grid a.live')); |
| 384 | + |
| 385 | + if (!phaseFilter || !tagFilter || !searchInput || !statusNode || linkNodes.length === 0) { |
| 386 | + return; |
| 387 | + } |
| 388 | + |
| 389 | + const linkModels = linkNodes.map((link) => { |
| 390 | + const href = String(link.getAttribute('href') || ''); |
| 391 | + const match = href.match(/^\.\/phase(\d{2})\/(\d{4})\/index\.html$/); |
| 392 | + const phase = match ? match[1] : ''; |
| 393 | + const sampleId = match ? match[2] : ''; |
| 394 | + const tags = String(link.getAttribute('data-tags') || '') |
| 395 | + .split(',') |
| 396 | + .map((tag) => tag.trim().toLowerCase()) |
| 397 | + .filter(Boolean); |
| 398 | + const titleText = String(link.textContent || '').trim().toLowerCase(); |
| 399 | + const descriptionText = String(link.getAttribute('title') || '').trim().toLowerCase(); |
| 400 | + const searchText = [sampleId, titleText, descriptionText, tags.join(' ')].join(' '); |
| 401 | + |
| 402 | + return { link, phase, tags, searchText }; |
| 403 | + }); |
| 404 | + |
| 405 | + const phaseValues = Array.from( |
| 406 | + new Set(linkModels.map((model) => model.phase).filter(Boolean)) |
| 407 | + ).sort(); |
| 408 | + for (const phase of phaseValues) { |
| 409 | + const option = document.createElement('option'); |
| 410 | + option.value = phase; |
| 411 | + option.textContent = 'Phase ' + phase; |
| 412 | + phaseFilter.appendChild(option); |
| 413 | + } |
| 414 | + |
| 415 | + const tagValues = Array.from( |
| 416 | + new Set(linkModels.flatMap((model) => model.tags).filter(Boolean)) |
| 417 | + ).sort(); |
| 418 | + for (const tag of tagValues) { |
| 419 | + const option = document.createElement('option'); |
| 420 | + option.value = tag; |
| 421 | + option.textContent = tag; |
| 422 | + tagFilter.appendChild(option); |
| 423 | + } |
| 424 | + |
| 425 | + const sections = Array.from(document.querySelectorAll('.wrap > section')).map((section) => ({ |
| 426 | + section, |
| 427 | + links: Array.from(section.querySelectorAll('.grid a.live')) |
| 428 | + })); |
| 429 | + |
| 430 | + function applyFilters() { |
| 431 | + const selectedPhase = String(phaseFilter.value || ''); |
| 432 | + const selectedTag = String(tagFilter.value || '').toLowerCase(); |
| 433 | + const query = String(searchInput.value || '').trim().toLowerCase(); |
| 434 | + let visibleCount = 0; |
| 435 | + |
| 436 | + for (const model of linkModels) { |
| 437 | + const phaseMatch = !selectedPhase || model.phase === selectedPhase; |
| 438 | + const tagMatch = |
| 439 | + !selectedTag || |
| 440 | + (selectedTag === '__untagged' ? model.tags.length === 0 : model.tags.includes(selectedTag)); |
| 441 | + const searchMatch = !query || model.searchText.includes(query); |
| 442 | + const isVisible = phaseMatch && tagMatch && searchMatch; |
| 443 | + |
| 444 | + model.link.style.display = isVisible ? '' : 'none'; |
| 445 | + if (isVisible) { |
| 446 | + visibleCount += 1; |
| 447 | + } |
| 448 | + } |
| 449 | + |
| 450 | + for (const sectionModel of sections) { |
| 451 | + if (sectionModel.links.length === 0) { |
| 452 | + continue; |
| 453 | + } |
| 454 | + const hasVisibleLink = sectionModel.links.some((link) => link.style.display !== 'none'); |
| 455 | + sectionModel.section.style.display = hasVisibleLink ? '' : 'none'; |
| 456 | + } |
| 457 | + |
| 458 | + statusNode.textContent = String(visibleCount) + ' of ' + String(linkModels.length) + ' samples shown'; |
| 459 | + } |
| 460 | + |
| 461 | + phaseFilter.addEventListener('change', applyFilters); |
| 462 | + tagFilter.addEventListener('change', applyFilters); |
| 463 | + searchInput.addEventListener('input', applyFilters); |
| 464 | + applyFilters(); |
| 465 | + })(); |
| 466 | + </script> |
329 | 467 | </body> |
330 | 468 | </html> |
331 | 469 |
|
0 commit comments