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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Branch Validation: PASS

PASS - Current branch: pr/26174-ALFA-010-game-hub-source-idea-child-table-polish.
PASS - Stack base: pr/26174-ALFA-009-game-hub-parent-child-table-layout.
PASS - Changes are scoped to Source Idea child table rendering, targeted tests, and required reports.
PASS - No merge to main performed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Manual Validation Notes: PASS

PASS - Confirmed the Idea Board-created Lantern Reef project expands in Game Hub.
PASS - Confirmed Source Idea child table shows Idea, Pitch, and Note 1 rows.
PASS - Confirmed Source Idea child table has no input, textarea, select, or button controls.
PASS - Confirmed source details still come from the existing Game Hub project record contract.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Requirement Checklist: PASS

PASS - Source Idea appears as a child table under the game parent row.
PASS - Source Idea includes source idea details.
PASS - Source Idea includes notes as read-only child/context rows where data supports it.
PASS - Source Idea is read-only.
PASS - No edit/delete controls for source idea context.
PASS - Creator-facing labels are clear and non-technical.
PASS - Existing API/service contract is preserved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Validation Lane: PASS

PASS - npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -g "Idea Board uses accordion table ideas and notes"
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# PR_26174_ALFA_010-game-hub-source-idea-child-table-polish

## Purpose

Make Source Idea a dedicated child table under the expanded game row.

## Summary

- Added a Source Idea child table to expanded Game Hub parent rows.
- Displays source idea, pitch, and read-only source note rows when data is present.
- Kept source idea context free of edit/delete controls.
- Preserved the existing Local API/service contract and source idea normalization.

## Validation

PASS - `npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs -g "Idea Board uses accordion table ideas and notes"`
12 changes: 6 additions & 6 deletions docs_build/dev/reports/codex_changed_files.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
toolbox/game-hub/game-hub.js
tests/playwright/tools/GameHubMockRepository.spec.mjs
tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
docs_build/dev/reports/codex_review.diff
docs_build/dev/reports/codex_changed_files.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout.md
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_009-game-hub-parent-child-table-layout-manual-validation-notes.txt
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish.md
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-branch-validation.txt
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-requirement-checklist.txt
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-validation-lane.txt
docs_build/dev/reports/PR_26174_ALFA_010-game-hub-source-idea-child-table-polish-manual-validation-notes.txt
255 changes: 88 additions & 167 deletions docs_build/dev/reports/codex_review.diff
Original file line number Diff line number Diff line change
@@ -1,195 +1,116 @@
diff --git a/tests/playwright/tools/GameHubMockRepository.spec.mjs b/tests/playwright/tools/GameHubMockRepository.spec.mjs
index e11631269..695b0a609 100644
--- a/tests/playwright/tools/GameHubMockRepository.spec.mjs
+++ b/tests/playwright/tools/GameHubMockRepository.spec.mjs
@@ -264,10 +264,32 @@ test("Game Hub creates, opens, and deletes mock games", async ({ page }) => {
await expect(page.locator("[data-game-list]")).toContainText("Gravity Demo");
await expect(page.locator("[data-game-list]")).toContainText("Collision Demo");
await expect(page.locator("[data-game-list]")).toContainText("Camera Follow Demo");
+ await expect(page.locator("[data-game-parent-table='open-games']")).toHaveAttribute("aria-label", "Open Games");
+ await expect(page.locator("[data-game-parent-table='open-games'] caption")).toHaveText("Open Games");
+ await expect(page.locator("[data-game-parent-table='open-games'] thead th")).toHaveText([
+ "Game",
+ "Purpose",
+ "Status",
+ "Owner",
+ "Actions",
diff --git a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
index 722595ae9..f861f1f44 100644
--- a/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
+++ b/tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
@@ -392,6 +392,18 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note.");
await expect(page.locator("[data-source-idea-section] :is(input, textarea, select, button)")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0);
+ const activeGameRow = page.locator("[data-game-row][data-game-active='true']");
+ await expect(activeGameRow).toContainText("Lantern Reef");
+ await activeGameRow.locator("[data-game-toggle]").click();
+ const sourceIdeaChildTable = page.locator("[data-game-expanded-row] [data-game-child-table='source-idea']");
+ await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea");
+ await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]);
+ await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([
+ "IdeaLantern Reef",
+ "PitchGuide light through a reef that rearranges at dusk.",
+ "Note 1Use dusk tide changes as the first Game Hub planning note.",
+ ]);
const demoGameRow = page.locator("[data-game-row='demo-game']");
await expect(demoGameRow.locator("> .status")).toHaveCount(0);
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "false");
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveClass(/primary/);
await expect(demoGameRow.getByRole("button", { name: "Open Demo Game (Active)" })).toHaveAttribute("aria-current", "true");
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
+ await expect(demoGameRow.locator("[data-game-toggle='demo-game']")).toHaveAttribute("aria-expanded", "true");
+ await expect(page.locator("[data-game-row='demo-game'] + [data-game-expanded-row='demo-game']")).toHaveCount(1);
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary']")).toContainText("Game Summary");
+ await expect(page.locator("[data-game-expanded-row='demo-game'] [data-game-child-table='summary'] tbody tr")).toHaveText([
+ "ProjectDemo Game",
+ "PurposeGame",
+ "StatusUnder Construction",
+ "OwnerUser 1",
+ ]);
+ await demoGameRow.locator("[data-game-toggle='demo-game']").click();
+ await expect(page.locator("[data-game-expanded-row='demo-game']")).toHaveCount(0);

await page.getByLabel("Game Name").fill("Launch Test Game");
await page.getByRole("button", { name: "Create Game" }).click();
+ await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0);
await page.reload({ waitUntil: "networkidle" });
await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef");
await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef");
diff --git a/toolbox/game-hub/game-hub.js b/toolbox/game-hub/game-hub.js
index 2d6f321e4..121e82562 100644
index 121e82562..67f56ca41 100644
--- a/toolbox/game-hub/game-hub.js
+++ b/toolbox/game-hub/game-hub.js
@@ -39,6 +39,10 @@ const elements = {
tableCounts: document.querySelector("[data-game-table-counts]"),
};

