From a32c2e208821665b126417278cb56c8616690712 Mon Sep 17 00:00:00 2001 From: Alfa Team Date: Wed, 24 Jun 2026 18:13:38 -0400 Subject: [PATCH] WIP preserve ALFA_048 chevron conversion --- assets/theme-v2/css/accordion.css | 89 +- assets/theme-v2/css/icons.css | 88 ++ assets/theme-v2/css/panels.css | 30 +- assets/theme-v2/css/tables.css | 19 +- assets/theme-v2/css/theme.css | 1 + assets/theme-v2/js/theme-icons.js | 73 + assets/theme-v2/js/tool-display-mode.js | 108 +- assets/toolbox/idea-board/js/index.js | 8 +- ...vron-conversion_manual-validation-notes.md | 6 + ..._048-theme-v2-chevron-conversion_report.md | 15 + ...evron-conversion_requirements-checklist.md | 11 + ...e-v2-chevron-conversion_validation-lane.md | 19 + .../dev/reports/codex_changed_files.txt | 53 +- docs_build/dev/reports/codex_review.diff | 1363 ++++++++--------- .../tools/ThemeV2SvgIconRegistry.spec.mjs | 70 +- 15 files changed, 1080 insertions(+), 873 deletions(-) create mode 100644 assets/theme-v2/css/icons.css create mode 100644 assets/theme-v2/js/theme-icons.js create mode 100644 docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_manual-validation-notes.md create mode 100644 docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_report.md create mode 100644 docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_requirements-checklist.md create mode 100644 docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_validation-lane.md diff --git a/assets/theme-v2/css/accordion.css b/assets/theme-v2/css/accordion.css index d4bf86f9a..b650f518e 100644 --- a/assets/theme-v2/css/accordion.css +++ b/assets/theme-v2/css/accordion.css @@ -50,7 +50,7 @@ details.vertical-accordion summary::-webkit-details-marker { display: none } -details.vertical-accordion summary:after { +.horizontal-accordion-toggle { --accordion-button-border: var(--line); --accordion-button-background: var(--panel-soft); --accordion-button-color: var(--gold); @@ -66,74 +66,53 @@ details.vertical-accordion summary:after { line-height: var(--line-height-single); display: inline-grid; place-items: center; - flex: 0 0 auto + flex: 0 0 auto; + margin-left: auto; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-heavy); + overflow: hidden; + position: relative } -.horizontal-accordion-toggle { +.horizontal-accordion-toggle__icon, +.vertical-accordion__chevron, +.tool-display-mode__chevron { --accordion-button-border: var(--line); --accordion-button-background: var(--panel-soft); --accordion-button-color: var(--gold); - min-width: var(--space-20); - width: var(--space-20); - height: var(--space-20); - padding: var(--space-0); + align-items: center; + background: var(--accordion-button-background); border: var(--border-standard); border-color: var(--accordion-button-border); border-radius: var(--radius-pill); - background: var(--accordion-button-background); color: var(--accordion-button-color); - line-height: var(--line-height-single); - display: inline-grid; - place-items: center; + display: inline-flex; flex: 0 0 auto; - margin-left: auto; - font-size: var(--space-0); - overflow: hidden; - position: relative + height: var(--space-20); + justify-content: center; + line-height: var(--line-height-single); + min-width: var(--space-20); + padding: var(--space-0); + width: var(--space-20) } -.horizontal-accordion-toggle:before { - content: ""; - position: absolute; - top: 50%; - left: 50%; - width: var(--space-14); - height: var(--space-14); +.horizontal-accordion-toggle__icon { background: currentColor; - clip-path: polygon(70% 50%, 30% 20%, 30% 80%); - transform-origin: center -} - -.horizontal-accordion-toggle[aria-expanded="true"]:before { - transform: translate(-50%, -50%) -} - -.horizontal-accordion-toggle[aria-expanded="false"]:before { - transform: translate(-50%, -50%) rotate(180deg) -} - -.horizontal-accordion-toggle--left[aria-expanded="true"]:before { - transform: translate(-50%, -50%) rotate(180deg) -} - -.horizontal-accordion-toggle--left[aria-expanded="false"]:before { - transform: translate(-50%, -50%) -} - -details.vertical-accordion summary:after { - content: ""; - background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); - background-position: calc(50% - var(--space-3)) 50%, calc(50% + var(--space-3)) 50%; - background-size: var(--space-7) var(--space-7); - background-repeat: no-repeat + border: 0; + border-radius: var(--space-0); + height: var(--space-14); + min-width: var(--space-14); + width: var(--space-14) } -details.vertical-accordion[open] summary:after { - background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) +.vertical-accordion__chevron { + margin-left: auto } -details.vertical-accordion summary:active:after { - background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) +.vertical-accordion__chevron .theme-icon, +.tool-display-mode__chevron .theme-icon { + height: var(--space-14); + width: var(--space-14) } details.vertical-accordion summary .status { @@ -150,7 +129,7 @@ details.vertical-accordion summary.accordion-summary--control-grid { align-items: center } -details.vertical-accordion summary.accordion-summary--control-grid:after { +details.vertical-accordion summary.accordion-summary--control-grid .vertical-accordion__chevron { justify-self: end } @@ -188,10 +167,6 @@ details.vertical-accordion summary.accordion-summary--control-grid .accordion-su min-width: var(--space-34) } -details.vertical-accordion[open] summary:active:after { - background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%) -} - .accordion-body { padding: var(--space-0) var(--space-14) var(--space-14); color: var(--muted); diff --git a/assets/theme-v2/css/icons.css b/assets/theme-v2/css/icons.css new file mode 100644 index 000000000..ce1af7c8c --- /dev/null +++ b/assets/theme-v2/css/icons.css @@ -0,0 +1,88 @@ +.theme-icon { + --theme-v2-icon-url: none; + background: currentColor; + color: inherit; + display: inline-block; + flex: 0 0 auto; + height: 1em; + line-height: var(--line-height-single); + pointer-events: none; + vertical-align: -0.125em; + width: 1em; + -webkit-mask-image: var(--theme-v2-icon-url); + mask-image: var(--theme-v2-icon-url); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-size: contain; + mask-size: contain +} + +.theme-icon--add { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-add.svg") +} + +.theme-icon--chevron-down { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-down.svg") +} + +.theme-icon--chevron-left { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-left.svg") +} + +.theme-icon--chevron-right { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-right.svg") +} + +.theme-icon--chevron-up { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-up.svg") +} + +.theme-icon--close { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-close.svg") +} + +.theme-icon--error { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-error.svg") +} + +.theme-icon--exit-fullscreen { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-exit-fullscreen.svg") +} + +.theme-icon--fullscreen { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-fullscreen.svg") +} + +.theme-icon--info { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-info.svg") +} + +.theme-icon--menu { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-menu.svg") +} + +.theme-icon--search { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-search.svg") +} + +.theme-icon--settings { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-settings.svg") +} + +.theme-icon--subtract { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-subtract.svg") +} + +.theme-icon--success { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-success.svg") +} + +.theme-icon--trash { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-trash.svg") +} + +.theme-icon--warning { + --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-warning.svg") +} diff --git a/assets/theme-v2/css/panels.css b/assets/theme-v2/css/panels.css index bcfa2785f..b80582336 100644 --- a/assets/theme-v2/css/panels.css +++ b/assets/theme-v2/css/panels.css @@ -206,7 +206,7 @@ } .tool-column[class*="tool-group-"] .horizontal-accordion-toggle, -.tool-column[class*="tool-group-"] details.vertical-accordion summary:after { +.tool-column[class*="tool-group-"] details.vertical-accordion .vertical-accordion__chevron { --accordion-button-border: var(--tool-group-accent); --accordion-button-color: var(--tool-group-color) } @@ -298,36 +298,12 @@ body.tool-focus-mode .tool-center-panel:has(>details.vertical-accordion)>p { display: none } -.tool-display-mode summary:after { - content: ""; +.tool-display-mode__chevron { position: absolute; right: var(--space-12); top: 50%; transform: translateY(-50%); - width: var(--space-20); - height: var(--space-20); - border: var(--border-standard); - border-radius: var(--radius-pill); - background: var(--panel-soft); - color: var(--gold); - display: inline-grid; - place-items: center; - background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); - background-position: calc(50% - var(--space-3)) 50%, calc(50% + var(--space-3)) 50%; - background-size: var(--space-7) var(--space-7); - background-repeat: no-repeat -} - -.tool-display-mode[open] summary:after { - background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) -} - -.tool-display-mode summary:active:after { - background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) -} - -.tool-display-mode[open] summary:active:after { - background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%) + z-index: var(--z-index-sm) } .tool-display-mode__badge { diff --git a/assets/theme-v2/css/tables.css b/assets/theme-v2/css/tables.css index 5575ff684..915a235e9 100644 --- a/assets/theme-v2/css/tables.css +++ b/assets/theme-v2/css/tables.css @@ -131,27 +131,10 @@ td { .idea-board-idea-chevron { display: inline-block; - width: 1em; height: 1em; margin-right: .35em; - background: currentColor; vertical-align: -0.125em; - -webkit-mask-position: center; - mask-position: center; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: contain; - mask-size: contain -} - -.idea-board-idea-chevron--down { - -webkit-mask-image: url("../images/gfs-chevron-down.svg"); - mask-image: url("../images/gfs-chevron-down.svg") -} - -.idea-board-idea-chevron--up { - -webkit-mask-image: url("../images/gfs-chevron-up.svg"); - mask-image: url("../images/gfs-chevron-up.svg") + width: 1em } .idea-board-notes-child-surface { diff --git a/assets/theme-v2/css/theme.css b/assets/theme-v2/css/theme.css index 944bfa356..fb8af1256 100644 --- a/assets/theme-v2/css/theme.css +++ b/assets/theme-v2/css/theme.css @@ -4,6 +4,7 @@ @import url("typography.css"); @import url("layout.css"); @import url("buttons.css"); +@import url("icons.css"); @import url("forms.css"); @import url("controls.css"); @import url("panels.css"); diff --git a/assets/theme-v2/js/theme-icons.js b/assets/theme-v2/js/theme-icons.js new file mode 100644 index 000000000..442614fc1 --- /dev/null +++ b/assets/theme-v2/js/theme-icons.js @@ -0,0 +1,73 @@ +const themeV2IconRegistry = Object.freeze({ + add: "gfs-add.svg", + "chevron-down": "gfs-chevron-down.svg", + "chevron-left": "gfs-chevron-left.svg", + "chevron-right": "gfs-chevron-right.svg", + "chevron-up": "gfs-chevron-up.svg", + close: "gfs-close.svg", + error: "gfs-error.svg", + "exit-fullscreen": "gfs-exit-fullscreen.svg", + fullscreen: "gfs-fullscreen.svg", + info: "gfs-info.svg", + menu: "gfs-menu.svg", + search: "gfs-search.svg", + settings: "gfs-settings.svg", + subtract: "gfs-subtract.svg", + success: "gfs-success.svg", + trash: "gfs-trash.svg", + warning: "gfs-warning.svg", +}); + +function themeIconFileName(name) { + const fileName = themeV2IconRegistry[name]; + if (!fileName) { + throw new RangeError(`Unknown Theme V2 icon: ${name}`); + } + return fileName; +} + +function themeIconPath(name) { + return `/assets/theme-v2/svg/${themeIconFileName(name)}`; +} + +function normalizeClassName(className) { + if (Array.isArray(className)) { + return className.filter(Boolean).join(" "); + } + return className || ""; +} + +function createThemeIcon(name, options = {}) { + const icon = document.createElement("span"); + const extraClassName = normalizeClassName(options.className); + icon.className = ["theme-icon", `theme-icon--${name}`, extraClassName].filter(Boolean).join(" "); + icon.dataset.themeIcon = name; + icon.dataset.themeIconFile = themeIconFileName(name); + + if (options.label) { + icon.setAttribute("role", "img"); + icon.setAttribute("aria-label", options.label); + } else { + icon.setAttribute("aria-hidden", "true"); + } + + return icon; +} + +const themeIconsApi = Object.freeze({ + createThemeIcon, + themeIconFileName, + themeIconPath, + themeV2IconRegistry, +}); + +if (typeof window !== "undefined") { + window.ThemeV2Icons = themeIconsApi; +} + +export { + createThemeIcon, + themeIconFileName, + themeIconPath, + themeV2IconRegistry, +}; diff --git a/assets/theme-v2/js/tool-display-mode.js b/assets/theme-v2/js/tool-display-mode.js index 08bada607..b3f6f3265 100644 --- a/assets/theme-v2/js/tool-display-mode.js +++ b/assets/theme-v2/js/tool-display-mode.js @@ -12,6 +12,102 @@ const toolName = pageTitle ? pageTitle.textContent.trim() : "Tool"; const routeSlug = window.location.pathname.split("/").pop().replace(/\.html$/, ""); const toolSlug = slot.dataset.toolSlug || routeSlug; + let themeIconRegistry = window.ThemeV2Icons || null; + + function fallbackThemeIconFileName(name) { + return "gfs-" + name + ".svg"; + } + + function createThemeIconNode(name, className) { + if (themeIconRegistry && typeof themeIconRegistry.createThemeIcon === "function") { + return themeIconRegistry.createThemeIcon(name, { className }); + } + + const icon = document.createElement("span"); + icon.className = ["theme-icon", "theme-icon--" + name, className].filter(Boolean).join(" "); + icon.dataset.themeIcon = name; + icon.dataset.themeIconFile = fallbackThemeIconFileName(name); + icon.setAttribute("aria-hidden", "true"); + return icon; + } + + function createChevronShell(name, shellClassName, iconClassName) { + const shell = document.createElement("span"); + shell.className = shellClassName; + shell.setAttribute("aria-hidden", "true"); + shell.appendChild(createThemeIconNode(name, iconClassName)); + return shell; + } + + function replaceIconNode(parent, selector, icon) { + const current = parent.querySelector(selector); + if (current) { + current.replaceWith(icon); + } else { + parent.appendChild(icon); + } + } + + function updateVerticalAccordionChevron(details) { + const accordionSummary = details.querySelector(":scope > summary"); + if (!accordionSummary) return; + const iconName = details.open ? "chevron-up" : "chevron-down"; + const shell = createChevronShell(iconName, "vertical-accordion__chevron", "vertical-accordion__chevron-icon"); + replaceIconNode(accordionSummary, ":scope > .vertical-accordion__chevron", shell); + } + + function wireVerticalAccordionChevron(details) { + if (details.dataset.themeV2ChevronWired === "true") { + updateVerticalAccordionChevron(details); + return; + } + + details.dataset.themeV2ChevronWired = "true"; + details.addEventListener("toggle", function () { + updateVerticalAccordionChevron(details); + }); + updateVerticalAccordionChevron(details); + } + + function refreshVerticalAccordionChevrons() { + document.querySelectorAll("details.vertical-accordion").forEach(wireVerticalAccordionChevron); + } + + function updateToolDisplayModeChevron() { + const iconName = displayMode.open ? "chevron-up" : "chevron-down"; + const shell = createChevronShell(iconName, "tool-display-mode__chevron", "tool-display-mode__chevron-icon"); + replaceIconNode(summary, ":scope > .tool-display-mode__chevron", shell); + } + + function horizontalToggleIconName(button) { + const expanded = button.getAttribute("aria-expanded") !== "false"; + const isLeft = button.classList.contains("horizontal-accordion-toggle--left"); + if (isLeft) { + return expanded ? "chevron-left" : "chevron-right"; + } + return expanded ? "chevron-right" : "chevron-left"; + } + + function updateHorizontalToggleIcon(button) { + button.replaceChildren(createThemeIconNode(horizontalToggleIconName(button), "horizontal-accordion-toggle__icon")); + } + + function refreshHorizontalToggleIcons() { + document.querySelectorAll(".horizontal-accordion-toggle").forEach(updateHorizontalToggleIcon); + } + + function refreshThemeIcons() { + refreshVerticalAccordionChevrons(); + updateToolDisplayModeChevron(); + refreshHorizontalToggleIcons(); + } + + import("/assets/theme-v2/js/theme-icons.js").then(function (module) { + themeIconRegistry = module; + refreshThemeIcons(); + }).catch(function () { + themeIconRegistry = window.ThemeV2Icons || themeIconRegistry; + }); function explicitPngName(source) { if (!source) return ""; @@ -53,6 +149,7 @@ fullscreenName.textContent = toolName; summary.appendChild(fullscreenName); displayMode.appendChild(summary); + displayMode.addEventListener("toggle", updateToolDisplayModeChevron); const body = document.createElement("div"); body.className = "tool-display-mode__body"; @@ -202,13 +299,16 @@ } }); + refreshVerticalAccordionChevrons(); + updateToolDisplayModeChevron(); + document.querySelectorAll(".tool-workspace").forEach(function (workspace) { const columns = workspace.querySelectorAll(":scope > .tool-column"); if (columns.length < 2) return; const sideColumns = [ - { column: columns[0], side: "left", openIndicator: "<", closedIndicator: ">" }, - { column: columns[columns.length - 1], side: "right", openIndicator: ">", closedIndicator: "<" } + { column: columns[0], side: "left" }, + { column: columns[columns.length - 1], side: "right" } ]; sideColumns.forEach(function (entry) { @@ -222,15 +322,15 @@ button.className = "horizontal-accordion-toggle horizontal-accordion-toggle--" + entry.side; button.setAttribute("aria-label", "Collapse " + label); button.setAttribute("aria-expanded", "true"); - button.textContent = entry.openIndicator; + updateHorizontalToggleIcon(button); header.insertBefore(button, header.firstChild); button.addEventListener("click", function () { const collapsed = entry.column.classList.toggle("is-collapsed"); workspace.classList.toggle("is-" + entry.side + "-collapsed", collapsed); - button.textContent = collapsed ? entry.closedIndicator : entry.openIndicator; button.setAttribute("aria-expanded", collapsed ? "false" : "true"); button.setAttribute("aria-label", (collapsed ? "Expand " : "Collapse ") + label); + updateHorizontalToggleIcon(button); }); }); }); diff --git a/assets/toolbox/idea-board/js/index.js b/assets/toolbox/idea-board/js/index.js index 0c10d2dc1..5b33ae05d 100644 --- a/assets/toolbox/idea-board/js/index.js +++ b/assets/toolbox/idea-board/js/index.js @@ -1,5 +1,6 @@ import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js"; import { getSessionCurrent } from "../../../../src/api/session-api-client.js"; +import { createThemeIcon, themeIconFileName } from "../../../theme-v2/js/theme-icons.js"; const editableStatusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready"]); const filterStatusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]); @@ -267,10 +268,9 @@ function renderIdeaRow(tbody, record) { const ideaText = document.createElement("span"); ideaText.className = "idea-board-idea-label__text"; ideaText.textContent = record.idea; - const chevron = document.createElement("span"); - const chevronIcon = expanded ? "gfs-chevron-up.svg" : "gfs-chevron-down.svg"; - chevron.className = `idea-board-idea-chevron idea-board-idea-chevron--${expanded ? "up" : "down"}`; - chevron.setAttribute("aria-hidden", "true"); + const chevronName = expanded ? "chevron-up" : "chevron-down"; + const chevron = createThemeIcon(chevronName, { className: "idea-board-idea-chevron" }); + const chevronIcon = themeIconFileName(chevronName); chevron.dataset.ideaBoardChevron = record.ideaId; chevron.dataset.ideaBoardChevronIcon = chevronIcon; ideaLabel.append(chevron, ideaText); diff --git a/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_manual-validation-notes.md b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_manual-validation-notes.md new file mode 100644 index 000000000..72032dd16 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_manual-validation-notes.md @@ -0,0 +1,6 @@ +# PR_26175_ALFA_048-theme-v2-chevron-conversion Manual Validation Notes + +- Confirmed scoped CSS scan returns no old chevron image masks, clip-path triangles, or currentColor gradient chevrons in accordion.css, panels.css, and tables.css. +- Confirmed Theme V2 SVG registry helper creates CSS-backed span icons with no inline SVG. +- Confirmed required chevron assertions in the Idea Board test progressed before the later Game Hub expanded-row failure. +- Did not merge ALFA_048 because branch validation is FAIL. diff --git a/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_report.md b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_report.md new file mode 100644 index 000000000..53ef7e2cd --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_report.md @@ -0,0 +1,15 @@ +# PR_26175_ALFA_048-theme-v2-chevron-conversion Report + +## Summary +- Added a shared Theme V2 icon module and CSS registry surface for SVG-backed icons. +- Converted vertical accordion, tool display mode, horizontal column toggle, and Idea Board row chevrons away from CSS-drawn gradients/clip-paths and old image masks. +- Kept chevron controls decorative with existing accessible names and aria-expanded behavior intact. + +## Branch Validation +FAIL + +Required static checks and two Playwright lanes passed, but the required Idea Board Playwright lane still fails on an existing Game Hub expanded-row expectation unrelated to the chevron conversion. No merge was performed. + +## Notes +- A local ignored .env from the original checkout was copied into the clean ALFA clone only to run database-dependent Playwright lanes. The file is ignored and is not included in the delta package. +- The blocking Idea Board assertion is at tests/playwright/tools/IdeaBoardTableNotes.spec.mjs:419: it expects 3 [data-game-expanded-row] rows after opening a Game Hub project; current Game Hub renders 2 child rows: source-idea and readiness-output. diff --git a/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_requirements-checklist.md b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_requirements-checklist.md new file mode 100644 index 000000000..b198d0700 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_requirements-checklist.md @@ -0,0 +1,11 @@ +# PR_26175_ALFA_048-theme-v2-chevron-conversion Requirements Checklist + +- PASS: Add shared Theme V2 SVG icon registry helper for chevron consumers. +- PASS: Replace Theme V2 chevron CSS drawings in accordion.css and panels.css. +- PASS: Replace Idea Board chevron mask-image classes in tables.css with shared icon classes. +- PASS: Preserve click/keyboard toggle behavior and accessible names. +- PASS: Keep chevron dimensions stable. +- PASS: No inline SVG blocks added to page HTML. +- PASS: No inline styles, style blocks, or page-local CSS added. +- PASS: No old gfs-chevron up/down mask-image usage remains in scoped active component CSS. +- FAIL: Full required validation lane is blocked by the pre-existing Game Hub row-count assertion in IdeaBoardTableNotes.spec.mjs. diff --git a/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_validation-lane.md b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_validation-lane.md new file mode 100644 index 000000000..8f4ff3051 --- /dev/null +++ b/docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_validation-lane.md @@ -0,0 +1,19 @@ +# PR_26175_ALFA_048-theme-v2-chevron-conversion Validation Lane + +## Static Checks +- PASS: node --check assets/theme-v2/js/theme-icons.js +- PASS: node --check assets/theme-v2/js/tool-display-mode.js +- PASS: node --check assets/toolbox/idea-board/js/index.js + +## Playwright +- PASS: npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1 (7 passed) +- FAIL: npx playwright test tests/playwright/tools/IdeaBoardTableNotes.spec.mjs --workers=1 (3 passed, 1 failed after local .env was provided) + - Failure: tests/playwright/tools/IdeaBoardTableNotes.spec.mjs:419 expected 3 [data-game-expanded-row] rows, received 2. + - Observed rows: source-idea and readiness-output. +- PASS: npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1 (7 passed after clearing stale Playwright artifact output from a prior timeout) + +## Pattern Scan +- PASS: rg -n "gfs-chevron-(up|down)\.svg|clip-path: polygon|linear-gradient\(45deg.*currentColor|linear-gradient\(135deg.*currentColor" assets/theme-v2/css/accordion.css assets/theme-v2/css/panels.css assets/theme-v2/css/tables.css + +## Branch Validation +FAIL: Required Idea Board lane is not green. diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt index a4b5f90f6..39b6818f7 100644 --- a/docs_build/dev/reports/codex_changed_files.txt +++ b/docs_build/dev/reports/codex_changed_files.txt @@ -1,38 +1,15 @@ -# PR_26175_ALFA_047-theme-v2-svg-icon-registry Changed Files - -## Source, Test, Documentation, And Report Files - -- `docs_build/dev/BUILD_PR.md` - updated -- `assets/theme-v2/svg/README.md` - added -- `assets/theme-v2/svg/gfs-add.svg` - added -- `assets/theme-v2/svg/gfs-chevron-down.svg` - added -- `assets/theme-v2/svg/gfs-chevron-left.svg` - added -- `assets/theme-v2/svg/gfs-chevron-right.svg` - added -- `assets/theme-v2/svg/gfs-chevron-up.svg` - added -- `assets/theme-v2/svg/gfs-close.svg` - added -- `assets/theme-v2/svg/gfs-error.svg` - added -- `assets/theme-v2/svg/gfs-exit-fullscreen.svg` - added -- `assets/theme-v2/svg/gfs-fullscreen.svg` - added -- `assets/theme-v2/svg/gfs-info.svg` - added -- `assets/theme-v2/svg/gfs-menu.svg` - added -- `assets/theme-v2/svg/gfs-search.svg` - added -- `assets/theme-v2/svg/gfs-settings.svg` - added -- `assets/theme-v2/svg/gfs-subtract.svg` - added -- `assets/theme-v2/svg/gfs-success.svg` - added -- `assets/theme-v2/svg/gfs-trash.svg` - added -- `assets/theme-v2/svg/gfs-warning.svg` - added -- `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md` - added -- `tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` - added -- `docs_build/pr/APPLY_PR_26175_ALFA_047-theme-v2-svg-icon-registry.md` - added -- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_report.md` - added -- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_validation-lane.md` - added -- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_requirements-checklist.md` - added -- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md` - added -- `docs_build/dev/reports/codex_changed_files.txt` - updated -- `docs_build/dev/reports/codex_review.diff` - updated - -## Validation - -- PASS: `npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1` ran 5 tests successfully. -- PASS: `rg -n "<[s]tyle|[s]tyle=" docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` returned no matches. -- PASS: Repo-structured ZIP refreshed at `tmp/PR_26175_ALFA_047-theme-v2-svg-icon-registry_delta.zip`. +assets/theme-v2/css/accordion.css +assets/theme-v2/css/icons.css +assets/theme-v2/css/panels.css +assets/theme-v2/css/tables.css +assets/theme-v2/css/theme.css +assets/theme-v2/js/theme-icons.js +assets/theme-v2/js/tool-display-mode.js +assets/toolbox/idea-board/js/index.js +tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs +docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_manual-validation-notes.md +docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_report.md +docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_requirements-checklist.md +docs_build/dev/reports/PR_26175_ALFA_048-theme-v2-chevron-conversion_validation-lane.md +docs_build/dev/reports/codex_changed_files.txt +docs_build/dev/reports/codex_review.diff diff --git a/docs_build/dev/reports/codex_review.diff b/docs_build/dev/reports/codex_review.diff index 14fe543ad..78bb926b5 100644 --- a/docs_build/dev/reports/codex_review.diff +++ b/docs_build/dev/reports/codex_review.diff @@ -1,772 +1,687 @@ -diff --git a/docs_build/dev/BUILD_PR.md b/docs_build/dev/BUILD_PR.md -@@ -1,74 +1,109 @@ --# PR_26175_ALFA_011-status-bar-journey-progress-context -+# PR_26175_ALFA_047-theme-v2-svg-icon-registry - - ## Purpose --Add right-anchored progress context to the shared toolbox status bar using the existing Game Journey completion metrics/API pipeline. -+Create a shared Theme V2 SVG icon asset registry and authoritative validation specification so toolbox and platform UI can use approved standalone SVG files from one repo-owned source instead of page-local SVG, ad hoc CSS drawings, Font Awesome glyphs, conversation screenshots, vague row references, CSS-only generation, or a JS-only registry. - - ## Source Of Truth --This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_011-status-bar-journey-progress-context`. -+This `BUILD_PR.md` is the source of truth for `PR_26175_ALFA_047-theme-v2-svg-icon-registry`. - - ## Exact Scope --- Preserve the ALFA_009 single-row toolbox status bar behavior: -- - left side displays only the selected Game Hub game name. -- - center displays only the current status message. --- Add right-anchored progress text in this format: -- - `{CurrentTool} {complete}/{total} ({percent}%) | Journey {complete}/{total} ({percent}%)` --- Use existing Game Journey completion metrics/API pipeline for Journey totals. --- Derive current-tool progress from the existing completion metrics record that matches the current toolbox tool/section. --- Do not add new storage. --- Do not use browser-owned authoritative progress data. --- Preserve fullscreen bottom anchoring and existing fullscreen content bottom reserve. --- Preserve normal placement above the footer. --- Use shared Theme V2 CSS/classes only. --- Update targeted Playwright coverage for the right-anchored progress text and existing left/center behavior. -+- Remove the incorrect JS-only icon registry implementation from the ALFA_047 delta. -+- Use the user-authored SVG files already present under `assets/theme-v2/svg/` as the authoritative source. -+- Do not regenerate, redesign, simplify, optimize, or redraw any SVG icon artwork in this PR. -+- Required SVG files: -+ - `gfs-chevron-left.svg` -+ - `gfs-chevron-right.svg` -+ - `gfs-chevron-up.svg` -+ - `gfs-chevron-down.svg` -+ - `gfs-add.svg` -+ - `gfs-subtract.svg` -+ - `gfs-trash.svg` -+ - `gfs-close.svg` -+ - `gfs-warning.svg` -+ - `gfs-error.svg` -+ - `gfs-success.svg` -+ - `gfs-info.svg` -+ - `gfs-fullscreen.svg` -+ - `gfs-exit-fullscreen.svg` -+ - `gfs-menu.svg` -+ - `gfs-search.svg` -+ - `gfs-settings.svg` -+- Validate each required SVG is well-formed XML. -+- Validate each SVG uses `viewBox="0 0 24 24"`, `fill="none"`, `stroke="currentColor"`, `stroke-linecap="round"`, and `stroke-linejoin="round"`. -+- Do not create `expand` or `collapse` icon naming. -+- Do not create `delete` icon naming. -+- Do not replace the standalone SVG assets with a JS-only icon registry. -+- Do not replace the standalone SVG assets with CSS-only icon generation. -+- Create `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md` as the authoritative specification for all future Theme V2 SVG icons. -+- Create or update `assets/theme-v2/svg/README.md` as the registry documentation for the authoritative SVG asset pack. -+- Document the approved validation rules and the no-regeneration/no-redesign policy. -+- If any required SVG is missing, report validation failure instead of generating a replacement. -+- Do not convert existing UI controls in this PR. - - ## Exact Targets - - `docs_build/dev/BUILD_PR.md` --- `assets/theme-v2/js/toolbox-status-bar.js` --- `assets/theme-v2/css/status.css` --- `tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs` --- `docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md` --- `docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md` --- `docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md` -+- `assets/theme-v2/svg/gfs-chevron-left.svg` -+- `assets/theme-v2/svg/gfs-chevron-right.svg` -+- `assets/theme-v2/svg/gfs-chevron-up.svg` -+- `assets/theme-v2/svg/gfs-chevron-down.svg` -+- `assets/theme-v2/svg/gfs-add.svg` -+- `assets/theme-v2/svg/gfs-subtract.svg` -+- `assets/theme-v2/svg/gfs-trash.svg` -+- `assets/theme-v2/svg/gfs-close.svg` -+- `assets/theme-v2/svg/gfs-warning.svg` -+- `assets/theme-v2/svg/gfs-error.svg` -+- `assets/theme-v2/svg/gfs-success.svg` -+- `assets/theme-v2/svg/gfs-info.svg` -+- `assets/theme-v2/svg/gfs-fullscreen.svg` -+- `assets/theme-v2/svg/gfs-exit-fullscreen.svg` -+- `assets/theme-v2/svg/gfs-menu.svg` -+- `assets/theme-v2/svg/gfs-search.svg` -+- `assets/theme-v2/svg/gfs-settings.svg` -+- `assets/theme-v2/svg/README.md` -+- `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md` -+- `tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_report.md` -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_validation-lane.md` -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_requirements-checklist.md` -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md` - - `docs_build/dev/reports/codex_review.diff` - - `docs_build/dev/reports/codex_changed_files.txt` - - ## Evidence Sources --- `assets/js/shared/game-journey-api-client.js` --- `src/api/game-journey-completion-api-client.js` --- `src/dev-runtime/server/local-api-router.mjs` --- `src/dev-runtime/persistence/game-journey-completion-metrics-store.mjs` -+- `docs_build/pr/PLAN_PR_26175_ALFA_047-theme-v2-svg-icon-registry.md` -+- `assets/theme-v2/images/gfs-chevron-down.svg` -+- `assets/theme-v2/images/gfs-chevron-up.svg` - - ## Out Of Scope --- No Game Journey API/service/repository contract changes. --- No new persistence/storage. -+- No chevron conversion. -+- No status/action icon conversion. -+- No layout utility icon conversion. -+- No JS-only icon registry. -+- No CSS-only icon generation. -+- No Theme V2 CSS changes. -+- No runtime UI conversion. -+- No accordion conversion. -+- No Font Awesome removal. -+- No broad visual redesign. -+- No page-local CSS. -+- No inline styles. -+- No style blocks. - - No browser-owned product data as source of truth. --- No silent fallback data. --- No environment/server details in the status bar. --- No selected game purpose in the visible status bar. --- No visible status category labels in the status bar. --- No large banners. --- No modal messages or modal-style status messages. --- No row highlights. --- No inline styles, style blocks, or page-local CSS. -+- No API/service/repository contract changes. - - No engine core changes. - - No `start_of_day` folder changes. - - ## Validation --Run targeted Playwright coverage: -+Run exactly: - - ```powershell --npx playwright test tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --workers=1 --``` +diff --git a/assets/theme-v2/css/accordion.css b/assets/theme-v2/css/accordion.css +index d4bf86f9a..b650f518e 100644 +--- a/assets/theme-v2/css/accordion.css ++++ b/assets/theme-v2/css/accordion.css +@@ -50,7 +50,7 @@ details.vertical-accordion summary::-webkit-details-marker { + display: none + } + +-details.vertical-accordion summary:after { ++.horizontal-accordion-toggle { + --accordion-button-border: var(--line); + --accordion-button-background: var(--panel-soft); + --accordion-button-color: var(--gold); +@@ -66,74 +66,53 @@ details.vertical-accordion summary:after { + line-height: var(--line-height-single); + display: inline-grid; + place-items: center; +- flex: 0 0 auto ++ flex: 0 0 auto; ++ margin-left: auto; ++ font-size: var(--font-size-sm); ++ font-weight: var(--font-weight-heavy); ++ overflow: hidden; ++ position: relative + } + +-.horizontal-accordion-toggle { ++.horizontal-accordion-toggle__icon, ++.vertical-accordion__chevron, ++.tool-display-mode__chevron { + --accordion-button-border: var(--line); + --accordion-button-background: var(--panel-soft); + --accordion-button-color: var(--gold); +- min-width: var(--space-20); +- width: var(--space-20); +- height: var(--space-20); +- padding: var(--space-0); ++ align-items: center; ++ background: var(--accordion-button-background); + border: var(--border-standard); + border-color: var(--accordion-button-border); + border-radius: var(--radius-pill); +- background: var(--accordion-button-background); + color: var(--accordion-button-color); +- line-height: var(--line-height-single); +- display: inline-grid; +- place-items: center; ++ display: inline-flex; + flex: 0 0 auto; +- margin-left: auto; +- font-size: var(--space-0); +- overflow: hidden; +- position: relative ++ height: var(--space-20); ++ justify-content: center; ++ line-height: var(--line-height-single); ++ min-width: var(--space-20); ++ padding: var(--space-0); ++ width: var(--space-20) + } + +-.horizontal-accordion-toggle:before { +- content: ""; +- position: absolute; +- top: 50%; +- left: 50%; +- width: var(--space-14); +- height: var(--space-14); ++.horizontal-accordion-toggle__icon { + background: currentColor; +- clip-path: polygon(70% 50%, 30% 20%, 30% 80%); +- transform-origin: center +-} - --Also verify changed source does not introduce inline styles or style blocks: +-.horizontal-accordion-toggle[aria-expanded="true"]:before { +- transform: translate(-50%, -50%) +-} - --```powershell --rg -n "<[s]tyle|[s]tyle=" assets/theme-v2/js/toolbox-status-bar.js assets/theme-v2/css/status.css tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs -+npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1 -+rg -n "<[s]tyle|[s]tyle=" docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs - ``` - - ## Artifact - Create repo-structured delta ZIP: - - ```text --tmp/PR_26175_ALFA_011-status-bar-journey-progress-context_delta.zip -+tmp/PR_26175_ALFA_047-theme-v2-svg-icon-registry_delta.zip - ``` -diff --git a/assets/theme-v2/svg/README.md b/assets/theme-v2/svg/README.md -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,35 @@ -+# GFS Icon Pack v1 Core -+ -+The SVG files in `assets/theme-v2/svg/` are the authoritative Theme V2 icon source. -+ -+These files are user-authored and approved. Do not regenerate, redesign, simplify, optimize, or redraw these SVG files during validation-only PRs. -+ -+If a required SVG is missing, report validation failure instead of generating a replacement. -+ -+Required validation: -+- each required file exists under `assets/theme-v2/svg/` -+- each SVG is well-formed XML -+- each SVG uses `viewBox="0 0 24 24"` -+- each SVG uses `fill="none"` -+- each SVG uses `stroke="currentColor"` -+- each SVG uses `stroke-linecap="round"` -+- each SVG uses `stroke-linejoin="round"` -+ -+Required filenames: -+- `gfs-chevron-left.svg` -+- `gfs-chevron-right.svg` -+- `gfs-chevron-up.svg` -+- `gfs-chevron-down.svg` -+- `gfs-add.svg` -+- `gfs-subtract.svg` -+- `gfs-trash.svg` -+- `gfs-close.svg` -+- `gfs-warning.svg` -+- `gfs-error.svg` -+- `gfs-success.svg` -+- `gfs-info.svg` -+- `gfs-fullscreen.svg` -+- `gfs-exit-fullscreen.svg` -+- `gfs-menu.svg` -+- `gfs-search.svg` -+- `gfs-settings.svg` -diff --git a/assets/theme-v2/svg/gfs-add.svg b/assets/theme-v2/svg/gfs-add.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,3 @@ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-chevron-down.svg b/assets/theme-v2/svg/gfs-chevron-down.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,4 @@ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-chevron-left.svg b/assets/theme-v2/svg/gfs-chevron-left.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,4 @@ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-chevron-right.svg b/assets/theme-v2/svg/gfs-chevron-right.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,4 @@ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-chevron-up.svg b/assets/theme-v2/svg/gfs-chevron-up.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,4 @@ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-close.svg b/assets/theme-v2/svg/gfs-close.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,5 @@ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-error.svg b/assets/theme-v2/svg/gfs-error.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,6 @@ -+ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-exit-fullscreen.svg b/assets/theme-v2/svg/gfs-exit-fullscreen.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,10 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-fullscreen.svg b/assets/theme-v2/svg/gfs-fullscreen.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,10 @@ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-info.svg b/assets/theme-v2/svg/gfs-info.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,6 @@ -+ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-menu.svg b/assets/theme-v2/svg/gfs-menu.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,5 @@ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-search.svg b/assets/theme-v2/svg/gfs-search.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,6 @@ -+ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-settings.svg b/assets/theme-v2/svg/gfs-settings.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,4 @@ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-subtract.svg b/assets/theme-v2/svg/gfs-subtract.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,3 @@ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-success.svg b/assets/theme-v2/svg/gfs-success.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,5 @@ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-trash.svg b/assets/theme-v2/svg/gfs-trash.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,8 @@ -+ -+ -+ -+ -+ -+ -+ -+ -diff --git a/assets/theme-v2/svg/gfs-warning.svg b/assets/theme-v2/svg/gfs-warning.svg -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,6 @@ -+ -+ -+ -+ -+ -+ -diff --git a/docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md b/docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md +-.horizontal-accordion-toggle[aria-expanded="false"]:before { +- transform: translate(-50%, -50%) rotate(180deg) +-} +- +-.horizontal-accordion-toggle--left[aria-expanded="true"]:before { +- transform: translate(-50%, -50%) rotate(180deg) +-} +- +-.horizontal-accordion-toggle--left[aria-expanded="false"]:before { +- transform: translate(-50%, -50%) +-} +- +-details.vertical-accordion summary:after { +- content: ""; +- background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); +- background-position: calc(50% - var(--space-3)) 50%, calc(50% + var(--space-3)) 50%; +- background-size: var(--space-7) var(--space-7); +- background-repeat: no-repeat ++ border: 0; ++ border-radius: var(--space-0); ++ height: var(--space-14); ++ min-width: var(--space-14); ++ width: var(--space-14) + } + +-details.vertical-accordion[open] summary:after { +- background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) ++.vertical-accordion__chevron { ++ margin-left: auto + } + +-details.vertical-accordion summary:active:after { +- background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) ++.vertical-accordion__chevron .theme-icon, ++.tool-display-mode__chevron .theme-icon { ++ height: var(--space-14); ++ width: var(--space-14) + } + + details.vertical-accordion summary .status { +@@ -150,7 +129,7 @@ details.vertical-accordion summary.accordion-summary--control-grid { + align-items: center + } + +-details.vertical-accordion summary.accordion-summary--control-grid:after { ++details.vertical-accordion summary.accordion-summary--control-grid .vertical-accordion__chevron { + justify-self: end + } + +@@ -188,10 +167,6 @@ details.vertical-accordion summary.accordion-summary--control-grid .accordion-su + min-width: var(--space-34) + } + +-details.vertical-accordion[open] summary:active:after { +- background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%) +-} +- + .accordion-body { + padding: var(--space-0) var(--space-14) var(--space-14); + color: var(--muted); +diff --git a/assets/theme-v2/css/icons.css b/assets/theme-v2/css/icons.css new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,55 @@ -+# Theme V2 Icon Style Guide -+ -+## Purpose -+Provide the approved validation and usage language for shared Theme V2 SVG icons. +index 000000000..ce1af7c8c +--- /dev/null ++++ b/assets/theme-v2/css/icons.css +@@ -0,0 +1,88 @@ ++.theme-icon { ++ --theme-v2-icon-url: none; ++ background: currentColor; ++ color: inherit; ++ display: inline-block; ++ flex: 0 0 auto; ++ height: 1em; ++ line-height: var(--line-height-single); ++ pointer-events: none; ++ vertical-align: -0.125em; ++ width: 1em; ++ -webkit-mask-image: var(--theme-v2-icon-url); ++ mask-image: var(--theme-v2-icon-url); ++ -webkit-mask-position: center; ++ mask-position: center; ++ -webkit-mask-repeat: no-repeat; ++ mask-repeat: no-repeat; ++ -webkit-mask-size: contain; ++ mask-size: contain ++} + -+The SVG files in `assets/theme-v2/svg/` are the authoritative Theme V2 icon source. ++.theme-icon--add { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-add.svg") ++} + -+## Artwork Authority -+The SVG artwork is user-authored and approved. ++.theme-icon--chevron-down { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-down.svg") ++} + -+Do not regenerate, redesign, simplify, optimize, or redraw these SVG files during validation-only PRs. ++.theme-icon--chevron-left { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-left.svg") ++} + -+If a required SVG is missing, report validation failure instead of generating a replacement. ++.theme-icon--chevron-right { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-right.svg") ++} + -+Do not replace the SVG files with CSS-only or JS-only icon generation. ++.theme-icon--chevron-up { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-chevron-up.svg") ++} + -+## SVG Validation Standard -+Every required SVG must be a standalone file under `assets/theme-v2/svg/`. ++.theme-icon--close { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-close.svg") ++} + -+Every required SVG must be well-formed XML. ++.theme-icon--error { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-error.svg") ++} + -+Every required SVG must use: -+- `viewBox="0 0 24 24"` -+- `fill="none"` -+- `stroke="currentColor"` -+- `stroke-linecap="round"` -+- `stroke-linejoin="round"` ++.theme-icon--exit-fullscreen { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-exit-fullscreen.svg") ++} + -+The approved files may include additional SVG attributes or path geometry as authored. Validation should not inspect, simplify, optimize, or rewrite artwork geometry. ++.theme-icon--fullscreen { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-fullscreen.svg") ++} + -+## Required Icon Files -+- `gfs-chevron-left.svg` -+- `gfs-chevron-right.svg` -+- `gfs-chevron-up.svg` -+- `gfs-chevron-down.svg` -+- `gfs-add.svg` -+- `gfs-subtract.svg` -+- `gfs-trash.svg` -+- `gfs-close.svg` -+- `gfs-warning.svg` -+- `gfs-error.svg` -+- `gfs-success.svg` -+- `gfs-info.svg` -+- `gfs-fullscreen.svg` -+- `gfs-exit-fullscreen.svg` -+- `gfs-menu.svg` -+- `gfs-search.svg` -+- `gfs-settings.svg` ++.theme-icon--info { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-info.svg") ++} + -+## Naming Rules -+Use `trash` naming instead of `delete`. ++.theme-icon--menu { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-menu.svg") ++} + -+Use `fullscreen` and `exit-fullscreen` naming. ++.theme-icon--search { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-search.svg") ++} + -+Do not add `expand`, `collapse`, or `delete` SVG names in this registry. -diff --git a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,138 @@ -+import { expect, test } from "@playwright/test"; -+import fs from "node:fs/promises"; -+import path from "node:path"; -+import { fileURLToPath } from "node:url"; -+import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; -+ -+const __filename = fileURLToPath(import.meta.url); -+const __dirname = path.dirname(__filename); -+const repoRoot = path.resolve(__dirname, "..", "..", ".."); -+const svgRoot = path.join(repoRoot, "assets", "theme-v2", "svg"); -+const readmePath = path.join(svgRoot, "README.md"); -+const styleGuidePath = path.join(repoRoot, "docs_build", "design", "theme-v2-icons", "theme-v2-icon-style-guide.md"); -+ -+const REQUIRED_SVG_FILES = [ -+ "gfs-add.svg", -+ "gfs-chevron-down.svg", -+ "gfs-chevron-left.svg", -+ "gfs-chevron-right.svg", -+ "gfs-chevron-up.svg", -+ "gfs-close.svg", -+ "gfs-error.svg", -+ "gfs-exit-fullscreen.svg", -+ "gfs-fullscreen.svg", -+ "gfs-info.svg", -+ "gfs-menu.svg", -+ "gfs-search.svg", -+ "gfs-settings.svg", -+ "gfs-subtract.svg", -+ "gfs-success.svg", -+ "gfs-trash.svg", -+ "gfs-warning.svg", -+]; -+ -+const FORBIDDEN_SVG_FILES = [ -+ "gfs-collapse.svg", -+ "gfs-delete.svg", -+ "gfs-expand.svg", -+]; -+ -+function attributeValues(content, attributeName) { -+ return [...content.matchAll(new RegExp(`\\s${attributeName}="([^"]+)"`, "g"))].map((match) => match[1]); ++.theme-icon--settings { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-settings.svg") +} + -+async function fileExists(filePath) { -+ try { -+ await fs.access(filePath); -+ return true; -+ } catch { -+ return false; -+ } ++.theme-icon--subtract { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-subtract.svg") +} + -+async function readSvg(fileName) { -+ return fs.readFile(path.join(svgRoot, fileName), "utf8"); ++.theme-icon--success { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-success.svg") +} + -+test("provides the required standalone Theme V2 SVG files", async () => { -+ const actualFiles = (await fs.readdir(svgRoot)).filter((name) => name.endsWith(".svg")).sort(); -+ expect(actualFiles).toEqual(REQUIRED_SVG_FILES); ++.theme-icon--trash { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-trash.svg") ++} + -+ for (const fileName of FORBIDDEN_SVG_FILES) { -+ await expect(fileExists(path.join(svgRoot, fileName))).resolves.toBe(false); -+ } ++.theme-icon--warning { ++ --theme-v2-icon-url: url("/assets/theme-v2/svg/gfs-warning.svg") ++} +diff --git a/assets/theme-v2/css/panels.css b/assets/theme-v2/css/panels.css +index bcfa2785f..b80582336 100644 +--- a/assets/theme-v2/css/panels.css ++++ b/assets/theme-v2/css/panels.css +@@ -206,7 +206,7 @@ + } + + .tool-column[class*="tool-group-"] .horizontal-accordion-toggle, +-.tool-column[class*="tool-group-"] details.vertical-accordion summary:after { ++.tool-column[class*="tool-group-"] details.vertical-accordion .vertical-accordion__chevron { + --accordion-button-border: var(--tool-group-accent); + --accordion-button-color: var(--tool-group-color) + } +@@ -298,36 +298,12 @@ body.tool-focus-mode .tool-center-panel:has(>details.vertical-accordion)>p { + display: none + } + +-.tool-display-mode summary:after { +- content: ""; ++.tool-display-mode__chevron { + position: absolute; + right: var(--space-12); + top: 50%; + transform: translateY(-50%); +- width: var(--space-20); +- height: var(--space-20); +- border: var(--border-standard); +- border-radius: var(--radius-pill); +- background: var(--panel-soft); +- color: var(--gold); +- display: inline-grid; +- place-items: center; +- background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%); +- background-position: calc(50% - var(--space-3)) 50%, calc(50% + var(--space-3)) 50%; +- background-size: var(--space-7) var(--space-7); +- background-repeat: no-repeat +-} +- +-.tool-display-mode[open] summary:after { +- background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) +-} +- +-.tool-display-mode summary:active:after { +- background-image: linear-gradient(135deg, transparent 50%, currentColor 50%), linear-gradient(45deg, currentColor 50%, transparent 50%) +-} +- +-.tool-display-mode[open] summary:active:after { +- background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%) ++ z-index: var(--z-index-sm) + } + + .tool-display-mode__badge { +diff --git a/assets/theme-v2/css/tables.css b/assets/theme-v2/css/tables.css +index 5575ff684..915a235e9 100644 +--- a/assets/theme-v2/css/tables.css ++++ b/assets/theme-v2/css/tables.css +@@ -131,27 +131,10 @@ td { + + .idea-board-idea-chevron { + display: inline-block; +- width: 1em; + height: 1em; + margin-right: .35em; +- background: currentColor; + vertical-align: -0.125em; +- -webkit-mask-position: center; +- mask-position: center; +- -webkit-mask-repeat: no-repeat; +- mask-repeat: no-repeat; +- -webkit-mask-size: contain; +- mask-size: contain +-} +- +-.idea-board-idea-chevron--down { +- -webkit-mask-image: url("../images/gfs-chevron-down.svg"); +- mask-image: url("../images/gfs-chevron-down.svg") +-} +- +-.idea-board-idea-chevron--up { +- -webkit-mask-image: url("../images/gfs-chevron-up.svg"); +- mask-image: url("../images/gfs-chevron-up.svg") ++ width: 1em + } + + .idea-board-notes-child-surface { +diff --git a/assets/theme-v2/css/theme.css b/assets/theme-v2/css/theme.css +index 944bfa356..fb8af1256 100644 +--- a/assets/theme-v2/css/theme.css ++++ b/assets/theme-v2/css/theme.css +@@ -4,6 +4,7 @@ + @import url("typography.css"); + @import url("layout.css"); + @import url("buttons.css"); ++@import url("icons.css"); + @import url("forms.css"); + @import url("controls.css"); + @import url("panels.css"); +diff --git a/assets/theme-v2/js/theme-icons.js b/assets/theme-v2/js/theme-icons.js +new file mode 100644 +index 000000000..442614fc1 +--- /dev/null ++++ b/assets/theme-v2/js/theme-icons.js +@@ -0,0 +1,73 @@ ++const themeV2IconRegistry = Object.freeze({ ++ add: "gfs-add.svg", ++ "chevron-down": "gfs-chevron-down.svg", ++ "chevron-left": "gfs-chevron-left.svg", ++ "chevron-right": "gfs-chevron-right.svg", ++ "chevron-up": "gfs-chevron-up.svg", ++ close: "gfs-close.svg", ++ error: "gfs-error.svg", ++ "exit-fullscreen": "gfs-exit-fullscreen.svg", ++ fullscreen: "gfs-fullscreen.svg", ++ info: "gfs-info.svg", ++ menu: "gfs-menu.svg", ++ search: "gfs-search.svg", ++ settings: "gfs-settings.svg", ++ subtract: "gfs-subtract.svg", ++ success: "gfs-success.svg", ++ trash: "gfs-trash.svg", ++ warning: "gfs-warning.svg", +}); + -+test("validates every SVG as well-formed XML", async ({ page }) => { -+ for (const fileName of REQUIRED_SVG_FILES) { -+ const content = await readSvg(fileName); -+ const result = await page.evaluate((svgText) => { -+ const document = new DOMParser().parseFromString(svgText, "image/svg+xml"); -+ const parserError = document.querySelector("parsererror"); -+ const root = document.documentElement; -+ return { -+ error: parserError?.textContent?.replace(/\s+/g, " ").trim() || "", -+ rootName: root?.tagName || "", -+ }; -+ }, content); -+ expect(result.error, fileName).toBe(""); -+ expect(result.rootName.toLowerCase(), fileName).toBe("svg"); ++function themeIconFileName(name) { ++ const fileName = themeV2IconRegistry[name]; ++ if (!fileName) { ++ throw new RangeError(`Unknown Theme V2 icon: ${name}`); + } -+}); ++ return fileName; ++} + -+test("validates required shared SVG attributes without inspecting artwork geometry", async () => { -+ for (const fileName of REQUIRED_SVG_FILES) { -+ const content = await readSvg(fileName); -+ const fillValues = attributeValues(content, "fill"); -+ const strokeValues = attributeValues(content, "stroke"); -+ const linecapValues = attributeValues(content, "stroke-linecap"); -+ const linejoinValues = attributeValues(content, "stroke-linejoin"); -+ -+ expect(content, fileName).toContain(" value === "none"), fileName).toBe(true); -+ expect(strokeValues.every((value) => value === "currentColor"), fileName).toBe(true); -+ expect(linecapValues.every((value) => value === "round"), fileName).toBe(true); -+ expect(linejoinValues.every((value) => value === "round"), fileName).toBe(true); -+ } -+}); ++function themeIconPath(name) { ++ return `/assets/theme-v2/svg/${themeIconFileName(name)}`; ++} + -+test("serves every Theme V2 SVG asset as an external file", async ({ request }) => { -+ const server = await startRepoServer(); -+ try { -+ for (const fileName of REQUIRED_SVG_FILES) { -+ const response = await request.get(`${server.baseUrl}/assets/theme-v2/svg/${fileName}`); -+ expect(response.ok(), fileName).toBe(true); -+ expect(response.headers()["content-type"], fileName).toContain("image/svg+xml"); -+ const body = await response.text(); -+ expect(body, fileName).toContain('viewBox="0 0 24 24"'); -+ expect(body, fileName).toContain('stroke="currentColor"'); -+ } -+ } finally { -+ await server.close(); ++function normalizeClassName(className) { ++ if (Array.isArray(className)) { ++ return className.filter(Boolean).join(" "); + } -+}); ++ return className || ""; ++} + -+test("documents the SVG registry and authoritative artwork policy", async () => { -+ const readme = await fs.readFile(readmePath, "utf8"); -+ const styleGuide = await fs.readFile(styleGuidePath, "utf8"); -+ const requiredPhrases = [ -+ "The SVG files in `assets/theme-v2/svg/` are the authoritative Theme V2 icon source.", -+ "Do not regenerate, redesign, simplify, optimize, or redraw these SVG files during validation-only PRs.", -+ "If a required SVG is missing, report validation failure instead of generating a replacement.", -+ '`viewBox="0 0 24 24"`', -+ '`fill="none"`', -+ '`stroke="currentColor"`', -+ '`stroke-linecap="round"`', -+ '`stroke-linejoin="round"`', -+ ]; -+ -+ for (const phrase of requiredPhrases) { -+ expect(readme).toContain(phrase); -+ expect(styleGuide).toContain(phrase); ++function createThemeIcon(name, options = {}) { ++ const icon = document.createElement("span"); ++ const extraClassName = normalizeClassName(options.className); ++ icon.className = ["theme-icon", `theme-icon--${name}`, extraClassName].filter(Boolean).join(" "); ++ icon.dataset.themeIcon = name; ++ icon.dataset.themeIconFile = themeIconFileName(name); ++ ++ if (options.label) { ++ icon.setAttribute("role", "img"); ++ icon.setAttribute("aria-label", options.label); ++ } else { ++ icon.setAttribute("aria-hidden", "true"); + } -+}); -diff --git a/docs_build/pr/APPLY_PR_26175_ALFA_047-theme-v2-svg-icon-registry.md b/docs_build/pr/APPLY_PR_26175_ALFA_047-theme-v2-svg-icon-registry.md -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,24 @@ -+# APPLY PR_26175_ALFA_047-theme-v2-svg-icon-registry + -+## Apply Summary ++ return icon; ++} ++ ++const themeIconsApi = Object.freeze({ ++ createThemeIcon, ++ themeIconFileName, ++ themeIconPath, ++ themeV2IconRegistry, ++}); + -+- Applied on branch `codex/pr-26175-alfa-047-theme-v2-svg-icon-registry`. -+- Preserved the user-authored SVG artwork under `assets/theme-v2/svg/` as the authoritative Theme V2 icon source. -+- Added registry documentation in `assets/theme-v2/svg/README.md`. -+- Added the Theme V2 icon style guide in `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md`. -+- Added targeted Playwright validation for required SVG filenames, forbidden names, XML parsing, shared attributes, static serving, and documentation policy. -+- No runtime UI conversion, accordion conversion, CSS-only icon generation, JS-only icon registry, or SVG artwork regeneration was applied. ++if (typeof window !== "undefined") { ++ window.ThemeV2Icons = themeIconsApi; ++} + -+## Requirement Evidence ++export { ++ createThemeIcon, ++ themeIconFileName, ++ themeIconPath, ++ themeV2IconRegistry, ++}; +diff --git a/assets/theme-v2/js/tool-display-mode.js b/assets/theme-v2/js/tool-display-mode.js +index 08bada607..b3f6f3265 100644 +--- a/assets/theme-v2/js/tool-display-mode.js ++++ b/assets/theme-v2/js/tool-display-mode.js +@@ -12,6 +12,102 @@ + const toolName = pageTitle ? pageTitle.textContent.trim() : "Tool"; + const routeSlug = window.location.pathname.split("/").pop().replace(/\.html$/, ""); + const toolSlug = slot.dataset.toolSlug || routeSlug; ++ let themeIconRegistry = window.ThemeV2Icons || null; ++ ++ function fallbackThemeIconFileName(name) { ++ return "gfs-" + name + ".svg"; ++ } + -+- PASS evidence is recorded in `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_requirements-checklist.md`. -+- Manual validation notes are recorded in `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md`. ++ function createThemeIconNode(name, className) { ++ if (themeIconRegistry && typeof themeIconRegistry.createThemeIcon === "function") { ++ return themeIconRegistry.createThemeIcon(name, { className }); ++ } ++ ++ const icon = document.createElement("span"); ++ icon.className = ["theme-icon", "theme-icon--" + name, className].filter(Boolean).join(" "); ++ icon.dataset.themeIcon = name; ++ icon.dataset.themeIconFile = fallbackThemeIconFileName(name); ++ icon.setAttribute("aria-hidden", "true"); ++ return icon; ++ } + -+## Validation Evidence ++ function createChevronShell(name, shellClassName, iconClassName) { ++ const shell = document.createElement("span"); ++ shell.className = shellClassName; ++ shell.setAttribute("aria-hidden", "true"); ++ shell.appendChild(createThemeIconNode(name, iconClassName)); ++ return shell; ++ } + -+- PASS: `npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1` -+- PASS: `rg -n "<[s]tyle|[s]tyle=" docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` returned no matches. ++ function replaceIconNode(parent, selector, icon) { ++ const current = parent.querySelector(selector); ++ if (current) { ++ current.replaceWith(icon); ++ } else { ++ parent.appendChild(icon); ++ } ++ } + -+## Artifact ++ function updateVerticalAccordionChevron(details) { ++ const accordionSummary = details.querySelector(":scope > summary"); ++ if (!accordionSummary) return; ++ const iconName = details.open ? "chevron-up" : "chevron-down"; ++ const shell = createChevronShell(iconName, "vertical-accordion__chevron", "vertical-accordion__chevron-icon"); ++ replaceIconNode(accordionSummary, ":scope > .vertical-accordion__chevron", shell); ++ } + -+- `tmp/PR_26175_ALFA_047-theme-v2-svg-icon-registry_delta.zip` -diff --git a/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_report.md b/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_report.md -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,40 @@ -+# PR_26175_ALFA_047-theme-v2-svg-icon-registry Report -+ -+## Status -+PASS -+ -+## Rework Input State -+- Branch at update start: `codex/pr-26175-alfa-047-theme-v2-svg-icon-registry`. -+- The working tree contained manually designed SVG files under `assets/theme-v2/svg/`. -+- Those SVG files are treated as user-authored authoritative artwork. -+ -+## Summary -+- Kept the existing SVG artwork unchanged during this update. -+- Updated registry documentation in `assets/theme-v2/svg/README.md`. -+- Updated the Theme V2 icon style guide to state that `assets/theme-v2/svg/` is the authoritative SVG source. -+- Updated Playwright validation to check only: -+ - required filenames exist -+ - forbidden filenames are absent -+ - SVG files are well-formed XML -+ - required shared SVG attributes are present and valid -+ - SVG files are served as external assets -+ - documentation records the no-regeneration policy -+- Removed geometry-specific validation from the test expectations. -+ -+## Evidence -+- Source of truth: `docs_build/dev/BUILD_PR.md` -+- SVG assets: `assets/theme-v2/svg/` -+- Registry documentation: `assets/theme-v2/svg/README.md` -+- Icon style guide: `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md` -+- Test coverage: `tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` -+- Manual validation notes: `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md` -+- Changed-file manifest: `docs_build/dev/reports/codex_changed_files.txt` -+- Review diff: `docs_build/dev/reports/codex_review.diff` -+ -+## Validation -+- PASS: `npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1` ran 5 tests. -+- PASS: `rg -n "<[s]tyle|[s]tyle=" docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` returned no matches. -+- PASS: Branch validation stayed on `codex/pr-26175-alfa-047-theme-v2-svg-icon-registry`. -+ -+## Artifact -+- `tmp/PR_26175_ALFA_047-theme-v2-svg-icon-registry_delta.zip` -diff --git a/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_validation-lane.md b/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_validation-lane.md -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,29 @@ -+# PR_26175_ALFA_047-theme-v2-svg-icon-registry Validation Lane ++ function wireVerticalAccordionChevron(details) { ++ if (details.dataset.themeV2ChevronWired === "true") { ++ updateVerticalAccordionChevron(details); ++ return; ++ } ++ ++ details.dataset.themeV2ChevronWired = "true"; ++ details.addEventListener("toggle", function () { ++ updateVerticalAccordionChevron(details); ++ }); ++ updateVerticalAccordionChevron(details); ++ } + -+## Commands ++ function refreshVerticalAccordionChevrons() { ++ document.querySelectorAll("details.vertical-accordion").forEach(wireVerticalAccordionChevron); ++ } + -+```powershell -+npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1 -+``` ++ function updateToolDisplayModeChevron() { ++ const iconName = displayMode.open ? "chevron-up" : "chevron-down"; ++ const shell = createChevronShell(iconName, "tool-display-mode__chevron", "tool-display-mode__chevron-icon"); ++ replaceIconNode(summary, ":scope > .tool-display-mode__chevron", shell); ++ } + -+Result: PASS ++ function horizontalToggleIconName(button) { ++ const expanded = button.getAttribute("aria-expanded") !== "false"; ++ const isLeft = button.classList.contains("horizontal-accordion-toggle--left"); ++ if (isLeft) { ++ return expanded ? "chevron-left" : "chevron-right"; ++ } ++ return expanded ? "chevron-right" : "chevron-left"; ++ } + -+Evidence: -+- 5 tests passed. -+- Coverage verifies required filenames, forbidden filenames, well-formed SVG XML, required shared SVG attributes, static serving, registry documentation, and style-guide authority. -+- Coverage does not inspect, simplify, optimize, redraw, or enforce artwork geometry. ++ function updateHorizontalToggleIcon(button) { ++ button.replaceChildren(createThemeIconNode(horizontalToggleIconName(button), "horizontal-accordion-toggle__icon")); ++ } + -+```powershell -+rg -n "<[s]tyle|[s]tyle=" docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs -+``` ++ function refreshHorizontalToggleIcons() { ++ document.querySelectorAll(".horizontal-accordion-toggle").forEach(updateHorizontalToggleIcon); ++ } + -+Result: PASS ++ function refreshThemeIcons() { ++ refreshVerticalAccordionChevrons(); ++ updateToolDisplayModeChevron(); ++ refreshHorizontalToggleIcons(); ++ } + -+Evidence: -+- No matches were returned. ++ import("/assets/theme-v2/js/theme-icons.js").then(function (module) { ++ themeIconRegistry = module; ++ refreshThemeIcons(); ++ }).catch(function () { ++ themeIconRegistry = window.ThemeV2Icons || themeIconRegistry; ++ }); + + function explicitPngName(source) { + if (!source) return ""; +@@ -53,6 +149,7 @@ + fullscreenName.textContent = toolName; + summary.appendChild(fullscreenName); + displayMode.appendChild(summary); ++ displayMode.addEventListener("toggle", updateToolDisplayModeChevron); + + const body = document.createElement("div"); + body.className = "tool-display-mode__body"; +@@ -202,13 +299,16 @@ + } + }); + ++ refreshVerticalAccordionChevrons(); ++ updateToolDisplayModeChevron(); ++ + document.querySelectorAll(".tool-workspace").forEach(function (workspace) { + const columns = workspace.querySelectorAll(":scope > .tool-column"); + if (columns.length < 2) return; + + const sideColumns = [ +- { column: columns[0], side: "left", openIndicator: "<", closedIndicator: ">" }, +- { column: columns[columns.length - 1], side: "right", openIndicator: ">", closedIndicator: "<" } ++ { column: columns[0], side: "left" }, ++ { column: columns[columns.length - 1], side: "right" } + ]; + + sideColumns.forEach(function (entry) { +@@ -222,15 +322,15 @@ + button.className = "horizontal-accordion-toggle horizontal-accordion-toggle--" + entry.side; + button.setAttribute("aria-label", "Collapse " + label); + button.setAttribute("aria-expanded", "true"); +- button.textContent = entry.openIndicator; ++ updateHorizontalToggleIcon(button); + header.insertBefore(button, header.firstChild); + + button.addEventListener("click", function () { + const collapsed = entry.column.classList.toggle("is-collapsed"); + workspace.classList.toggle("is-" + entry.side + "-collapsed", collapsed); +- button.textContent = collapsed ? entry.closedIndicator : entry.openIndicator; + button.setAttribute("aria-expanded", collapsed ? "false" : "true"); + button.setAttribute("aria-label", (collapsed ? "Expand " : "Collapse ") + label); ++ updateHorizontalToggleIcon(button); + }); + }); + }); +diff --git a/assets/toolbox/idea-board/js/index.js b/assets/toolbox/idea-board/js/index.js +index 0c10d2dc1..5b33ae05d 100644 +--- a/assets/toolbox/idea-board/js/index.js ++++ b/assets/toolbox/idea-board/js/index.js +@@ -1,5 +1,6 @@ + import { createServerRepositoryClient } from "../../../../src/api/server-api-client.js"; + import { getSessionCurrent } from "../../../../src/api/session-api-client.js"; ++import { createThemeIcon, themeIconFileName } from "../../../theme-v2/js/theme-icons.js"; + + const editableStatusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready"]); + const filterStatusOptions = Object.freeze(["New", "Exploring", "Refining", "Ready", "Project", "Archived"]); +@@ -267,10 +268,9 @@ function renderIdeaRow(tbody, record) { + const ideaText = document.createElement("span"); + ideaText.className = "idea-board-idea-label__text"; + ideaText.textContent = record.idea; +- const chevron = document.createElement("span"); +- const chevronIcon = expanded ? "gfs-chevron-up.svg" : "gfs-chevron-down.svg"; +- chevron.className = `idea-board-idea-chevron idea-board-idea-chevron--${expanded ? "up" : "down"}`; +- chevron.setAttribute("aria-hidden", "true"); ++ const chevronName = expanded ? "chevron-up" : "chevron-down"; ++ const chevron = createThemeIcon(chevronName, { className: "idea-board-idea-chevron" }); ++ const chevronIcon = themeIconFileName(chevronName); + chevron.dataset.ideaBoardChevron = record.ideaId; + chevron.dataset.ideaBoardChevronIcon = chevronIcon; + ideaLabel.append(chevron, ideaText); +diff --git a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs +index 2465fa5d1..e517d0623 100644 +--- a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs ++++ b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs +@@ -1,7 +1,7 @@ + import { expect, test } from "@playwright/test"; + import fs from "node:fs/promises"; + import path from "node:path"; +-import { fileURLToPath } from "node:url"; ++import { fileURLToPath, pathToFileURL } from "node:url"; + import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; + + const __filename = fileURLToPath(import.meta.url); +@@ -9,6 +9,7 @@ const __dirname = path.dirname(__filename); + const repoRoot = path.resolve(__dirname, "..", "..", ".."); + const svgRoot = path.join(repoRoot, "assets", "theme-v2", "svg"); + const readmePath = path.join(svgRoot, "README.md"); ++const themeIconsPath = path.join(repoRoot, "assets", "theme-v2", "js", "theme-icons.js"); + const styleGuidePath = path.join(repoRoot, "docs_build", "design", "theme-v2-icons", "theme-v2-icon-style-guide.md"); + + const REQUIRED_SVG_FILES = [ +@@ -37,6 +38,26 @@ const FORBIDDEN_SVG_FILES = [ + "gfs-expand.svg", + ]; + ++const REQUIRED_ICON_REGISTRY = { ++ add: "gfs-add.svg", ++ "chevron-down": "gfs-chevron-down.svg", ++ "chevron-left": "gfs-chevron-left.svg", ++ "chevron-right": "gfs-chevron-right.svg", ++ "chevron-up": "gfs-chevron-up.svg", ++ close: "gfs-close.svg", ++ error: "gfs-error.svg", ++ "exit-fullscreen": "gfs-exit-fullscreen.svg", ++ fullscreen: "gfs-fullscreen.svg", ++ info: "gfs-info.svg", ++ menu: "gfs-menu.svg", ++ search: "gfs-search.svg", ++ settings: "gfs-settings.svg", ++ subtract: "gfs-subtract.svg", ++ success: "gfs-success.svg", ++ trash: "gfs-trash.svg", ++ warning: "gfs-warning.svg", ++}; ++ + function attributeValues(content, attributeName) { + return [...content.matchAll(new RegExp(`\\s${attributeName}="([^"]+)"`, "g"))].map((match) => match[1]); + } +@@ -117,6 +138,53 @@ test("serves every Theme V2 SVG asset as an external file", async ({ request }) + } + }); + ++test("maps shared Theme V2 icon names to standalone SVG files", async () => { ++ const themeIcons = await import(`${pathToFileURL(themeIconsPath).href}?cacheBust=${Date.now()}`); ++ ++ expect(themeIcons.themeV2IconRegistry).toEqual(REQUIRED_ICON_REGISTRY); ++ for (const [iconName, fileName] of Object.entries(REQUIRED_ICON_REGISTRY)) { ++ expect(REQUIRED_SVG_FILES, iconName).toContain(fileName); ++ expect(themeIcons.themeIconFileName(iconName)).toBe(fileName); ++ expect(themeIcons.themeIconPath(iconName)).toBe(`/assets/theme-v2/svg/${fileName}`); ++ } ++}); + -+## Final Validation Status -+PASS ++test("creates CSS-backed registry icon nodes without inline SVG", async ({ page }) => { ++ const server = await startRepoServer(); ++ try { ++ await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); ++ const result = await page.evaluate(async () => { ++ const themeIcons = await import("/assets/theme-v2/js/theme-icons.js"); ++ const icon = themeIcons.createThemeIcon("chevron-down", { className: "test-registry-icon" }); ++ document.body.appendChild(icon); ++ const iconStyles = getComputedStyle(icon); ++ return { ++ ariaHidden: icon.getAttribute("aria-hidden"), ++ className: icon.className, ++ iconFile: icon.dataset.themeIconFile, ++ iconName: icon.dataset.themeIcon, ++ inlineSvgCount: icon.querySelectorAll("svg").length, ++ maskImage: iconStyles.getPropertyValue("-webkit-mask-image") || iconStyles.maskImage, ++ role: icon.getAttribute("role"), ++ tagName: icon.tagName.toLowerCase(), ++ }; ++ }); ++ ++ expect(result).toEqual({ ++ ariaHidden: "true", ++ className: "theme-icon theme-icon--chevron-down test-registry-icon", ++ iconFile: "gfs-chevron-down.svg", ++ iconName: "chevron-down", ++ inlineSvgCount: 0, ++ maskImage: expect.stringContaining("gfs-chevron-down.svg"), ++ role: null, ++ tagName: "span", ++ }); ++ } finally { ++ await server.close(); ++ } ++}); + -+## Branch Validation -+PASS: Work remained on `codex/pr-26175-alfa-047-theme-v2-svg-icon-registry`. -diff --git a/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_requirements-checklist.md b/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_requirements-checklist.md -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,28 @@ -+# PR_26175_ALFA_047-theme-v2-svg-icon-registry Requirements Checklist -+ -+| Requirement | Status | Evidence | -+| --- | --- | --- | -+| Stay on the current Team Alfa PR branch. | PASS | Work stayed on `codex/pr-26175-alfa-047-theme-v2-svg-icon-registry`. | -+| Use SVG files already present under `assets/theme-v2/svg/` as authoritative source. | PASS | Tests validate the current file set in `assets/theme-v2/svg/`; docs state those files are authoritative. | -+| Do not regenerate SVG artwork. | PASS | No generation command was used; tests/docs only were updated for validation behavior. | -+| Do not redesign SVG artwork. | PASS | This update did not edit SVG geometry. | -+| Do not simplify or optimize path geometry. | PASS | Playwright no longer asserts or rewrites path geometry. | -+| Do not redraw any icon. | PASS | Existing SVG artwork is treated as user-authored content. | -+| Required SVG filenames exist. | PASS | Playwright verifies the exact required filename list. | -+| Missing required SVGs are validation failures, not generated replacements. | PASS | Style guide and README document failure behavior; tests fail if the required list is incomplete. | -+| Forbidden names `expand`, `collapse`, and `delete` are absent. | PASS | Playwright verifies those filenames do not exist. | -+| Each SVG is well-formed. | PASS | Playwright parses each SVG with `DOMParser` as `image/svg+xml`. | -+| Each SVG uses `viewBox="0 0 24 24"`. | PASS | Playwright verifies every required SVG. | -+| Each SVG uses `fill="none"`. | PASS | Playwright verifies every `fill` attribute value is `none`. | -+| Each SVG uses `stroke="currentColor"`. | PASS | Playwright verifies every `stroke` attribute value is `currentColor`. | -+| Each SVG uses rounded line caps. | PASS | Playwright verifies `stroke-linecap="round"`. | -+| Each SVG uses rounded line joins. | PASS | Playwright verifies `stroke-linejoin="round"`. | -+| Do not use CSS-only or JS-only icon generation. | PASS | No Theme V2 CSS/JS generator files are included; docs forbid replacement with CSS-only or JS-only generation. | -+| Update registry documentation. | PASS | `assets/theme-v2/svg/README.md` documents the authoritative asset pack and validation policy. | -+| Update Theme V2 icon style guide. | PASS | `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md` documents the same authority and validation policy. | -+| Update Playwright validation. | PASS | `tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` now validates presence, XML, attributes, serving, and docs without geometry assertions. | -+| No accordion conversion in ALFA_047. | PASS | No runtime UI files were modified. | -+| No runtime UI conversion in ALFA_047. | PASS | Final delta is assets, docs, tests, reports, and BUILD metadata only. | -+ -+## Overall Status -+PASS -diff --git a/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md b/docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md -new file mode 100644 -index 000000000..000000000 -@@ -0,0 +1,25 @@ -+# PR_26175_ALFA_047-theme-v2-svg-icon-registry Manual Validation Notes -+ -+## Branch -+PASS: Work remained on `codex/pr-26175-alfa-047-theme-v2-svg-icon-registry`. -+ -+## Scope Review -+PASS: ALFA_047 remains limited to standalone SVG assets, registry/style-guide documentation, targeted tests, BUILD doc updates, and reports. -+ -+PASS: No runtime UI conversion, accordion conversion, Theme V2 CSS change, or Theme V2 JS change is included. -+ -+## Artwork Policy Review -+PASS: The current SVG files under `assets/theme-v2/svg/` are treated as user-authored authoritative artwork. -+ -+PASS: Validation was updated to avoid redesigning, redrawing, simplifying, optimizing, or enforcing path geometry. -+ -+PASS: If a required SVG is missing, the Playwright validation fails instead of generating a replacement. -+ -+## Validation Review -+PASS: Required filenames are checked. -+ -+PASS: Each SVG is parsed as well-formed XML. -+ -+PASS: Each SVG is checked for `viewBox="0 0 24 24"`, `fill="none"`, `stroke="currentColor"`, `stroke-linecap="round"`, and `stroke-linejoin="round"`. -+ -+PASS: Registry documentation and the Theme V2 icon style guide document the authoritative-source policy. -diff --git a/docs_build/dev/reports/codex_changed_files.txt b/docs_build/dev/reports/codex_changed_files.txt -@@ -1,9 +1,38 @@ --docs_build/dev/BUILD_PR.md --assets/theme-v2/js/toolbox-status-bar.js --assets/theme-v2/css/status.css --tests/playwright/tools/ToolboxSelectedGameStatusBar.spec.mjs --docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_report.md --docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_validation-lane.md --docs_build/dev/reports/PR_26175_ALFA_011-status-bar-journey-progress-context_requirements-checklist.md --docs_build/dev/reports/codex_review.diff --docs_build/dev/reports/codex_changed_files.txt -+# PR_26175_ALFA_047-theme-v2-svg-icon-registry Changed Files -+ -+## Source, Test, Documentation, And Report Files -+ -+- `docs_build/dev/BUILD_PR.md` - updated -+- `assets/theme-v2/svg/README.md` - added -+- `assets/theme-v2/svg/gfs-add.svg` - added -+- `assets/theme-v2/svg/gfs-chevron-down.svg` - added -+- `assets/theme-v2/svg/gfs-chevron-left.svg` - added -+- `assets/theme-v2/svg/gfs-chevron-right.svg` - added -+- `assets/theme-v2/svg/gfs-chevron-up.svg` - added -+- `assets/theme-v2/svg/gfs-close.svg` - added -+- `assets/theme-v2/svg/gfs-error.svg` - added -+- `assets/theme-v2/svg/gfs-exit-fullscreen.svg` - added -+- `assets/theme-v2/svg/gfs-fullscreen.svg` - added -+- `assets/theme-v2/svg/gfs-info.svg` - added -+- `assets/theme-v2/svg/gfs-menu.svg` - added -+- `assets/theme-v2/svg/gfs-search.svg` - added -+- `assets/theme-v2/svg/gfs-settings.svg` - added -+- `assets/theme-v2/svg/gfs-subtract.svg` - added -+- `assets/theme-v2/svg/gfs-success.svg` - added -+- `assets/theme-v2/svg/gfs-trash.svg` - added -+- `assets/theme-v2/svg/gfs-warning.svg` - added -+- `docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md` - added -+- `tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` - added -+- `docs_build/pr/APPLY_PR_26175_ALFA_047-theme-v2-svg-icon-registry.md` - added -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_report.md` - added -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_validation-lane.md` - added -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_requirements-checklist.md` - added -+- `docs_build/dev/reports/PR_26175_ALFA_047-theme-v2-svg-icon-registry_manual-validation-notes.md` - added -+- `docs_build/dev/reports/codex_changed_files.txt` - updated -+- `docs_build/dev/reports/codex_review.diff` - updated -+ -+## Validation -+ -+- PASS: `npx playwright test tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs --workers=1` ran 5 tests successfully. -+- PASS: `rg -n "<[s]tyle|[s]tyle=" docs_build/design/theme-v2-icons/theme-v2-icon-style-guide.md tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs` returned no matches. -+- PASS: Repo-structured ZIP refreshed at `tmp/PR_26175_ALFA_047-theme-v2-svg-icon-registry_delta.zip`. + test("documents the SVG registry and authoritative artwork policy", async () => { + const readme = await fs.readFile(readmePath, "utf8"); + const styleGuide = await fs.readFile(styleGuidePath, "utf8"); diff --git a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs index 2465fa5d1..e517d0623 100644 --- a/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs +++ b/tests/playwright/tools/ThemeV2SvgIconRegistry.spec.mjs @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import fs from "node:fs/promises"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { startRepoServer } from "../../helpers/playwrightRepoServer.mjs"; const __filename = fileURLToPath(import.meta.url); @@ -9,6 +9,7 @@ const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, "..", "..", ".."); const svgRoot = path.join(repoRoot, "assets", "theme-v2", "svg"); const readmePath = path.join(svgRoot, "README.md"); +const themeIconsPath = path.join(repoRoot, "assets", "theme-v2", "js", "theme-icons.js"); const styleGuidePath = path.join(repoRoot, "docs_build", "design", "theme-v2-icons", "theme-v2-icon-style-guide.md"); const REQUIRED_SVG_FILES = [ @@ -37,6 +38,26 @@ const FORBIDDEN_SVG_FILES = [ "gfs-expand.svg", ]; +const REQUIRED_ICON_REGISTRY = { + add: "gfs-add.svg", + "chevron-down": "gfs-chevron-down.svg", + "chevron-left": "gfs-chevron-left.svg", + "chevron-right": "gfs-chevron-right.svg", + "chevron-up": "gfs-chevron-up.svg", + close: "gfs-close.svg", + error: "gfs-error.svg", + "exit-fullscreen": "gfs-exit-fullscreen.svg", + fullscreen: "gfs-fullscreen.svg", + info: "gfs-info.svg", + menu: "gfs-menu.svg", + search: "gfs-search.svg", + settings: "gfs-settings.svg", + subtract: "gfs-subtract.svg", + success: "gfs-success.svg", + trash: "gfs-trash.svg", + warning: "gfs-warning.svg", +}; + function attributeValues(content, attributeName) { return [...content.matchAll(new RegExp(`\\s${attributeName}="([^"]+)"`, "g"))].map((match) => match[1]); } @@ -117,6 +138,53 @@ test("serves every Theme V2 SVG asset as an external file", async ({ request }) } }); +test("maps shared Theme V2 icon names to standalone SVG files", async () => { + const themeIcons = await import(`${pathToFileURL(themeIconsPath).href}?cacheBust=${Date.now()}`); + + expect(themeIcons.themeV2IconRegistry).toEqual(REQUIRED_ICON_REGISTRY); + for (const [iconName, fileName] of Object.entries(REQUIRED_ICON_REGISTRY)) { + expect(REQUIRED_SVG_FILES, iconName).toContain(fileName); + expect(themeIcons.themeIconFileName(iconName)).toBe(fileName); + expect(themeIcons.themeIconPath(iconName)).toBe(`/assets/theme-v2/svg/${fileName}`); + } +}); + +test("creates CSS-backed registry icon nodes without inline SVG", async ({ page }) => { + const server = await startRepoServer(); + try { + await page.goto(`${server.baseUrl}/toolbox/idea-board/index.html`, { waitUntil: "networkidle" }); + const result = await page.evaluate(async () => { + const themeIcons = await import("/assets/theme-v2/js/theme-icons.js"); + const icon = themeIcons.createThemeIcon("chevron-down", { className: "test-registry-icon" }); + document.body.appendChild(icon); + const iconStyles = getComputedStyle(icon); + return { + ariaHidden: icon.getAttribute("aria-hidden"), + className: icon.className, + iconFile: icon.dataset.themeIconFile, + iconName: icon.dataset.themeIcon, + inlineSvgCount: icon.querySelectorAll("svg").length, + maskImage: iconStyles.getPropertyValue("-webkit-mask-image") || iconStyles.maskImage, + role: icon.getAttribute("role"), + tagName: icon.tagName.toLowerCase(), + }; + }); + + expect(result).toEqual({ + ariaHidden: "true", + className: "theme-icon theme-icon--chevron-down test-registry-icon", + iconFile: "gfs-chevron-down.svg", + iconName: "chevron-down", + inlineSvgCount: 0, + maskImage: expect.stringContaining("gfs-chevron-down.svg"), + role: null, + tagName: "span", + }); + } finally { + await server.close(); + } +}); + test("documents the SVG registry and authoritative artwork policy", async () => { const readme = await fs.readFile(readmePath, "utf8"); const styleGuide = await fs.readFile(styleGuidePath, "utf8");