Skip to content

Commit 43d6b0a

Browse files
author
DavidQ
committed
BUILD_PR: add filtering and search to samples index
1 parent 18ead5e commit 43d6b0a

6 files changed

Lines changed: 167 additions & 76 deletions

File tree

docs/dev/CODEX_COMMANDS.md

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,23 @@ MODEL: GPT-5.4-codex
22
REASONING: high
33

44
COMMAND:
5-
Create PLAN_PR_SAMPLES_FILTER_AND_SEARCH as docs-only planning.
5+
Execute BUILD_PR_SAMPLES_FILTER_AND_SEARCH
66

7-
OBJECTIVE:
8-
Define a narrow, testable filter-and-search layer for the samples index using canonical sample paths and metadata-driven readable content.
7+
ENVIRONMENT:
8+
- Windows
9+
- Use Node.js or vanilla JS only
10+
- No npm install
11+
- No node_modules
912

10-
CONSTRAINTS:
11-
- docs only
12-
- no implementation code changes
13-
- no gameplay scope
14-
- no engine-core scope
15-
- no path normalization changes
16-
- no start_of_day directory changes
13+
RULES:
14+
- Do not change canonical paths
15+
- Do not modify gameplay
16+
- Keep changes minimal
1717

18-
PLANNING REQUIREMENTS:
19-
1. Define minimal UX for:
20-
- phase filtering
21-
- tag filtering
22-
- search
23-
2. Define combined state behavior
24-
3. Define source-of-truth boundaries among:
25-
- canonical folder structure
26-
- metadata
27-
- index UI behavior
28-
4. Define validation and fail-fast expectations
29-
5. Keep the future BUILD testable and narrowly scoped
18+
VALIDATION:
19+
- filters work
20+
- search works
21+
- 1316–1318 load correctly
3022

31-
OUTPUT FILES:
32-
- docs/pr/PLAN_PR_SAMPLES_FILTER_AND_SEARCH.md
33-
- docs/dev/codex_commands.md
34-
- docs/dev/commit_comment.txt
35-
- docs/dev/reports/change_summary.txt
36-
- docs/dev/reports/validation_checklist.txt
37-
- docs/dev/reports/file_tree_delta.txt
38-
39-
ZIP OUTPUT REQUIREMENT:
40-
- MUST produce ZIP:
41-
<project folder>/tmp/PLAN_PR_SAMPLES_FILTER_AND_SEARCH.zip
42-
- Task is not complete until the ZIP exists at that exact path
23+
ZIP:
24+
<project folder>/tmp/BUILD_PR_SAMPLES_FILTER_AND_SEARCH.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1 @@
1-
PLAN_PR_SAMPLES_FILTER_AND_SEARCH
2-
3-
- Define docs-only plan for samples index phase/tag/search UX
4-
- Define combined-state filtering behavior and deterministic ordering
5-
- Define source-of-truth boundaries (canonical paths, metadata, UI)
6-
- Define validation + fail-fast expectations for future BUILD
7-
- Keep future BUILD narrowly scoped and testable
1+
BUILD_PR: add filtering and search to samples index
Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1 @@
1-
TYPE: PLANNING ONLY
2-
3-
Created planning artifacts for samples index filter-and-search.
4-
5-
Defined:
6-
- minimal UX contract for phase filtering, tag filtering, and search
7-
- combined-state behavior and deterministic result ordering
8-
- source-of-truth boundaries across canonical folders, metadata, and UI behavior
9-
- fail-fast expectations for duplicates, mismatches, and missing required fields
10-
- narrow, testable future BUILD scope
11-
12-
No implementation/runtime/gameplay/engine-core changes.
1+
Add phase filter, tag filter, and search to samples index
Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
1-
PLAN_PR_SAMPLES_FILTER_AND_SEARCH DELTA
1+
MODIFIED:
2+
- samples/index.html
23

3-
Modified:
4-
- docs/pr/PLAN_PR_SAMPLES_FILTER_AND_SEARCH.md
5-
- docs/dev/codex_commands.md
6-
- docs/dev/commit_comment.txt
7-
- docs/dev/reports/change_summary.txt
8-
- docs/dev/reports/validation_checklist.txt
9-
- docs/dev/reports/file_tree_delta.txt
10-
11-
Scope:
12-
- docs-only planning
13-
- no implementation/runtime code changes
4+
ADDED:
5+
- minimal filter/search JS
Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
1-
[x] Docs-only scope preserved
2-
[x] No implementation code changes
3-
[x] No gameplay scope introduced
4-
[x] No engine-core scope introduced
5-
[x] No path normalization scope introduced
6-
[x] No start_of_day directory changes
7-
[x] Minimal UX contract defined (phase, tag, search)
8-
[x] Combined-state behavior defined
9-
[x] Source-of-truth boundaries defined
10-
[x] Validation + fail-fast expectations documented
11-
[x] Future BUILD scope kept narrow and testable
1+
[ ] phase filter works
2+
[ ] tag filter works
3+
[ ] search works
4+
[ ] combined filtering works
5+
[ ] links resolve (1316–1318)
6+
[ ] no console errors
7+
[ ] zip exists

samples/index.html

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,34 @@
1010
<meta charset="UTF-8" />
1111
<title>ToolboxAid - Sample Launcher</title>
1212
<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>
1341
</head>
1442
<body class="hub-page-samples">
1543
<div class="wrap">
@@ -22,6 +50,26 @@
2250
<h1>ToolboxAid - Sample Launcher</h1>
2351
<p class="subtitle">Samples are regrouped into phase folders and renumbered as LLSS (Level + Sample).</p>
2452
</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>
2573
<!-- AUTO-GENERATED SAMPLE SECTIONS START -->
2674
<section>
2775
<h2>Phase 01 - Core Engine (0101-0124)</h2>
@@ -326,6 +374,96 @@ <h2>Phase 16 - 3D Games (160x-160x)</h2>
326374
</div>
327375
</section>
328376
</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>
329467
</body>
330468
</html>
331469

0 commit comments

Comments
 (0)