Skip to content

Commit 8330579

Browse files
author
DavidQ
committed
BUILD_PR: enhance sample detail pages with metadata-driven UI and navigation
1 parent b558f3a commit 8330579

204 files changed

Lines changed: 572 additions & 111 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/dev/CODEX_COMMANDS.md

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

44
COMMAND:
5-
Create PLAN_PR_SAMPLES_DETAIL_PAGE_ENHANCEMENT
5+
Execute BUILD_PR_SAMPLES_DETAIL_PAGE_ENHANCEMENT
6+
7+
ENVIRONMENT:
8+
- Windows
9+
- Use Node.js or vanilla JS only
10+
- No npm install
11+
- No node_modules
612

713
RULES:
8-
- Docs only
9-
- No implementation code
10-
- No gameplay changes
11-
- No engine changes
14+
- Do not change canonical paths
15+
- Do not modify gameplay
16+
- Keep changes minimal
17+
18+
VALIDATION:
19+
- sample page shows title/description
20+
- navigation works
21+
- related samples resolve
22+
- 1316–1318 still load
1223

1324
ZIP:
14-
<project folder>/tmp/PLAN_PR_SAMPLES_DETAIL_PAGE_ENHANCEMENT.zip
25+
<project folder>/tmp/BUILD_PR_SAMPLES_DETAIL_PAGE_ENHANCEMENT.zip