+const state = {
+ expandedGameId: "",
+};
+
function setText(element, value) {
if (element && typeof element.forEach === "function" && !element.nodeType) {
element.forEach((item) => {
@@ -225,6 +229,90 @@ function createGameListStatus(message, state) {
return emptyState;
@@ -271,6 +271,54 @@ function renderGameSummaryChildTable(parent, game) {
parent.append(wrapper);
}

+function createCell(value, tagName = "td") {
+ const cell = document.createElement(tagName);
+ cell.textContent = value;
+ return cell;
+function gameSourceIdeaDetails(game) {
+ const sourceIdea = isRecord(game?.sourceIdea) ? game.sourceIdea : null;
+ const name = String(sourceIdea?.idea || "").trim();
+ const pitch = String(sourceIdea?.pitch || "").trim();
+ const notes = Array.isArray(sourceIdea?.notes)
+ ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean)
+ : [];
+ return {
+ name,
+ notes,
+ pitch,
+ };
+}
+
+function createGameToggleButton(game, expanded) {
+ const button = document.createElement("button");
+ button.className = expanded ? "btn btn--compact primary" : "btn btn--compact";
+ button.type = "button";
+ button.dataset.gameToggle = game.id;
+ button.setAttribute("aria-expanded", String(expanded));
+ button.setAttribute("aria-controls", `game-child-${game.id}`);
+ button.textContent = game.name;
+ return button;
+}
+
+function renderGameSummaryChildTable(parent, game) {
+function renderSourceIdeaChildTable(parent, game) {
+ const sourceIdea = gameSourceIdeaDetails(game);
+ const wrapper = document.createElement("div");
+ wrapper.className = "table-wrapper";
+ const table = document.createElement("table");
+ table.className = "data-table data-table--fixed";
+ table.dataset.gameChildTable = "summary";
+ table.setAttribute("aria-label", `${game.name} game summary`);
+ table.innerHTML = "<caption>Game Summary</caption><thead><tr><th scope=\"col\">Field</th><th scope=\"col\">Value</th></tr></thead>";
+ table.dataset.gameChildTable = "source-idea";
+ table.setAttribute("aria-label", `${game.name} source idea`);
+ table.innerHTML = "<caption>Source Idea</caption><thead><tr><th scope=\"col\">Context</th><th scope=\"col\">Details</th></tr></thead>";
+ const body = document.createElement("tbody");
+ [
+ ["Project", game.name],
+ ["Purpose", game.purpose],
+ ["Status", game.status],
+ ["Owner", game.ownerDisplayName],
+ ["Idea", sourceIdea.name || "No source idea yet"],
+ ["Pitch", sourceIdea.pitch || "Create a project from Idea Board to see source details."],
+ ].forEach(([label, value]) => {
+ const row = document.createElement("tr");
+ row.append(createCell(label, "th"), createCell(value || "Not set"));
+ row.append(createCell(label, "th"), createCell(value));
+ row.firstElementChild.scope = "row";
+ body.append(row);
+ });
+
+ const notes = sourceIdea.notes.length ? sourceIdea.notes : ["No source notes."];
+ notes.forEach((note, index) => {
+ const row = document.createElement("tr");
+ row.dataset.sourceIdeaNoteRow = String(index + 1);
+ row.append(createCell(`Note ${index + 1}`, "th"), createCell(note));
+ row.firstElementChild.scope = "row";
+ body.append(row);
+ });
+
+ table.append(body);
+ wrapper.append(table);
+ parent.append(wrapper);
+}
+
+function renderExpandedGameRow(tbody, game) {
+ const row = document.createElement("tr");
+ row.dataset.gameExpandedRow = game.id;
+ row.id = `game-child-${game.id}`;
+ const content = document.createElement("td");
+ content.colSpan = 5;
+ const stack = document.createElement("div");
+ stack.className = "content-stack content-stack--compact";
+ renderGameSummaryChildTable(stack, game);
+ content.append(stack);
+ row.append(content);
+ tbody.append(row);
+}
+
+function renderGameParentRow(tbody, game, activeGame) {
+ const expanded = state.expandedGameId === game.id;
+ const row = document.createElement("tr");
+ row.dataset.gameRow = game.id;
+ if (activeGame?.id === game.id) {
+ row.dataset.gameActive = "true";
+ }
+
+ const nameCell = document.createElement("th");
+ nameCell.scope = "row";
+ nameCell.append(createGameToggleButton(game, expanded));
+ row.append(
+ nameCell,
+ createCell(game.purpose || "Game"),
+ createCell(game.status || "No status"),
+ createCell(game.ownerDisplayName || "No owner"),
+ );
+
+ const actions = document.createElement("td");
+ actions.append(createGameButton(game, activeGame?.id === game.id));
+ row.append(actions);
+ tbody.append(row);
+
+ if (expanded) {
+ renderExpandedGameRow(tbody, game);
+ }
+}
+
function renderProjectInformation(activeGame, currentMember, progress) {
if (!elements.projectRecordsTable) {
return;
@@ -276,25 +364,18 @@ function renderGameList() {
return;
}

- listResult.forEach((game) => {
- const row = document.createElement("article");
- row.className = "callout";
- row.dataset.gameRow = game.id;
-
- const title = document.createElement("h4");
- title.textContent = game.name;
-
- const meta = document.createElement("p");
- meta.className = "eyebrow";
- meta.textContent = `${game.purpose} | ${game.status} | ${game.ownerDisplayName}`;
-
- const isActive = activeGame?.id === game.id;
- const action = createGameButton(game, isActive);
-
- row.append(title, meta, action);
-
- elements.gameList.append(row);
- });
+ const wrapper = document.createElement("div");
+ wrapper.className = "table-wrapper";
+ const table = document.createElement("table");
+ table.className = "data-table data-table--fixed";
+ table.dataset.gameParentTable = "open-games";
+ table.setAttribute("aria-label", "Open Games");
+ table.innerHTML = "<caption>Open Games</caption><thead><tr><th scope=\"col\">Game</th><th scope=\"col\">Purpose</th><th scope=\"col\">Status</th><th scope=\"col\">Owner</th><th scope=\"col\">Actions</th></tr></thead>";
+ const body = document.createElement("tbody");
+ listResult.forEach((game) => renderGameParentRow(body, game, activeGame));
+ table.append(body);
+ wrapper.append(table);
+ elements.gameList.append(wrapper);
function renderExpandedGameRow(tbody, game) {
const row = document.createElement("tr");
row.dataset.gameExpandedRow = game.id;
@@ -280,6 +328,7 @@ function renderExpandedGameRow(tbody, game) {
const stack = document.createElement("div");
stack.className = "content-stack content-stack--compact";
renderGameSummaryChildTable(stack, game);
+ renderSourceIdeaChildTable(stack, game);
content.append(stack);
row.append(content);
tbody.append(row);
@@ -443,20 +492,15 @@ function renderTableCounts() {
}

function renderMembersTable(activeGame) {
@@ -471,6 +552,13 @@ elements.form?.addEventListener("submit", (event) => {
});
function renderSourceIdea(activeGame) {
- const sourceIdea = isRecord(activeGame?.sourceIdea) ? activeGame.sourceIdea : null;
- const name = String(sourceIdea?.idea || "").trim();
- const pitch = String(sourceIdea?.pitch || "").trim();
- const notes = Array.isArray(sourceIdea?.notes)
- ? sourceIdea.notes.map((note) => String(note || "").trim()).filter(Boolean)
- : [];
+ const sourceIdea = gameSourceIdeaDetails(activeGame);

elements.gameList?.addEventListener("click", (event) => {
+ const toggle = event.target.closest("[data-game-toggle]");
+ if (toggle) {
+ state.expandedGameId = state.expandedGameId === toggle.dataset.gameToggle ? "" : toggle.dataset.gameToggle;
+ renderWorkspace();
+ return;
+ }
+
const button = event.target.closest("[data-game-open]");
- setText(elements.sourceIdeaName, name || "No source idea yet");
- setText(elements.sourceIdeaDisplay, name || "No source idea yet");
- setText(elements.sourceIdeaPitch, pitch || "Create a project from Idea Board to see source details.");
+ setText(elements.sourceIdeaName, sourceIdea.name || "No source idea yet");
+ setText(elements.sourceIdeaDisplay, sourceIdea.name || "No source idea yet");
+ setText(elements.sourceIdeaPitch, sourceIdea.pitch || "Create a project from Idea Board to see source details.");

if (!button) {
if (elements.sourceIdeaNotes) {
elements.sourceIdeaNotes.replaceChildren();
- const visibleNotes = notes.length ? notes : ["No source notes."];
+ const visibleNotes = sourceIdea.notes.length ? sourceIdea.notes : ["No source notes."];
visibleNotes.forEach((note) => {
const item = document.createElement("li");
item.textContent = note;
12 changes: 12 additions & 0 deletions tests/playwright/tools/IdeaBoardTableNotes.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,18 @@ test("Idea Board uses accordion table ideas and notes", async ({ page }) => {
await expect(page.locator("[data-source-idea-notes]")).toContainText("Use dusk tide changes as the first Game Hub planning note.");
await expect(page.locator("[data-source-idea-section] :is(input, textarea, select, button)")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Delete Open Game" })).toHaveCount(0);
const activeGameRow = page.locator("[data-game-row][data-game-active='true']");
await expect(activeGameRow).toContainText("Lantern Reef");
await activeGameRow.locator("[data-game-toggle]").click();
const sourceIdeaChildTable = page.locator("[data-game-expanded-row] [data-game-child-table='source-idea']");
await expect(sourceIdeaChildTable.locator("caption")).toHaveText("Source Idea");
await expect(sourceIdeaChildTable.locator("thead th")).toHaveText(["Context", "Details"]);
await expect(sourceIdeaChildTable.locator("tbody tr")).toHaveText([
"IdeaLantern Reef",
"PitchGuide light through a reef that rearranges at dusk.",
"Note 1Use dusk tide changes as the first Game Hub planning note.",
]);
await expect(sourceIdeaChildTable.locator(":is(input, textarea, select, button)")).toHaveCount(0);
await page.reload({ waitUntil: "networkidle" });
await expect(page.locator("[data-active-game-name]")).toHaveText("Lantern Reef");
await expect(page.locator("[data-game-list]")).toContainText("Lantern Reef");
Expand Down
Loading
Loading