Skip to content

Commit 9c5f542

Browse files
author
DavidQ
committed
Normalize samples metadata and tighten samples index filter/tile UX.
Convert tags into real descriptive tags, surface class values clearly, alphabetize class filter options, align search with Phase/Class/Tag, and move the pin affordance to the preview top-right with a green pinned state. PR: BUILD_PR_SAMPLES_METADATA_TAG_NORMALIZATION_AND_FILTER_BAR_UX
1 parent 8bbb617 commit 9c5f542

8 files changed

Lines changed: 3988 additions & 1649 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
Samples metadata + filter/tile UX strategy
2+
3+
Goals:
4+
- convert tags into real metadata
5+
- keep class as a broad category
6+
- surface class values clearly
7+
- sort class filter values alphabetically
8+
- keep search on the same row as Phase/Class/Tag
9+
- move pin affordance to preview top-right if cleanly supported
10+
- use green for pinned state
11+
12+
Preserve:
13+
- current accepted page shell
14+
- preview image launch behavior
15+
- hover zoom behavior
16+
- shared header/body consistency

docs/operations/dev/codex_commands.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,28 @@ MODEL: GPT-5.4-codex
22
REASONING: high
33

44
COMMAND:
5-
Create BUILD_PR_STYLE_SAMPLES_INDEX_UI_BEHAVIOR_RESTORE
5+
Create BUILD_PR_SAMPLES_METADATA_TAG_NORMALIZATION_AND_FILTER_BAR_UX
66

77
Rules:
88
- output ONLY the final zip to:
9-
<project folder>/tmp/BUILD_PR_STYLE_SAMPLES_INDEX_UI_BEHAVIOR_RESTORE.zip
9+
<project folder>/tmp/BUILD_PR_SAMPLES_METADATA_TAG_NORMALIZATION_AND_FILTER_BAR_UX.zip
1010
- do NOT create staging folders in <project folder>/tmp
1111
- do NOT modify roadmap in the PR bundle
1212
- Codex updates roadmap during execution only if execution-backed status changes are earned
1313
- no embedded <style> blocks
1414
- no inline style=""
1515
- no JS-generated styling
16-
- keep scope limited to /samples/index.html and directly related sample-index UI/data/rendering dependencies
17-
- preserve the current accepted page shell
16+
- keep scope limited to /samples/index.html and directly related sample metadata/rendering/filter dependencies
17+
- preserve the current accepted page shell and shared header/body consistency
1818

1919
Required work:
20-
1. Restore all intended sample filter dropdowns on /samples/index.html, including phase, class, and the remaining third filter.
21-
2. Restore functioning filter behavior.
22-
3. Restore the pin affordance as a pin, not a button.
23-
4. Restore the pinned list/section and its behavior.
24-
5. Restore preview image rendering from the correct sample metadata.
25-
6. Make the preview image the launch <a> target.
26-
7. Restore hover zoom behavior on the preview image.
27-
8. Keep the page shell and shared header/body consistency intact.
28-
9. Keep the change narrow, testable, and behavior-restoring rather than redesigning.
20+
1. Normalize sample metadata so tags are real descriptive tags instead of duplicates of class values.
21+
2. Keep/assign a clear class value for each sample.
22+
3. Include/display the class list for each sample.
23+
4. Ensure class filter values are sorted alphabetically.
24+
5. Put search on the same row as Phase, Class, and Tag.
25+
6. Move the pin affordance to the top-right over the preview image if the current tile structure allows it cleanly.
26+
7. Make the pinned visual state green instead of red.
27+
8. Preserve preview image launch behavior and hover zoom.
28+
9. Keep filtering and pinning behavior working.
29+
10. Keep the change narrow, testable, and behavior-focused rather than redesigning the page.
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
Restore the remaining samples index UI capabilities without redesigning the page shell.
1+
Normalize samples metadata and tighten samples index filter/tile UX.
22

3-
Bring back all filter dropdowns, pinned list behavior, pin affordance, preview images, image-launch links, and hover zoom on /samples/index.html.
3+
Convert tags into real descriptive tags, surface class values clearly, alphabetize class filter options, align search with Phase/Class/Tag, and move the pin affordance to the preview top-right with a green pinned state.
44

