From 0f8af641c264bb5fef42415e0ebc3cfb582b3b56 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:01:31 +0000 Subject: [PATCH 1/5] Initial plan From 77297978abbfb41c0fdd54757904c94dd6d92cfb Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:03:38 +0000 Subject: [PATCH 2/5] feat(docs): add desktop hamburger toggle for left sidebar Co-authored-by: hellices <5661904+hellices@users.noreply.github.com> --- docs/assets/javascripts/sidebar-toggle.js | 93 ++++++++++++++++ docs/assets/stylesheets/extra.css | 24 +++++ mkdocs.yml | 1 + tests/docs/sidebar-toggle.test.cjs | 126 ++++++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 docs/assets/javascripts/sidebar-toggle.js create mode 100644 tests/docs/sidebar-toggle.test.cjs diff --git a/docs/assets/javascripts/sidebar-toggle.js b/docs/assets/javascripts/sidebar-toggle.js new file mode 100644 index 00000000..ceca0cb0 --- /dev/null +++ b/docs/assets/javascripts/sidebar-toggle.js @@ -0,0 +1,93 @@ +(function () { + "use strict"; + + const storageKey = "dg-sidebar-collapsed"; + const desktopQuery = "(min-width: 1220px)"; + + function readStoredState(storage) { + if (!storage) return false; + try { + return storage.getItem(storageKey) === "true"; + } catch { + return false; + } + } + + function writeStoredState(storage, collapsed) { + if (!storage) return; + try { + storage.setItem(storageKey, collapsed ? "true" : "false"); + } catch { + // Ignore storage failures (private mode / strict policies). + } + } + + function applyState(root, button, storage, collapsed) { + root.dataset.dgSidebarCollapsed = collapsed ? "true" : "false"; + button.setAttribute("aria-pressed", collapsed ? "true" : "false"); + button.setAttribute("aria-label", collapsed ? "왼쪽 메뉴 펼치기" : "왼쪽 메뉴 접기"); + writeStoredState(storage, collapsed); + } + + function mountSidebarToggle(documentRef, windowRef) { + if (!documentRef || !windowRef) return null; + const root = documentRef.documentElement; + if (!root || root.dataset.dgSidebarToggleReady === "true") return null; + const headerInner = documentRef.querySelector(".md-header__inner"); + if (!headerInner) return null; + + root.dataset.dgSidebarToggleReady = "true"; + + const button = documentRef.createElement("button"); + button.type = "button"; + button.className = "md-header__button md-icon dg-sidebar-toggle"; + button.setAttribute("aria-pressed", "false"); + button.setAttribute("aria-label", "왼쪽 메뉴 접기"); + button.title = "왼쪽 메뉴 접기/펼치기"; + button.innerHTML = + ''; + + const mediaQuery = typeof windowRef.matchMedia === "function" + ? windowRef.matchMedia(desktopQuery) + : null; + const storage = windowRef.localStorage || null; + + const syncState = () => { + const collapsed = readStoredState(storage); + applyState(root, button, storage, collapsed); + }; + + button.addEventListener("click", () => { + const collapsed = root.dataset.dgSidebarCollapsed === "true"; + applyState(root, button, storage, !collapsed); + }); + + if (mediaQuery) { + const onMediaChange = event => { + if (event.matches) syncState(); + }; + if (typeof mediaQuery.addEventListener === "function") { + mediaQuery.addEventListener("change", onMediaChange); + } else if (typeof mediaQuery.addListener === "function") { + mediaQuery.addListener(onMediaChange); + } + } + + syncState(); + headerInner.insertBefore(button, headerInner.firstChild); + return button; + } + + if (typeof module !== "undefined" && module.exports) { + module.exports = {mountSidebarToggle, readStoredState, applyState}; + } + + if (typeof document !== "undefined") { + const start = () => mountSidebarToggle(document, window); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", start); + } else { + start(); + } + } +})(); diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css index 36dae0db..d6cc68be 100644 --- a/docs/assets/stylesheets/extra.css +++ b/docs/assets/stylesheets/extra.css @@ -286,7 +286,31 @@ body { box-shadow: inset 2px 0 var(--dg-accent); } +.dg-sidebar-toggle { + display: none; +} + @media (min-width: 1220px) { + .dg-sidebar-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + } + + .dg-sidebar-toggle svg { + width: 1.1rem; + height: 1.1rem; + fill: currentColor; + } + + [data-dg-sidebar-collapsed="true"] .md-sidebar--primary { + display: none; + } + + [data-dg-sidebar-collapsed="true"] .dg-sidebar-toggle { + color: var(--dg-accent); + } + .md-nav--primary .md-nav[data-md-level="1"], .md-nav--primary .md-nav[data-md-level="2"] { border-left: 1px solid var(--dg-border); diff --git a/mkdocs.yml b/mkdocs.yml index 73df1196..ecf9af16 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -82,5 +82,6 @@ extra_css: extra_javascript: - assets/javascripts/explore.js + - assets/javascripts/sidebar-toggle.js copyright: Copyright © Azure Engineering Notes contributors diff --git a/tests/docs/sidebar-toggle.test.cjs b/tests/docs/sidebar-toggle.test.cjs new file mode 100644 index 00000000..8b3b00a5 --- /dev/null +++ b/tests/docs/sidebar-toggle.test.cjs @@ -0,0 +1,126 @@ +const test = require("node:test"); +const assert = require("node:assert/strict"); +const toggle = require("../../docs/assets/javascripts/sidebar-toggle.js"); + +function createFixture(stored = "false") { + const root = {dataset: {}}; + const listeners = {}; + const buttonListeners = {}; + const inserted = []; + const mediaListeners = []; + + const button = { + attributes: {}, + type: "", + className: "", + title: "", + innerHTML: "", + setAttribute(name, value) { + this.attributes[name] = value; + }, + addEventListener(type, callback) { + buttonListeners[type] = callback; + }, + }; + + const headerInner = { + firstChild: null, + insertBefore(node) { + inserted.push(node); + }, + }; + + const localStorage = { + value: stored, + getItem() { + return this.value; + }, + setItem(_key, value) { + this.value = value; + }, + }; + + const media = { + matches: true, + addEventListener(type, callback) { + if (type === "change") mediaListeners.push(callback); + }, + emit(matches) { + this.matches = matches; + for (const callback of mediaListeners) callback({matches}); + }, + }; + + const documentRef = { + documentElement: root, + querySelector(selector) { + return selector === ".md-header__inner" ? headerInner : null; + }, + createElement(tag) { + assert.equal(tag, "button"); + return button; + }, + addEventListener(type, callback) { + listeners[type] = callback; + }, + }; + + const windowRef = { + localStorage, + matchMedia() { + return media; + }, + }; + + return { + root, + button, + buttonListeners, + inserted, + documentRef, + windowRef, + localStorage, + media, + listeners, + }; +} + +test("mountSidebarToggle inserts a button and applies stored collapsed state", () => { + const fixture = createFixture("true"); + const mounted = toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); + assert.equal(mounted, fixture.button); + assert.equal(fixture.inserted.length, 1); + assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); + assert.equal(fixture.button.attributes["aria-pressed"], "true"); + assert.equal(fixture.button.attributes["aria-label"], "왼쪽 메뉴 펼치기"); + assert.equal(fixture.root.dataset.dgSidebarToggleReady, "true"); +}); + +test("button click toggles collapsed state and persists to storage", () => { + const fixture = createFixture("false"); + toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); + fixture.buttonListeners.click(); + assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); + assert.equal(fixture.localStorage.value, "true"); + fixture.buttonListeners.click(); + assert.equal(fixture.root.dataset.dgSidebarCollapsed, "false"); + assert.equal(fixture.localStorage.value, "false"); +}); + +test("media change back to desktop re-syncs persisted state", () => { + const fixture = createFixture("false"); + toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); + fixture.buttonListeners.click(); + assert.equal(fixture.localStorage.value, "true"); + fixture.root.dataset.dgSidebarCollapsed = "false"; + fixture.media.emit(true); + assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); +}); + +test("mountSidebarToggle is idempotent once initialized", () => { + const fixture = createFixture("false"); + toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); + const second = toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); + assert.equal(second, null); + assert.equal(fixture.inserted.length, 1); +}); From 37ec495d304e39b9673387141121598b0c134cbe Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:07:08 +0000 Subject: [PATCH 3/5] fix(docs): hide desktop sidebar toggle on mobile widths Co-authored-by: hellices <5661904+hellices@users.noreply.github.com> --- docs/assets/stylesheets/extra.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css index d6cc68be..66e8a6b9 100644 --- a/docs/assets/stylesheets/extra.css +++ b/docs/assets/stylesheets/extra.css @@ -290,8 +290,14 @@ body { display: none; } +@media (max-width: 1219px) { + .md-header .dg-sidebar-toggle { + display: none !important; + } +} + @media (min-width: 1220px) { - .dg-sidebar-toggle { + .md-header .dg-sidebar-toggle { display: inline-flex; align-items: center; justify-content: center; From a96ef2f3330a066b178cf507147c8af364671dc7 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Mon, 14 Sep 2026 02:37:43 +0000 Subject: [PATCH 4/5] fix(docs): address sidebar toggle review feedback Co-authored-by: hellices <5661904+hellices@users.noreply.github.com> --- docs/assets/javascripts/sidebar-toggle.js | 20 ++++++++++++++------ tests/docs/sidebar-toggle.test.cjs | 22 ++++++++++++++++++++-- tests/docs/test_sidebar_toggle.py | 20 ++++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 tests/docs/test_sidebar_toggle.py diff --git a/docs/assets/javascripts/sidebar-toggle.js b/docs/assets/javascripts/sidebar-toggle.js index ceca0cb0..9e12d149 100644 --- a/docs/assets/javascripts/sidebar-toggle.js +++ b/docs/assets/javascripts/sidebar-toggle.js @@ -22,10 +22,18 @@ } } + function getStorage(windowRef) { + try { + return windowRef.localStorage || null; + } catch { + return null; + } + } + function applyState(root, button, storage, collapsed) { root.dataset.dgSidebarCollapsed = collapsed ? "true" : "false"; - button.setAttribute("aria-pressed", collapsed ? "true" : "false"); - button.setAttribute("aria-label", collapsed ? "왼쪽 메뉴 펼치기" : "왼쪽 메뉴 접기"); + button.setAttribute("aria-expanded", collapsed ? "false" : "true"); + button.setAttribute("aria-label", "왼쪽 메뉴"); writeStoredState(storage, collapsed); } @@ -41,8 +49,8 @@ const button = documentRef.createElement("button"); button.type = "button"; button.className = "md-header__button md-icon dg-sidebar-toggle"; - button.setAttribute("aria-pressed", "false"); - button.setAttribute("aria-label", "왼쪽 메뉴 접기"); + button.setAttribute("aria-expanded", "true"); + button.setAttribute("aria-label", "왼쪽 메뉴"); button.title = "왼쪽 메뉴 접기/펼치기"; button.innerHTML = ''; @@ -50,7 +58,7 @@ const mediaQuery = typeof windowRef.matchMedia === "function" ? windowRef.matchMedia(desktopQuery) : null; - const storage = windowRef.localStorage || null; + const storage = getStorage(windowRef); const syncState = () => { const collapsed = readStoredState(storage); @@ -79,7 +87,7 @@ } if (typeof module !== "undefined" && module.exports) { - module.exports = {mountSidebarToggle, readStoredState, applyState}; + module.exports = {mountSidebarToggle, readStoredState, applyState, getStorage}; } if (typeof document !== "undefined") { diff --git a/tests/docs/sidebar-toggle.test.cjs b/tests/docs/sidebar-toggle.test.cjs index 8b3b00a5..34bdb13f 100644 --- a/tests/docs/sidebar-toggle.test.cjs +++ b/tests/docs/sidebar-toggle.test.cjs @@ -91,8 +91,9 @@ test("mountSidebarToggle inserts a button and applies stored collapsed state", ( assert.equal(mounted, fixture.button); assert.equal(fixture.inserted.length, 1); assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); - assert.equal(fixture.button.attributes["aria-pressed"], "true"); - assert.equal(fixture.button.attributes["aria-label"], "왼쪽 메뉴 펼치기"); + assert.equal(fixture.button.attributes["aria-expanded"], "false"); + assert.equal(fixture.button.attributes["aria-label"], "왼쪽 메뉴"); + assert.equal(fixture.button.attributes["aria-pressed"], undefined); assert.equal(fixture.root.dataset.dgSidebarToggleReady, "true"); }); @@ -101,12 +102,29 @@ test("button click toggles collapsed state and persists to storage", () => { toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); fixture.buttonListeners.click(); assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); + assert.equal(fixture.button.attributes["aria-expanded"], "false"); assert.equal(fixture.localStorage.value, "true"); fixture.buttonListeners.click(); assert.equal(fixture.root.dataset.dgSidebarCollapsed, "false"); + assert.equal(fixture.button.attributes["aria-expanded"], "true"); assert.equal(fixture.localStorage.value, "false"); }); +test("blocked localStorage still mounts without persistence", () => { + const fixture = createFixture("true"); + Object.defineProperty(fixture.windowRef, "localStorage", { + get() { + throw new Error("storage blocked"); + }, + }); + const mounted = toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); + assert.equal(mounted, fixture.button); + assert.equal(fixture.inserted.length, 1); + assert.equal(fixture.root.dataset.dgSidebarCollapsed, "false"); + fixture.buttonListeners.click(); + assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); +}); + test("media change back to desktop re-syncs persisted state", () => { const fixture = createFixture("false"); toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); diff --git a/tests/docs/test_sidebar_toggle.py b/tests/docs/test_sidebar_toggle.py new file mode 100644 index 00000000..e941b261 --- /dev/null +++ b/tests/docs/test_sidebar_toggle.py @@ -0,0 +1,20 @@ +import shutil +import subprocess +from pathlib import Path + +import pytest + +ROOT = Path(__file__).parents[2] + + +def test_sidebar_toggle_javascript_behavior_with_available_node(): + node = shutil.which("node") + if not node: + pytest.skip("Node unavailable: parent browser verification is required") + result = subprocess.run( + [node, "--test", str(ROOT / "tests/docs/sidebar-toggle.test.cjs")], + cwd=ROOT, + capture_output=True, + text=True, + ) + assert result.returncode == 0, result.stdout + result.stderr From a0746a8b1500ba080dcf12236c03141bc65c94b8 Mon Sep 17 00:00:00 2001 From: "openai-code-agent[bot]" <242516109+Codex@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:03:10 +0000 Subject: [PATCH 5/5] test(docs): keep sidebar toggle checks minimal Co-authored-by: hellices <5661904+hellices@users.noreply.github.com> --- tests/docs/sidebar-toggle.test.cjs | 32 +--------------------------- tests/docs/test_faceted_discovery.py | 5 +++-- tests/docs/test_sidebar_toggle.py | 20 ----------------- 3 files changed, 4 insertions(+), 53 deletions(-) delete mode 100644 tests/docs/test_sidebar_toggle.py diff --git a/tests/docs/sidebar-toggle.test.cjs b/tests/docs/sidebar-toggle.test.cjs index 34bdb13f..f20c0e2e 100644 --- a/tests/docs/sidebar-toggle.test.cjs +++ b/tests/docs/sidebar-toggle.test.cjs @@ -4,10 +4,8 @@ const toggle = require("../../docs/assets/javascripts/sidebar-toggle.js"); function createFixture(stored = "false") { const root = {dataset: {}}; - const listeners = {}; const buttonListeners = {}; const inserted = []; - const mediaListeners = []; const button = { attributes: {}, @@ -42,13 +40,7 @@ function createFixture(stored = "false") { const media = { matches: true, - addEventListener(type, callback) { - if (type === "change") mediaListeners.push(callback); - }, - emit(matches) { - this.matches = matches; - for (const callback of mediaListeners) callback({matches}); - }, + addEventListener() {}, }; const documentRef = { @@ -60,9 +52,6 @@ function createFixture(stored = "false") { assert.equal(tag, "button"); return button; }, - addEventListener(type, callback) { - listeners[type] = callback; - }, }; const windowRef = { @@ -81,7 +70,6 @@ function createFixture(stored = "false") { windowRef, localStorage, media, - listeners, }; } @@ -124,21 +112,3 @@ test("blocked localStorage still mounts without persistence", () => { fixture.buttonListeners.click(); assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); }); - -test("media change back to desktop re-syncs persisted state", () => { - const fixture = createFixture("false"); - toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); - fixture.buttonListeners.click(); - assert.equal(fixture.localStorage.value, "true"); - fixture.root.dataset.dgSidebarCollapsed = "false"; - fixture.media.emit(true); - assert.equal(fixture.root.dataset.dgSidebarCollapsed, "true"); -}); - -test("mountSidebarToggle is idempotent once initialized", () => { - const fixture = createFixture("false"); - toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); - const second = toggle.mountSidebarToggle(fixture.documentRef, fixture.windowRef); - assert.equal(second, null); - assert.equal(fixture.inserted.length, 1); -}); diff --git a/tests/docs/test_faceted_discovery.py b/tests/docs/test_faceted_discovery.py index 549c95a1..df1565e1 100644 --- a/tests/docs/test_faceted_discovery.py +++ b/tests/docs/test_faceted_discovery.py @@ -261,11 +261,12 @@ def test_service_discovery_reports_topics_and_documents(taxonomy, catalog): assert "개 주제 · " in pages[PurePosixPath(path)] -def test_javascript_behavior_with_available_node(): +@pytest.mark.parametrize("script_name", ("explore.test.cjs", "sidebar-toggle.test.cjs")) +def test_javascript_behavior_with_available_node(script_name): node = shutil.which("node") if not node: pytest.skip("Node unavailable: parent browser verification is required") - result = subprocess.run([node, "--test", str(ROOT / "tests/docs/explore.test.cjs")], + result = subprocess.run([node, "--test", str(ROOT / "tests/docs" / script_name)], cwd=ROOT, capture_output=True, text=True) assert result.returncode == 0, result.stdout + result.stderr diff --git a/tests/docs/test_sidebar_toggle.py b/tests/docs/test_sidebar_toggle.py deleted file mode 100644 index e941b261..00000000 --- a/tests/docs/test_sidebar_toggle.py +++ /dev/null @@ -1,20 +0,0 @@ -import shutil -import subprocess -from pathlib import Path - -import pytest - -ROOT = Path(__file__).parents[2] - - -def test_sidebar_toggle_javascript_behavior_with_available_node(): - node = shutil.which("node") - if not node: - pytest.skip("Node unavailable: parent browser verification is required") - result = subprocess.run( - [node, "--test", str(ROOT / "tests/docs/sidebar-toggle.test.cjs")], - cwd=ROOT, - capture_output=True, - text=True, - ) - assert result.returncode == 0, result.stdout + result.stderr