') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); PR_26174_ALFA_018-game-selection-button-state by ToolboxAid · Pull Request #112 · ToolboxAid/HTML-JavaScript-Gaming · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions assets/theme-v2/css/tables.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -115,9 +115,10 @@ td {
cursor: pointer
}

.data-table [data-game-active-cell="true"] {
box-shadow: inset var(--space-3) 0 0 var(--gold);
border-bottom-color: var(--gold-border-muted)
.data-table [data-game-toggle][aria-current="true"] {
border-color: var(--gold);
background: color-mix(in srgb, var(--gold) 18%, var(--panel-soft));
box-shadow: inset 0 0 0 1px var(--gold-border-muted)
}

.idea-board-idea-label {
Expand Down
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
# PR_26174_ALFA_018-game-selection-button-state

## Summary

Moved Game Hub selected-game indication to the game button only.

## Implementation

- Removed active-game markers from parent rows.
- Removed active-game markers from parent table cells.
- Replaced the left-border cell highlight with selected styling on the game button.
- Removed the `Selected {game}.` status log update when selecting a game row.
- Preserved child row behavior so Source Idea and Readiness Output follow the selected game.
- Added targeted Playwright assertions for one selected button, unchanged sibling columns, child row movement, and no selected status copy.

## Scope Control

- Preserved the existing API/service contract.
- Preserved the Game row parent structure.
- Preserved Source Idea and Readiness Output child rows/tables.
- Did not add browser-owned product data.
- Did not introduce silent fallbacks.

## ZIP

- `tmp/PR_26174_ALFA_018-game-selection-button-state_delta.zip`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
Branch validation: PASS

Branch:
pr/26174-ALFA-018-game-selection-button-state

Base stack branch:
pr/26174-ALFA-017-game-hub-guest-save-and-crew-cleanup

Checks:
- Current branch is the ALFA_018 branch: PASS
- Worktree was clean before ALFA_018 edits: PASS
- Scope limited to Game Hub selection state, targeted Playwright coverage, and required reports: PASS
- No protected Project Instructions changes: PASS
- No merge to main performed: PASS
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
Manual validation notes: PASS

- Reviewed `docs_build/dev/ProjectInstructions/addendums/table_first_ui.md`.
- Confirmed selected state remains on the game button through `aria-current`.
- Confirmed parent rows no longer receive active attributes.
- Confirmed cells no longer receive active markers.
- Confirmed the old left-border cell selector was removed.
- Confirmed selecting another game leaves only one selected game button.
- Confirmed Source Idea and Readiness Output child rows move with the selected game.
- Confirmed the bottom status area no longer receives `Selected {game}.` when a game row is selected.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
Requirement checklist: PASS

- Selected game is indicated only by the Game button: PASS
- Row-level selection indicators removed: PASS
- Left border highlight removed: PASS
- Row background highlight removed: PASS
- Cell background highlights removed: PASS
- Purpose, Status, and Actions columns remain visually identical for all rows: PASS
- Only one game may be selected at a time: PASS
- Selecting a different game moves selected styling to that game button: PASS
- Child rows continue to follow the selected game: PASS
- Follow table_first_ui.md: PASS
- Removed selected-game status copy from bottom status area: PASS
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
Validation lane: PASS

Commands:
- `git diff --check -- toolbox/game-hub/game-hub.js assets/theme-v2/css/tables.css tests/playwright/tools/GameHubMockRepository.spec.mjs`
- PASS
- `node --check toolbox/game-hub/game-hub.js`
- PASS
- `npx playwright test tests/playwright/tools/GameHubMockRepository.spec.mjs -g "Game Hub"`
- PASS, 11 passed

Generated coverage reports were restored after Playwright validation to keep this PR scoped.
12 changes: 6 additions & 6 deletions docs_build/dev/reports/codex_changed_files.txt
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup.md
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_017-game-hub-guest-save-and-crew-cleanup_validation-lane.txt
assets/theme-v2/css/tables.css
docs_build/dev/reports/PR_26174_ALFA_018-game-selection-button-state.md
docs_build/dev/reports/PR_26174_ALFA_018-game-selection-button-state_branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_018-game-selection-button-state_manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_018-game-selection-button-state_requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_018-game-selection-button-state_validation-lane.txt
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/codex_review.diff
tests/playwright/tools/GameHubMockRepository.spec.mjs
toolbox/game-hub/game-hub.js
toolbox/game-hub/index.html
Binary file modifieddocs_build/dev/reports/codex_review.diff
Binary file not shown.
68 changes: 51 additions & 17 deletions tests/playwright/tools/GameHubMockRepository.spec.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -290,24 +290,48 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
const demoGameRow = page.locator("[data-game-row='demo-game']");
await expect(demoGameRow.locator("td")).toHaveText(["Game", "Under Construction", "Edit"]);
await expect(demoGameRow).not.toContainText("User 1");
await expect(demoGameRow).toHaveAttribute("data-game-active", "true");
await expect(demoGameRow).toHaveAttribute("aria-current", "true");
await expect(demoGameRow.locator("th[data-game-active-cell='true']")).toContainText("Demo Game");
const activeCellStyle = await demoGameRow.locator("th[data-game-active-cell='true']").evaluate((cell) => {
const styles = getComputedStyle(cell);
return {
backgroundColor: styles.backgroundColor,
boxShadow: styles.boxShadow,
};
});
const inactiveCellBackground = await page.locator("[data-game-row='gravity-demo'] th").evaluate((cell) => getComputedStyle(cell).backgroundColor);
expect(activeCellStyle.backgroundColor).toBe(inactiveCellBackground);
expect(activeCellStyle.boxShadow).not.toBe("none");
await expect(demoGameRow).not.toHaveAttribute("data-game-active", "true");
await expect(demoGameRow).not.toHaveAttribute("aria-current", "true");
await expect(demoGameRow.locator("th[data-game-active-cell='true']")).toHaveCount(0);
await expect(page.locator("[data-game-row][data-game-active='true']")).toHaveCount(0);
await expect(page.locator("[data-game-row][aria-current='true']")).toHaveCount(0);
await expect(page.locator("[data-game-active-cell='true']")).toHaveCount(0);
const rowVisuals = await page.locator("[data-game-row]").evaluateAll((rows) => rows.map((row) => {
const cells = Array.from(row.children).slice(1);
return cells.map((cell) => {
const styles = getComputedStyle(cell);
return {
backgroundColor: styles.backgroundColor,
boxShadow: styles.boxShadow,
};
});
}));
expect(rowVisuals[0]).toEqual(rowVisuals[1]);
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).not.toHaveClass(/primary/);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveClass(/\bbtn--compact\b/);
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("data-game-active", "true");
await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-current", "true");
await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1);
await expect(page.locator("[data-game-toggle][data-game-active='true']")).toHaveCount(1);
const activeButtonStyle = await demoGameRow.locator("[data-game-toggle='demo-game']").evaluate((button) => {
const styles = getComputedStyle(button);
return {
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
boxShadow: styles.boxShadow,
};
});
const inactiveButtonStyle = await page.locator("[data-game-row='gravity-demo'] [data-game-toggle='gravity-demo']").evaluate((button) => {
const styles = getComputedStyle(button);
return {
backgroundColor: styles.backgroundColor,
borderColor: styles.borderColor,
boxShadow: styles.boxShadow,
};
});
expect(activeButtonStyle).not.toEqual(inactiveButtonStyle);
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).toHaveText("Edit");
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).not.toHaveClass(/primary/);
await expect(demoGameRow.getByRole("button", { name: "Edit Demo Game" })).toHaveClass(/\bbtn--compact\b/);
Expand DownExpand Up@@ -348,7 +372,10 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await addGameRow.getByLabel("Status").selectOption("Ready for Testing");
await addGameRow.getByRole("button", { name: "Save" }).click();
await expect(page.locator("[data-game-list]")).toContainText("Launch Test Game");
await expect(page.locator("[data-game-row='launch-test-game-1']")).toHaveAttribute("data-game-active", "true");
await expect(page.locator("[data-game-row='launch-test-game-1']")).not.toHaveAttribute("data-game-active", "true");
await expect(page.locator("[data-game-row='launch-test-game-1']")).not.toHaveAttribute("aria-current", "true");
await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1);
await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveAttribute("aria-current", "true");
await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Edit Launch Test Game" })).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-row='launch-test-game-1'] td").nth(0)).toHaveText("Learning Game");
Expand DownExpand Up@@ -376,14 +403,21 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await page.getByRole("button", { name: "Add Game" }).click();
await page.locator("[data-game-add-row='input']").getByLabel("Game").fill("Archive Game");
await page.locator("[data-game-add-row='input']").getByRole("button", { name: "Save" }).click();
await expect(page.locator("[data-game-row='archive-game-2']")).toHaveAttribute("data-game-active", "true");
await expect(page.locator("[data-game-row='archive-game-2']")).not.toHaveAttribute("data-game-active", "true");
await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).toHaveAttribute("aria-current", "true");
await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).not.toHaveClass(/primary/);