5-
PR: BUILD_PR_STYLE_SAMPLES_INDEX_UI_BEHAVIOR_RESTORE
5+
PR: BUILD_PR_SAMPLES_METADATA_TAG_NORMALIZATION_AND_FILTER_BAR_UX
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# BUILD_PR_SAMPLES_METADATA_TAG_NORMALIZATION_AND_FILTER_BAR_UX
2+
3+
## Purpose
4+
Improve `/samples/index.html` sample discovery by normalizing metadata and tightening the filter/tile UX without redesigning the page shell.
5+
6+
## Single PR Purpose
7+
Fix the samples metadata and filter/tile behavior in one focused pass:
8+
- make tags real tags instead of duplicated class values
9+
- expose the sample class list clearly
10+
- sort classes alphabetically
11+
- align search on the same row as Phase / Class / Tag
12+
- move the pin affordance to the top-right over the preview image if allowed by the current tile structure
13+
- use green instead of red when pinned
14+
15+
## Metadata Rules
16+
### Class
17+
- each sample must have a clear class value
18+
- class values should be broad categories
19+
- class options presented in the UI must be sorted alphabetically
20+
21+
### Tags
22+
- tags must be meaningful descriptive labels, not copies of class values
23+
- tags should reflect capabilities, behaviors, systems, or topics in the sample
24+
- examples:
25+
- physics
26+
- collision
27+
- parallax
28+
- networking
29+
- debug
30+
- editor
31+
- tilemap
32+
- input
33+
- particles
34+
- replay
35+
- tag filtering must use the real tag values
36+
37+
### Class List Visibility
38+
- include/display the class list for each sample
39+
- keep it readable and clearly distinct from tags
40+
41+
## Filter Bar UX Rules
42+
- search must be on the same line as:
43+
- Phase
44+
- Class
45+
- Tag
46+
- keep the filter bar compact and readable
47+
- preserve existing functionality
48+
- do not redesign the entire page
49+
50+
## Tile UX Rules
51+
### Pin
52+
- move pinned control/indicator to the top-right over the preview image if the tile structure allows it cleanly
53+
- pin should look like a pin, not a generic button
54+
- pinned visual state should change from red to green
55+
- preserve pinning behavior and pinned list behavior
56+
57+
### Preview Image
58+
- keep preview image as the launch anchor
59+
- preserve hover zoom behavior
60+
- do not break launch behavior
61+
62+
## Required Rules
63+
- no embedded `<style>` blocks
64+
- no inline `style=""`
65+
- no JS-generated styling
66+
- keep scope limited to `/samples/index.html` and directly related sample metadata/rendering/filter dependencies
67+
- preserve the accepted page shell and shared header/body consistency
68+
69+
## Acceptance
70+
- tags are real descriptive tags, not class duplicates
71+
- class values/lists are clearly available and class filter options are alphabetical
72+
- search is on the same line as Phase / Class / Tag
73+
- pinned control is top-right over the preview image when allowed
74+
- pinned state uses green, not red
75+
- preview image remains the launch anchor with hover zoom
76+
- filtering and pinning continue to work
77+
- change is visually and functionally testable

samples/index.css

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,20 @@
1010
gap: 6px;
1111
}
1212

13-
.samples-filter-search {
14-
grid-column: 1 / -1;
15-
}
16-
1713
.sample-card {
1814
overflow: hidden;
1915
}
2016

17+
.sample-preview-wrap {
18+
position: relative;
19+
margin-bottom: 10px;
20+
}
21+
2122
.sample-preview-link {
2223
display: block;
2324
border-radius: 10px;
2425
overflow: hidden;
2526
border: 1px solid var(--line, rgba(221, 214, 254, 0.26));
26-
margin-bottom: 10px;
2727
}
2828

2929
.sample-thumb {
@@ -57,30 +57,30 @@
5757
text-decoration: underline;
5858
}
5959

60-
.sample-pin-row {
61-
display: flex;
62-
justify-content: flex-end;
63-
}
64-
6560
.sample-pin-toggle {
6661
position: absolute;
6762
opacity: 0;
6863
pointer-events: none;
6964
}
7065

7166
.sample-pin-label {
67+
position: absolute;
68+
top: 8px;
69+
right: 8px;
70+
z-index: 2;
7271
display: inline-flex;
7372
align-items: center;
7473
justify-content: center;
7574
min-width: 32px;
7675
min-height: 32px;
7776
border-radius: 999px;
78-
border: 1px solid var(--line, rgba(221, 214, 254, 0.26));
77+
border: 1px solid rgba(34, 197, 94, 0.75);
78+
background: rgba(15, 23, 42, 0.72);
7979
cursor: pointer;
8080
user-select: none;
8181
}
8282

