Refine settings accordion and API examples modal

- Replace the two-step settings nav with a sticky accordion that tracks active sections - Restyle the settings rail with opacity-based active state and hash-aware opening - Reinitialize and clean up API example Ace editors across modal reopen cycles - Preserve modal html classes and center settings loading/error states across the full modal body

Alessandro committed May 7, 2026 at 19:41 UTC 36c2e3d6b8f9cce7230585c3a14bee082eea12d2
5 files changed +357 -93
webui/components/settings/external/api-examples.html
+76 -9
@@ -5,7 +5,11 @@
5 </head>
6
7 <body>
8 - <div x-data>
8 + <div
9 + x-data
10 + x-init="window.initExternalApiExamples && window.initExternalApiExamples($el)"
11 + x-destroy="window.destroyExternalApiExamples && window.destroyExternalApiExamples($el)"
12 + >
13 <p>Agent Zero provides external API endpoints for integration with other applications.</p>
14 <p>These endpoints use API key authentication and support text messages and file attachments.</p>
15
@@ -193,7 +197,51 @@
197 <script type="module">
198 import * as API from "/js/api.js";
199
196 - setTimeout(async () => {
200 + const mountedEditors = new WeakMap();
201 +
202 + function scheduleEditorResize(root, editors) {
203 + const state = {
204 + editors,
205 + frameId: null,
206 + timeoutIds: [],
207 + };
208 + const resize = () => {
209 + if (mountedEditors.get(root) !== state) return;
210 + editors.forEach((editor) => editor.resize(true));
211 + };
212 + state.frameId = requestAnimationFrame(() => {
213 + state.frameId = null;
214 + resize();
215 + state.timeoutIds = [
216 + setTimeout(resize, 80),
217 + setTimeout(resize, 240),
218 + ];
219 + });
220 + mountedEditors.set(root, state);
221 + }
222 +
223 + function destroyEditors(root) {
224 + const state = mountedEditors.get(root);
225 + if (!state) return;
226 +
227 + if (state.frameId) {
228 + cancelAnimationFrame(state.frameId);
229 + }
230 + state.timeoutIds.forEach((timeoutId) => clearTimeout(timeoutId));
231 +
232 + state.editors.forEach((editor) => {
233 + try {
234 + editor.destroy();
235 + editor.container.removeAttribute("data-ace-editor-id");
236 + editor.container.textContent = "";
237 + } catch (error) {
238 + console.warn("Failed to destroy API example editor:", error);
239 + }
240 + });
241 + mountedEditors.delete(root);
242 + }
243 +
244 + async function buildExamples() {
245 const url = window.location.origin;
246
247 // Fetch token from settings API
@@ -670,8 +718,7 @@ async function sendMessageWithProject() {
718 // Call the function
719 sendMessageWithProject();`;
720
673 - // Initialize ACE editors
674 - const editors = [
721 + return [
722 { id: "api-basic-example", content: basicExample },
723 { id: "api-continuation-example", content: continuationExample },
724 { id: "api-attachment-example", content: attachmentExample },
@@ -682,9 +729,25 @@ sendMessageWithProject();`;
729 { id: "api-reset-example", content: resetExample },
730 { id: "api-files-get-example", content: filesGetExample }
731 ];
732 + }
733 +
734 + window.destroyExternalApiExamples = destroyEditors;
735 +
736 + window.initExternalApiExamples = async (root) => {
737 + if (!root || !window.ace) return;
738
686 - editors.forEach(({ id, content }) => {
687 - const editor = ace.edit(id);
739 + destroyEditors(root);
740 + const examples = await buildExamples();
741 + if (!root.isConnected) return;
742 +
743 + const editors = [];
744 +
745 + examples.forEach(({ id, content }) => {
746 + const container = root.querySelector(`#${id}`);
747 + if (!container) return;
748 + container.textContent = "";
749 +
750 + const editor = ace.edit(container);
751 const dark = localStorage.getItem("darkMode");
752 if (dark != "false") {
753 editor.setTheme("ace/theme/github_dark");
@@ -692,11 +755,15 @@ sendMessageWithProject();`;
755 editor.setTheme("ace/theme/tomorrow");
756 }
757 editor.session.setMode("ace/mode/javascript");
695 - editor.setValue(content);
696 - editor.clearSelection();
758 + editor.setValue(content, -1);
759 editor.setReadOnly(true);
760 + editor.renderer.setShowGutter(true);
761 + editor.renderer.setPadding(8);
762 + editors.push(editor);
763 });
699 - }, 0);
764 +
765 + scheduleEditorResize(root, editors);
766 + };
767 </script>
768 </div>
769
webui/components/settings/settings-store.js
+138 -12
@@ -6,6 +6,8 @@ import { store as notificationStore } from "/components/notifications/notificati
6 const VIEW_MODE_STORAGE_KEY = "settingsActiveTab";
7 const DEFAULT_TAB = "agent";
8 const UPDATE_STATUS_REFRESH_COOLDOWN_MS = 60 * 1000;
9 +// Match the modal header/padding breathing room before promoting a section link.
10 +const SECTION_ACTIVATION_OFFSET = 56;
11
12 const TAB_ITEMS = Object.freeze([
13 {
@@ -93,7 +95,10 @@ const model = {
95 settings: null,
96 additional: null,
97 workdirFileStructureTestOutput: "",
96 - navMode: "categories",
98 + _activeSection: null,
99 + _paneScrollHandler: null,
100 + _paneScrollPane: null,
101 + _scrollSyncFrame: null,
102 _updateStatusRefreshedAt: 0,
103
104 // Tab state
@@ -103,8 +108,12 @@ const model = {
108 },
109 set activeTab(value) {
110 const previous = this._activeTab;
106 - this._activeTab = value;
107 - this.applyActiveTab(previous, value);
111 + this._activeTab = this.normalizeTabId(value);
112 + this.applyActiveTab(previous, this._activeTab);
113 + },
114 +
115 + get activeSection() {
116 + return this._activeSection || this.getFirstSectionId(this.activeTab);
117 },
118
119 // Lifecycle
@@ -112,8 +121,9 @@ const model = {
121 // Restore persisted tab
122 try {
123 const saved = localStorage.getItem(VIEW_MODE_STORAGE_KEY);
115 - if (saved) this._activeTab = saved;
124 + if (saved) this._activeTab = this.normalizeTabId(saved);
125 } catch {}
126 + this._activeSection = this.getFirstSectionId(this._activeTab);
127 },
128
129 async onOpen() {
@@ -138,30 +148,51 @@ const model = {
148
149 this.refreshUpdateStatus();
150
151 + const hashSectionId = this.getHashSectionId();
152 + const openedHashSection = hashSectionId
153 + ? this.activateSection(hashSectionId, { persist: false })
154 + : false;
155 +
156 // Trigger tab activation for current tab
157 + this._activeTab = this.normalizeTabId(this._activeTab);
158 this.applyActiveTab(null, this._activeTab);
159 + this.bindPaneScroll();
160 +
161 + if (openedHashSection) {
162 + this.scrollToSection(hashSectionId);
163 + }
164 },
165
166 cleanup() {
167 + this.unbindPaneScroll();
168 this.settings = null;
169 this.additional = null;
170 this.error = null;
171 this.isLoading = false;
150 - this.navMode = "categories";
172 },
173
174 // Tab management
175 applyActiveTab(previous, current) {
176 + if (!this.sectionBelongsToTab(this._activeSection, current)) {
177 + this._activeSection = this.getFirstSectionId(current);
178 + }
179 +
180 // Persist
181 try {
182 localStorage.setItem(VIEW_MODE_STORAGE_KEY, current);
183 } catch {}
184 +
185 + this.bindPaneScroll();
186 },
187
188 switchTab(tabName) {
189 this.activeTab = tabName;
190 },
191
192 + normalizeTabId(tabName) {
193 + return TAB_ITEMS.some((item) => item.id === tabName) ? tabName : DEFAULT_TAB;
194 + },
195 +
196 get navItems() {
197 return TAB_ITEMS;
198 },
@@ -174,21 +205,62 @@ const model = {
205 return this.activeTabItem?.sections || [];
206 },
207
208 + getFirstSectionId(tabName = this.activeTab) {
209 + const tab = TAB_ITEMS.find((item) => item.id === tabName) || TAB_ITEMS[0];
210 + return tab?.sections?.[0]?.id || null;
211 + },
212 +
213 + getTabIdForSection(sectionId) {
214 + if (!sectionId) return null;
215 + const tab = TAB_ITEMS.find((item) =>
216 + item.sections?.some((section) => section.id === sectionId)
217 + );
218 + return tab?.id || null;
219 + },
220 +
221 + sectionBelongsToTab(sectionId, tabName = this.activeTab) {
222 + if (!sectionId) return false;
223 + return this.getTabIdForSection(sectionId) === tabName;
224 + },
225 +
226 + getHashSectionId() {
227 + const rawHash = window.location.hash || "";
228 + if (!rawHash.startsWith("#section-")) return null;
229 + try {
230 + return decodeURIComponent(rawHash.slice(1));
231 + } catch {
232 + return rawHash.slice(1);
233 + }
234 + },
235 +
236 + activateSection(sectionId, { persist = true } = {}) {
237 + const tabId = this.getTabIdForSection(sectionId);
238 + if (!tabId) return false;
239 +
240 + const previous = this._activeTab;
241 + this._activeTab = tabId;
242 + this._activeSection = sectionId;
243 + if (persist) {
244 + this.applyActiveTab(previous, tabId);
245 + }
246 + if (tabId === "backup") this.refreshUpdateStatus();
247 + return true;
248 + },
249 +
250 enterTab(tabName) {
251 this.activeTab = tabName;
179 - this.navMode = "sections";
252 + this._activeSection = this.getFirstSectionId(this.activeTab);
253 this.resetPaneScroll();
254 if (tabName === "backup") this.refreshUpdateStatus();
255 },
256
184 - backToCategories() {
185 - this.navMode = "categories";
186 - },
187 -
257 resetPaneScroll() {
258 requestAnimationFrame(() => {
259 const pane = this.getSettingsPane();
191 - if (pane) pane.scrollTop = 0;
260 + if (pane) {
261 + pane.scrollTop = 0;
262 + this.updateActiveSectionFromScroll();
263 + }
264 });
265 },
266
@@ -196,6 +268,57 @@ const model = {
268 return document.querySelector(".modal-inner.settings-modal .settings-pane");
269 },
270
271 + bindPaneScroll() {
272 + requestAnimationFrame(() => {
273 + const pane = this.getSettingsPane();
274 + if (!pane || this._paneScrollPane === pane) {
275 + if (pane) this.updateActiveSectionFromScroll();
276 + return;
277 + }
278 +
279 + this.unbindPaneScroll();
280 + this._paneScrollPane = pane;
281 + this._paneScrollHandler = () => this.updateActiveSectionFromScroll();
282 + pane.addEventListener("scroll", this._paneScrollHandler, { passive: true });
283 + this.updateActiveSectionFromScroll();
284 + });
285 + },
286 +
287 + unbindPaneScroll() {
288 + if (this._paneScrollPane && this._paneScrollHandler) {
289 + this._paneScrollPane.removeEventListener("scroll", this._paneScrollHandler);
290 + }
291 + if (this._scrollSyncFrame) {
292 + cancelAnimationFrame(this._scrollSyncFrame);
293 + }
294 + this._paneScrollPane = null;
295 + this._paneScrollHandler = null;
296 + this._scrollSyncFrame = null;
297 + },
298 +
299 + updateActiveSectionFromScroll() {
300 + if (this._scrollSyncFrame) return;
301 + this._scrollSyncFrame = requestAnimationFrame(() => {
302 + this._scrollSyncFrame = null;
303 + const pane = this.getSettingsPane();
304 + if (!pane) return;
305 +
306 + const paneRect = pane.getBoundingClientRect();
307 + const activationTop = paneRect.top + SECTION_ACTIVATION_OFFSET;
308 + let activeId = this.getFirstSectionId(this.activeTab);
309 +
310 + for (const section of this.sectionItems) {
311 + const target = this.getSectionTarget(section.id, pane);
312 + if (!target || target.offsetParent === null) continue;
313 + if (target.getBoundingClientRect().top <= activationTop) {
314 + activeId = section.id;
315 + }
316 + }
317 +
318 + this._activeSection = activeId;
319 + });
320 + },
321 +
322 get selfUpdate() {
323 return globalThis.Alpine?.store?.("selfUpdateStore") || null;
324 },
@@ -210,7 +333,9 @@ const model = {
333
334 scrollToSection(sectionId, event = null) {
335 event?.preventDefault?.();
213 - this.navMode = "sections";
336 + if (!this.activateSection(sectionId)) {
337 + this._activeSection = sectionId;
338 + }
339
340 const performScroll = () => {
341 const pane = this.getSettingsPane();
@@ -231,6 +356,7 @@ const model = {
356 behavior: "smooth",
357 });
358 history.replaceState(null, "", `#${sectionId}`);
359 + this.updateActiveSectionFromScroll();
360 };
361
362 requestAnimationFrame(() => requestAnimationFrame(performScroll));
webui/components/settings/settings.html
+48 -37
@@ -29,45 +29,52 @@
29 <div x-show="$store.settings.settings" class="settings-content">
30 <!-- Tab Navigation -->
31 <aside class="settings-tabs-container" aria-label="Settings categories">
32 - <div class="settings-tabs no-scrollbar" role="tablist" aria-orientation="vertical" x-show="$store.settings.navMode === 'categories'">
32 + <div class="settings-tabs no-scrollbar" role="tablist" aria-orientation="vertical">
33 <template x-for="item in $store.settings.navItems" :key="item.id">
34 - <button type="button"
35 - class="settings-tab"
36 - role="tab"
37 - :aria-selected="$store.settings.activeTab === item.id"
38 - :class="{
39 - 'active': $store.settings.activeTab === item.id,
40 - 'settings-tab-attention': $store.settings.navItemHasAttention(item)
41 - }"
42 - @click="$store.settings.enterTab(item.id)">
43 - <span class="material-symbols-outlined" aria-hidden="true" x-text="item.icon"></span>
44 - <span class="settings-tab-label" x-text="item.label"></span>
45 - <span class="settings-attention-dot"
46 - x-show="$store.settings.navItemHasAttention(item)"
47 - aria-hidden="true"></span>
48 - </button>
49 - </template>
50 - </div>
34 + <div class="settings-nav-group"
35 + :class="{'settings-nav-group-active': $store.settings.activeTab === item.id}">
36 + <button type="button"
37 + class="settings-tab settings-parent-tab"
38 + role="tab"
39 + :aria-selected="$store.settings.activeTab === item.id"
40 + :aria-expanded="$store.settings.activeTab === item.id"
41 + :class="{
42 + 'active': $store.settings.activeTab === item.id,
43 + 'settings-tab-attention': $store.settings.navItemHasAttention(item)
44 + }"
45 + @click="$store.settings.enterTab(item.id)">
46 + <span class="material-symbols-outlined" aria-hidden="true" x-text="item.icon"></span>
47 + <span class="settings-tab-label" x-text="item.label"></span>
48 + <span class="settings-tab-meta">
49 + <span class="settings-attention-dot"
50 + x-show="$store.settings.navItemHasAttention(item)"
51 + aria-hidden="true"></span>
52 + <span class="material-symbols-outlined settings-tab-chevron"
53 + aria-hidden="true">expand_more</span>
54 + </span>
55 + </button>
56
52 - <div class="settings-tabs no-scrollbar settings-section-tabs" x-show="$store.settings.navMode === 'sections'" style="display: none;">
53 - <button type="button"
54 - class="settings-tab settings-back-tab"
55 - @click="$store.settings.backToCategories()">
56 - <span class="material-symbols-outlined" aria-hidden="true">arrow_back</span>
57 - <span class="settings-tab-label">&lt; Back</span>
58 - </button>
59 - <div class="settings-tabs-heading" x-text="$store.settings.activeTabItem?.label || 'Settings'"></div>
60 - <template x-for="item in $store.settings.sectionItems" :key="item.id">
61 - <a class="settings-tab settings-section-link"
62 - :class="{'settings-tab-attention': $store.settings.sectionItemHasAttention(item)}"
63 - :href="`#${item.id}`"
64 - @click="$store.settings.scrollToSection(item.id, $event)">
65 - <span class="material-symbols-outlined" aria-hidden="true" x-text="item.icon"></span>
66 - <span class="settings-tab-label" x-text="item.label"></span>
67 - <span class="settings-attention-dot"
68 - x-show="$store.settings.sectionItemHasAttention(item)"
69 - aria-hidden="true"></span>
70 - </a>
57 + <div class="settings-section-list"
58 + x-show="$store.settings.activeTab === item.id"
59 + x-transition.opacity.duration.120ms>
60 + <template x-for="section in item.sections" :key="section.id">
61 + <a class="settings-section-link"
62 + :class="{
63 + 'active': $store.settings.activeSection === section.id,
64 + 'settings-tab-attention': $store.settings.sectionItemHasAttention(section)
65 + }"
66 + :aria-current="$store.settings.activeSection === section.id ? 'true' : null"
67 + :href="`#${section.id}`"
68 + @click="$store.settings.scrollToSection(section.id, $event)">
69 + <span class="material-symbols-outlined" aria-hidden="true" x-text="section.icon"></span>
70 + <span class="settings-tab-label" x-text="section.label"></span>
71 + <span class="settings-attention-dot"
72 + x-show="$store.settings.sectionItemHasAttention(section)"
73 + aria-hidden="true"></span>
74 + </a>
75 + </template>
76 + </div>
77 + </div>
78 </template>
79 </div>
80 </aside>
@@ -118,9 +125,13 @@
125 .settings-loading,
126 .settings-error {
127 display: flex;
128 + flex: 1 1 auto;
129 align-items: center;
130 justify-content: center;
131 + align-self: stretch;
132 gap: 0.75rem;
133 + width: 100%;
134 + min-height: 0;
135 padding: 2rem;
136 color: var(--color-text-secondary, #999);
137 font-size: 1rem;
webui/css/settings.css
+92 -33
@@ -150,8 +150,17 @@ select:disabled {
150
151 /* Settings modal shell */
152 .modal-inner.settings-modal {
153 + right: auto;
154 + bottom: auto;
155 + z-index: 2;
156 + display: flex;
157 + align-items: stretch;
158 + justify-content: flex-start;
159 width: min(92vw, 1180px);
160 height: min(88vh, 900px);
161 + padding: 0;
162 + overflow: hidden;
163 + background: color-mix(in srgb, var(--color-background) 94%, #000 6%);
164 }
165
166 .modal-inner.settings-modal .modal-scroll,
@@ -209,6 +218,13 @@ select:disabled {
218 overflow-y: auto;
219 }
220
221 +.settings-nav-group {
222 + display: flex;
223 + flex-direction: column;
224 + gap: 3px;
225 + min-width: 0;
226 +}
227 +
228 .settings-tab {
229 display: grid;
230 grid-template-columns: 22px minmax(0, 1fr) auto;
@@ -217,8 +233,8 @@ select:disabled {
233 width: 100%;
234 min-height: 38px;
235 padding: 0 10px;
220 - border: 1px solid transparent;
221 - border-radius: 7px;
236 + border: 0;
237 + border-radius: 0;
238 appearance: none;
239 background: transparent;
240 color: var(--color-text);
@@ -227,9 +243,27 @@ select:disabled {
243 font-size: 0.83rem;
244 font-weight: 560;
245 line-height: 1.1;
246 + opacity: 0.68;
247 text-align: left;
248 text-decoration: none;
232 - transition: background-color 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
249 + transition: color 0.16s ease, opacity 0.16s ease;
250 +}
251 +
252 +.settings-tab-meta {
253 + display: inline-flex;
254 + align-items: center;
255 + justify-content: flex-end;
256 + gap: 6px;
257 + min-width: 18px;
258 +}
259 +
260 +.settings-tab-chevron {
261 + transform: rotate(-90deg);
262 + transition: transform 0.16s ease;
263 +}
264 +
265 +.settings-nav-group-active .settings-tab-chevron {
266 + transform: rotate(0deg);
267 }
268
269 .settings-tab-label {
@@ -239,34 +273,32 @@ select:disabled {
273 white-space: nowrap;
274 }
275
242 -.settings-tab .material-symbols-outlined {
276 +.settings-tab .material-symbols-outlined,
277 +.settings-section-link .material-symbols-outlined {
278 color: var(--color-text-muted);
279 font-size: 18px;
280 }
281
282 .settings-tab:hover,
248 -.settings-tab.active {
249 - border-color: color-mix(in srgb, var(--color-primary) 25%, var(--color-border));
250 - background: color-mix(in srgb, var(--color-background-hover) 56%, transparent);
283 +.settings-tab.active,
284 +.settings-section-link:hover,
285 +.settings-section-link.active {
286 opacity: 1;
287 }
288
254 -.settings-tab.active {
289 +.settings-tab.active,
290 +.settings-section-link.active {
291 color: var(--color-text);
292 font-weight: 750;
293 }
294
259 -.settings-tab.active .material-symbols-outlined {
295 +.settings-tab.active .material-symbols-outlined,
296 +.settings-section-link.active .material-symbols-outlined {
297 color: var(--color-primary);
298 }
299
300 .settings-tab-attention {
264 - border-color: color-mix(in srgb, #f2bf4b 42%, var(--color-border));
265 - background: linear-gradient(
266 - 90deg,
267 - color-mix(in srgb, #f2bf4b 13%, transparent),
268 - transparent 62%
269 - );
301 + opacity: 1;
302 }
303
304 .settings-tab-attention .material-symbols-outlined {
@@ -276,27 +308,49 @@ select:disabled {
308 .settings-attention-dot {
309 width: 7px;
310 height: 7px;
311 + flex: 0 0 auto;
312 border-radius: 999px;
313 background: #f2bf4b;
314 box-shadow: 0 0 0 3px color-mix(in srgb, #f2bf4b 18%, transparent);
315 }
316
284 -.settings-back-tab {
285 - margin-bottom: 4px;
286 - color: var(--color-text-muted);
287 -}
288 -
289 -.settings-tabs-heading {
290 - margin: 8px 8px 6px;
291 - color: var(--color-primary);
292 - font-size: 0.72rem;
293 - font-weight: 750;
294 - letter-spacing: 0;
295 - text-transform: uppercase;
317 +.settings-section-list {
318 + display: flex;
319 + flex-direction: column;
320 + gap: 2px;
321 + min-width: 0;
322 + margin: 0 0 6px 30px;
323 + padding-left: 10px;
324 + border-left: 1px solid color-mix(in srgb, var(--color-border) 54%, transparent);
325 }
326
327 .settings-section-link {
328 + display: grid;
329 + grid-template-columns: 20px minmax(0, 1fr) auto;
330 + align-items: center;
331 + gap: 7px;
332 + min-height: 32px;
333 + padding: 0 9px;
334 + border: 0;
335 + border-radius: 0;
336 + background: transparent;
337 color: var(--color-text);
338 + font-size: 0.78rem;
339 + font-weight: 560;
340 + line-height: 1.12;
341 + opacity: 0.72;
342 + text-align: left;
343 + text-decoration: none;
344 + transition: color 0.16s ease, opacity 0.16s ease;
345 +}
346 +
347 +.settings-section-link .material-symbols-outlined {
348 + font-size: 17px;
349 +}
350 +
351 +.settings-section-link:hover,
352 +.settings-section-link.active {
353 + opacity: 1;
354 }
355
356 .settings-pane {
@@ -599,25 +653,30 @@ select:disabled {
653 top: 0;
654 z-index: 3;
655 height: auto;
656 + max-height: min(42vh, 300px);
657 padding: 7px 8px;
603 - overflow-x: auto;
658 + overflow: hidden;
659 border-right: 0;
660 border-bottom: 1px solid color-mix(in srgb, var(--color-border) 58%, transparent);
661 }
662
663 .settings-tabs {
609 - flex-direction: row;
610 - width: max-content;
664 + flex-direction: column;
665 + width: 100%;
666 min-width: 100%;
612 - overflow-y: hidden;
667 + overflow-y: auto;
668 }
669
670 .settings-tab {
616 - width: auto;
617 - min-width: max-content;
671 + width: 100%;
672 + min-width: 0;
673 padding: 0 9px;
674 }
675
676 + .settings-section-list {
677 + margin-left: 28px;
678 + }
679 +
680 .settings-pane {
681 padding: 14px 16px 24px;
682 }
webui/js/modals.js
+3 -2
@@ -250,9 +250,10 @@ export async function openModal(modalPath, beforeClose = null) {
250 .then(async (doc) => {
251 // Set the title from the document
252 modal.title.innerHTML = doc.title || modalPath;
253 - if (doc.html && doc.html.classList) {
253 + const htmlElement = doc.documentElement;
254 + if (htmlElement && htmlElement.classList) {
255 const inner = modal.element.querySelector(".modal-inner");
255 - if (inner) inner.classList.add(...doc.html.classList);
256 + if (inner) inner.classList.add(...htmlElement.classList);
257 }
258 if (doc.body && doc.body.classList) {
259 modal.body.classList.add(...doc.body.classList);