await page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']").click();
await expect(page.locator("[data-game-row='launch-test-game-1']")).toHaveAttribute("data-game-active", "true");
await expect(page.locator("[data-game-row='launch-test-game-1']")).not.toHaveAttribute("data-game-active", "true");
await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).toHaveAttribute("aria-current", "true");
await expect(page.locator("[data-game-row='archive-game-2'] [data-game-toggle='archive-game-2']")).not.toHaveAttribute("aria-current", "true");
await expect(page.locator("[data-game-toggle][aria-current='true']")).toHaveCount(1);
await expect(page.locator("[data-game-toggle][data-game-active='true']")).toHaveCount(1);
await expect(page.locator("[data-game-expanded-row='launch-test-game-1']")).toHaveCount(2);
await expect(page.locator("[data-game-expanded-row='archive-game-2']")).toHaveCount(0);
await expect(page.locator("[data-game-row='launch-test-game-1'] [data-game-toggle='launch-test-game-1']")).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-row='launch-test-game-1']").getByRole("button", { name: "Edit Launch Test Game" })).not.toHaveClass(/primary/);
await expect(page.locator("[data-game-hub-log]")).toHaveText("Selected Launch Test Game.");
await expect(page.locator("[data-game-hub-log]")).not.toHaveText("Selected Launch Test Game.");

await page.getByRole("button", { name: "Delete Open Game" }).click();
await expect(page.locator("[data-game-row='launch-test-game-1']")).toHaveCount(0);
Expand Down
10 changes: 0 additions & 10 deletions toolbox/game-hub/game-hub.js
Original file line numberDiff line numberDiff line change
Expand Up@@ -482,16 +482,9 @@ function renderGameParentRow(tbody, game, activeGame, progress) {

const row = document.createElement("tr");
row.dataset.gameRow = game.id;
if (active) {
row.dataset.gameActive = "true";
row.setAttribute("aria-current", "true");
}

const nameCell = document.createElement("th");
nameCell.scope = "row";
if (active) {
nameCell.dataset.gameActiveCell = "true";
}
nameCell.append(createGameToggleButton(game, expanded, active));
row.append(
nameCell,
Expand DownExpand Up@@ -731,9 +724,6 @@ elements.gameList?.addEventListener("click", (event) => {
renderWorkspace();
return;
}
if (game) {
setStatusLog(`Selected ${game.name}.`);
}
state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle;
renderWorkspace();
return;
Expand Down
Loading