8383
.sample-pin-toggle:checked + .sample-pin-label {
84-
background: rgba(167, 139, 250, 0.34);
85-
border-color: var(--card-hover-border, #c4b5fd);
84+
background: rgba(34, 197, 94, 0.35);
85+
border-color: rgba(34, 197, 94, 0.95);
8686
}

samples/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ <h2>Filter Samples</h2>
5454
<option value="">All tags</option>
5555
</select>
5656
</div>
57-
<div class="samples-filter-field samples-filter-search">
57+
<div class="samples-filter-field">
5858
<label for="samples-phase-filter-input">Search</label>
5959
<input id="samples-phase-filter-input" type="text" placeholder="Phase 17, rendering, runtime..." autocomplete="off" />
6060
</div>

samples/index.render.js

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ function writePinnedSet(pinnedSet) {
5050
window.localStorage.setItem(PINNED_KEY, JSON.stringify([...pinnedSet].sort()));
5151
}
5252

53-
function buildClassTokens(engineClassesUsed) {
54-
const classEntries = asArray(engineClassesUsed)
55-
.map((entry) => normalize(entry))
56-
.filter(Boolean);
57-
return classEntries.map((entry) => {
58-
const name = entry.split("/").at(-1) || entry;
59-
return { value: entry, label: name };
60-
});
53+
function buildClassTokens(classValues, engineClassesUsed) {
54+
const classEntries = asArray(classValues).length > 0 ? asArray(classValues) : asArray(engineClassesUsed);
55+
const deduped = [...new Set(classEntries.map((entry) => normalize(entry)).filter(Boolean))];
56+
return deduped
57+
.map((entry) => {
58+
const name = entry.split("/").at(-1) || entry;
59+
return { value: entry, label: name };
60+
})
61+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
6162
}
6263

6364
function buildSampleRows(metadata, pinnedSet) {
@@ -90,7 +91,7 @@ function buildSampleRows(metadata, pinnedSet) {
9091
const description = normalize(sample?.description) || "No description available.";
9192
const href = normalize(sample?.href) || `./phase-${phase}/${id}/index.html`;
9293
const tags = asArray(sample?.tags).map((tag) => normalizeTag(tag)).filter(Boolean);
93-
const classTokens = buildClassTokens(sample?.engineClassesUsed);
94+
const classTokens = buildClassTokens(sample?.classValues, sample?.engineClassesUsed);
9495
const previewSrc = normalize(sample?.thumbnail) || normalize(sample?.preview) || "";
9596
return {
9697
id,
@@ -110,7 +111,11 @@ function buildSampleRows(metadata, pinnedSet) {
110111
.sort((a, b) => a.id.localeCompare(b.id));
111112

112113
const phases = [...new Set(sampleRows.map((sample) => sample.phase))].sort(sortPhase);
113-
const classes = [...new Set(sampleRows.flatMap((sample) => sample.classTokens.map((token) => token.value)))].sort();
114+
const classes = [...new Map(
115+
sampleRows.flatMap((sample) => sample.classTokens).map((token) => [token.value, token.label])
116+
).entries()]
117+
.map(([value, label]) => ({ value, label }))
118+
.sort((a, b) => a.label.localeCompare(b.label, undefined, { sensitivity: "base" }));
114119
const tags = [...new Set(sampleRows.flatMap((sample) => sample.tags))].sort();
115120

116121
return { sampleRows, phases, classes, tags, phaseInfoMap };
@@ -154,24 +159,14 @@ function groupByPhase(sampleRows, phaseInfoMap) {
154159
return [...grouped.values()].sort((a, b) => sortPhase(a.phase, b.phase));
155160
}
156161

157-
function renderPinnedList(container, rows) {
158-
container.innerHTML = "";
159-
if (rows.length === 0) {
160-
const note = document.createElement("p");
161-
note.textContent = "No pinned samples yet.";
162-
container.appendChild(note);
163-
return;
164-
}
165-
for (const sample of rows) {
166-
container.appendChild(buildSampleCard(sample));
167-
}
168-
}
169-
170162
function buildSampleCard(sample) {
171163
const card = document.createElement("article");
172164
card.className = "card-link sample-card";
173165
card.dataset.sampleId = sample.id;
174166

167+
const previewWrap = document.createElement("div");
168+
previewWrap.className = "sample-preview-wrap";
169+
175170
const launch = document.createElement("a");
176171
launch.className = "sample-preview-link";
177172
launch.href = sample.href;
@@ -183,32 +178,56 @@ function buildSampleCard(sample) {
183178
launch.classList.add("sample-preview-missing");
184179
}
185180

181+
const pinInputId = `sample-pin-${sample.id}`;
182+
const pinInput = document.createElement("input");
183+
pinInput.id = pinInputId;
184+
pinInput.type = "checkbox";
185+
pinInput.className = "sample-pin-toggle";
186+
pinInput.dataset.samplePin = sample.id;
187+
pinInput.checked = sample.pinned;
188+
189+
const pinLabel = document.createElement("label");
190+
pinLabel.className = "sample-pin-label";
191+
pinLabel.setAttribute("for", pinInputId);
192+
pinLabel.setAttribute("title", sample.pinned ? "Unpin" : "Pin");
193+
pinLabel.setAttribute("aria-label", sample.pinned ? "Unpin sample" : "Pin sample");
194+
pinLabel.textContent = "??";
195+
196+
previewWrap.appendChild(launch);
197+
previewWrap.appendChild(pinInput);
198+
previewWrap.appendChild(pinLabel);
199+
186200
const title = document.createElement("h3");
187201
title.innerHTML = `<a class="sample-title-link" href="${escapeHtml(sample.href)}">${escapeHtml(sample.title)}</a>`;
188202

189203
const description = document.createElement("p");
190204
description.textContent = sample.description;
191205

192206
const meta = document.createElement("p");
193-
const classLabel = sample.classTokens.length > 0
194-
? sample.classTokens.map((token) => token.label).join(", ")
195-
: "none";
207+
const classLabel = sample.classTokens.length > 0 ? sample.classTokens.map((token) => token.label).join(", ") : "none";
196208
const tagLabel = sample.tags.length > 0 ? sample.tags.join(", ") : "none";
197209
meta.textContent = `Phase ${sample.phase} | Classes: ${classLabel} | Tags: ${tagLabel}`;
198210

199-
const pinRow = document.createElement("div");
200-
pinRow.className = "sample-pin-row";
201-
const pinInputId = `sample-pin-${sample.id}`;
202-
pinRow.innerHTML = `<input id="${pinInputId}" type="checkbox" class="sample-pin-toggle" data-sample-pin="${sample.id}" ${sample.pinned ? "checked" : ""}><label for="${pinInputId}" class="sample-pin-label" title="${sample.pinned ? "Unpin" : "Pin"}">📌</label>`;
203-
204-
card.appendChild(launch);
211+
card.appendChild(previewWrap);
205212
card.appendChild(title);
206213
card.appendChild(description);
207214
card.appendChild(meta);
208-
card.appendChild(pinRow);
209215
return card;
210216
}
211217

218+
function renderPinnedList(container, rows) {
219+
container.innerHTML = "";
220+
if (rows.length === 0) {
221+
const note = document.createElement("p");
222+
note.textContent = "No pinned samples yet.";
223+
container.appendChild(note);
224+
return;
225+
}
226+
for (const sample of rows) {
227+
container.appendChild(buildSampleCard(sample));
228+
}
229+
}
230+
212231
function renderPhaseSections(container, phaseGroups) {
213232
container.innerHTML = "";
214233
for (const phaseGroup of phaseGroups) {
@@ -267,23 +286,26 @@ export async function initSamplesIndex() {
267286

268287
const model = buildSampleRows(metadata, pinnedSet);
269288
setSelectOptions(phaseSelect, model.phases, (value) => `Phase ${value}`);
270-
setSelectOptions(classSelect, model.classes, (value) => value.split("/").at(-1) || value);
289+
setSelectOptions(classSelect, model.classes.map((entry) => entry.value), (value) => {
290+
const found = model.classes.find((entry) => entry.value === value);
291+
return found?.label || value.split("/").at(-1) || value;
292+
});
271293
setSelectOptions(tagSelect, model.tags, (value) => value);
272294

273295
const render = () => {
274-
const model = buildSampleRows(metadata, pinnedSet);
296+
const nextModel = buildSampleRows(metadata, pinnedSet);
275297
const filterState = {
276298
phase: normalize(phaseSelect.value),
277299
className: normalize(classSelect.value),
278300
tag: normalize(tagSelect.value),
279301
query: normalize(searchInput.value)
280302
};
281-
const filteredRows = filterSampleRows(model.sampleRows, filterState);
303+
const filteredRows = filterSampleRows(nextModel.sampleRows, filterState);
282304
const pinnedRows = filteredRows.filter((entry) => entry.pinned);
283-
const phaseGroups = groupByPhase(filteredRows, model.phaseInfoMap);
305+
const phaseGroups = groupByPhase(filteredRows, nextModel.phaseInfoMap);
284306
renderPinnedList(pinnedContainer, pinnedRows);
285307
renderPhaseSections(listContainer, phaseGroups);
286-
updateStatus(statusNode, filteredRows, model.sampleRows, phaseGroups);
308+
updateStatus(statusNode, filteredRows, nextModel.sampleRows, phaseGroups);
287309
};
288310

289311
const handlePinEvent = (event) => {
@@ -317,4 +339,4 @@ export async function initSamplesIndex() {
317339

318340
if (typeof window !== "undefined" && typeof document !== "undefined") {
319341
initSamplesIndex();
320-
}
342+
}

0 commit comments

Comments
 (0)