docs/dev/COMMIT_COMMENT.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
PLAN_PR: enhance sample detail pages with metadata-driven UI
1+
BUILD_PR: enhance sample detail pages with metadata-driven UI and navigation
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Define improvements for sample pages including headers, previews, and navigation
1+
Add header, preview, related samples, and navigation to sample pages
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
MODIFIED:
2+
- sample pages (index.html in samples)
3+
14
ADDED:
2-
- docs/pr/PLAN_PR_SAMPLES_DETAIL_PAGE_ENHANCEMENT.md
5+
- minimal UI JS for metadata rendering
Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
[ ] header enhancement defined
2-
[ ] preview support defined
3-
[ ] related samples defined
4-
[ ] navigation defined
5-
[ ] no path changes
1+
[ ] header renders
2+
[ ] preview optional works
3+
[ ] navigation works
4+
[ ] related samples work
5+
[ ] links resolve (1316–1318)
6+
[ ] no console errors
67
[ ] zip exists
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# BUILD_PR_SAMPLES_DETAIL_PAGE_ENHANCEMENT
2+
3+
## Objective
4+
Implement metadata-driven enhancements on each sample page:
5+
- title + description header
6+
- optional preview/screenshot
7+
- next/previous navigation
8+
- related samples
9+
10+
## Scope
11+
- read metadata layer
12+
- render header UI on sample pages
13+
- add navigation + related links
14+
15+
## Out of Scope
16+
- gameplay changes
17+
- engine changes
18+
- path changes
19+
20+
## Acceptance
21+
- sample pages render metadata
22+
- navigation works
23+
- related samples load correctly
24+
- no console errors
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
const METADATA_URL = '/samples/metadata/samples.index.metadata.json';
2+
const BACK_TO_SAMPLES_HREF = '/samples/index.html';
3+
const ENHANCER_STYLE_ID = 'sample-detail-enhancement-style';
4+
const ENHANCER_ROOT_ID = 'sample-detail-enhancement-root';
5+
6+
export function parseSampleFromPathname(pathname) {
7+
const text = String(pathname || '');
8+
const direct = text.match(/\/samples\/phase(\d{2})\/(\d{4})(?:\/index\.html)?\/?$/);
9+
if (direct) {
10+
return { phase: direct[1], id: direct[2] };
11+
}
12+
13+
const parts = text.split('/').filter(Boolean);
14+
for (let i = 0; i < parts.length - 1; i += 1) {
15+
if (/^phase\d{2}$/.test(parts[i]) && /^\d{4}$/.test(parts[i + 1])) {
16+
return { phase: parts[i].slice(5), id: parts[i + 1] };
17+
}
18+
}
19+
return null;
20+
}
21+
22+
export function canonicalSampleHref(sample) {
23+
return '/samples/phase' + sample.phase + '/' + sample.id + '/index.html';
24+
}
25+
26+
export function normalizeMetadata(raw) {
27+
if (!raw || typeof raw !== 'object') {
28+
throw new Error('Metadata root must be an object.');
29+
}
30+
const samples = Array.isArray(raw.samples) ? raw.samples : [];
31+
32+
const normalized = samples.map((entry) => ({
33+
id: String(entry.id || '').trim(),
34+
phase: String(entry.phase || '').trim(),
35+
title: String(entry.title || '').trim(),
36+
description: String(entry.description || '').trim(),
37+
tags: Array.isArray(entry.tags) ? entry.tags.map((tag) => String(tag).trim()).filter(Boolean) : [],
38+
preview: entry && typeof entry.preview === 'string' ? entry.preview.trim() : ''
39+
}));
40+
41+
const valid = normalized.filter((entry) => /^\d{4}$/.test(entry.id) && /^\d{2}$/.test(entry.phase));
42+
valid.sort((a, b) => a.id.localeCompare(b.id));
43+
return valid;
44+
}
45+
46+
export function buildDetailModel(samples, currentId) {
47+
const list = Array.isArray(samples) ? samples : [];
48+
const index = list.findIndex((entry) => entry.id === currentId);
49+
if (index < 0) {
50+
return null;
51+
}
52+
53+
const current = list[index];
54+
const previous = index > 0 ? list[index - 1] : null;
55+
const next = index < list.length - 1 ? list[index + 1] : null;
56+
57+
const samePhase = list.filter((entry) => entry.phase === current.phase && entry.id !== current.id);
58+
const related = samePhase.slice(0, 3);
59+
60+
return { current, previous, next, related };
61+
}
62+
63+
function ensureStyles() {
64+
if (document.getElementById(ENHANCER_STYLE_ID)) {
65+
return;
66+
}
67+
68+
const style = document.createElement('style');
69+
style.id = ENHANCER_STYLE_ID;
70+
style.textContent = [
71+
'.sample-detail-enhancement {',
72+
' margin: 0 0 14px;',
73+
' padding: 12px;',
74+
' border: 1px solid #2d3a52;',
75+
' border-radius: 8px;',
76+
' background: #111826;',
77+
' color: #e7edf8;',
78+
'}',
79+
'.sample-detail-enhancement h2 { margin: 0 0 6px; font-size: 1.1rem; }',
80+
'.sample-detail-enhancement p { margin: 0 0 8px; color: #c7d5ea; }',
81+
'.sample-detail-enhancement .meta-row { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0; }',
82+
'.sample-detail-enhancement .chip {',
83+
' display: inline-block;',
84+
' padding: 3px 8px;',
85+
' border-radius: 999px;',
86+
' border: 1px solid #415172;',
87+
' background: #1a2538;',
88+
' font: 600 12px/1.2 monospace;',
89+
'}',
90+
'.sample-detail-enhancement nav { display: flex; flex-wrap: wrap; gap: 12px; margin: 10px 0 2px; }',
91+
'.sample-detail-enhancement a { color: #9bd1ff; }',
92+
'.sample-detail-enhancement .related { margin: 10px 0 0; }',
93+
'.sample-detail-enhancement .related ul { margin: 6px 0 0 18px; padding: 0; }',
94+
'.sample-detail-enhancement .related li { margin: 2px 0; }',
95+
'.sample-detail-enhancement figure { margin: 10px 0 0; }',
96+
'.sample-detail-enhancement img { max-width: 100%; height: auto; border-radius: 6px; border: 1px solid #374765; }'
97+
].join('\n');
98+
99+
document.head.appendChild(style);
100+
}
101+
102+
function createLink(href, text) {
103+
const link = document.createElement('a');
104+
link.href = href;
105+
link.textContent = text;
106+
return link;
107+
}
108+
109+
function buildEnhancementElement(model) {
110+
const root = document.createElement('section');
111+
root.className = 'sample-detail-enhancement';
112+
root.id = ENHANCER_ROOT_ID;
113+
114+
const title = document.createElement('h2');
115+
title.textContent = 'Sample ' + model.current.id + ' - ' + model.current.title;
116+
root.appendChild(title);
117+
118+
const description = document.createElement('p');
119+
description.textContent = model.current.description || 'No description available.';
120+
root.appendChild(description);
121+
122+
const metaRow = document.createElement('div');
123+
metaRow.className = 'meta-row';
124+
125+
const phaseChip = document.createElement('span');
126+
phaseChip.className = 'chip';
127+
phaseChip.textContent = 'Phase ' + model.current.phase;
128+
metaRow.appendChild(phaseChip);
129+
130+
const tags = model.current.tags.length > 0 ? model.current.tags : ['Untagged'];
131+
for (const tag of tags) {
132+
const tagChip = document.createElement('span');
133+
tagChip.className = 'chip';
134+
tagChip.textContent = tag;
135+
metaRow.appendChild(tagChip);
136+
}
137+
138+
root.appendChild(metaRow);
139+
140+
const nav = document.createElement('nav');
141+
nav.appendChild(createLink(BACK_TO_SAMPLES_HREF, 'Back to Samples'));
142+
143+
if (model.previous) {
144+
nav.appendChild(createLink(canonicalSampleHref(model.previous), 'Previous: ' + model.previous.id));
145+
}
146+
if (model.next) {
147+
nav.appendChild(createLink(canonicalSampleHref(model.next), 'Next: ' + model.next.id));
148+
}
149+
150+
root.appendChild(nav);
151+
152+
if (model.current.preview) {
153+
const figure = document.createElement('figure');
154+
const image = document.createElement('img');
155+
image.src = model.current.preview;
156+
image.alt = 'Preview for sample ' + model.current.id;
157+
figure.appendChild(image);
158+
root.appendChild(figure);
159+
}
160+
161+
const relatedWrap = document.createElement('div');
162+
relatedWrap.className = 'related';
163+
const relatedHeading = document.createElement('strong');
164+
relatedHeading.textContent = 'Related samples';
165+
relatedWrap.appendChild(relatedHeading);
166+
167+
if (model.related.length === 0) {
168+
const none = document.createElement('p');
169+
none.textContent = 'No related samples available.';
170+
relatedWrap.appendChild(none);
171+
} else {
172+
const list = document.createElement('ul');
173+
for (const sample of model.related) {
174+
const item = document.createElement('li');
175+
item.appendChild(createLink(canonicalSampleHref(sample), 'Sample ' + sample.id + ' - ' + sample.title));
176+
list.appendChild(item);
177+
}
178+
relatedWrap.appendChild(list);
179+
}
180+
181+
root.appendChild(relatedWrap);
182+
return root;
183+
}
184+
185+
async function loadMetadata() {
186+
const response = await fetch(METADATA_URL, { cache: 'no-store' });
187+
if (!response.ok) {
188+
throw new Error('Metadata request failed: ' + response.status);
189+
}
190+
return response.json();
191+
}
192+
193+
export async function applySampleDetailEnhancement() {
194+
const current = parseSampleFromPathname(window.location.pathname);
195+
if (!current) {
196+
return;
197+
}
198+
199+
const mount = document.querySelector('main');
200+
if (!mount || document.getElementById(ENHANCER_ROOT_ID)) {
201+
return;
202+
}
203+
204+
const raw = await loadMetadata();
205+
const samples = normalizeMetadata(raw);
206+
const model = buildDetailModel(samples, current.id);
207+
if (!model) {
208+
return;
209+
}
210+
211+
ensureStyles();
212+
const element = buildEnhancementElement(model);
213+
mount.insertBefore(element, mount.firstChild);
214+
215+
const titleText = 'Sample ' + model.current.id + ' - ' + model.current.title;
216+
if (!document.title || !document.title.startsWith('Sample ' + model.current.id)) {
217+
document.title = titleText;
218+
}
219+
}
220+
221+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
222+
applySampleDetailEnhancement().catch((error) => {
223+
console.warn('[sample-detail-enhancement]', error && error.message ? error.message : error);
224+
});
225+
}

samples/phase01/0101/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ <h3>Engine Classes Used</h3>
3131
</section>
3232
</main>
3333

34-
<script type="module" src="./main.js"></script>
34+
<script type="module" src="/samples/_shared/sampleDetailPageEnhancement.js"></script>
35+
<script type="module" src="./main.js"></script>
3536
</body>
3637
</html>
3738

samples/phase01/0102/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ <h3>Engine Classes Used</h3>
3232
</section>
3333
</main>
3434

35-
<script type="module" src="./main.js"></script>
35+
<script type="module" src="/samples/_shared/sampleDetailPageEnhancement.js"></script>
36+
<script type="module" src="./main.js"></script>
3637
</body>
3738
</html>
3839

samples/phase01/0103/index.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ <h3>Engine Classes Used</h3>
3232
</section>
3333
</main>
3434

35-
<script type="module" src="./main.js"></script>
35+
<script type="module" src="/samples/_shared/sampleDetailPageEnhancement.js"></script>
36+
<script type="module" src="./main.js"></script>
3637
</body>
3738
</html>
3839

0 commit comments

Comments
 (0)