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