From f2daac4f9d294d7b60d954eaa0e6f430889c88e3 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 25 Jun 2026 11:22:34 +0200 Subject: [PATCH 1/6] fix(mobile-api): align settings page chrome with the standard admin settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Mobile API plugin settings page diverged from every other settings page: a sticky header bar with an indigo→purple gradient icon box and indigo/purple accents throughout, instead of the canonical admin chrome (plain text-3xl heading + flat blue icon, blue accents, .btn-primary). Now it matches /admin/settings and the sibling plugin settings pages (api-book-scraper, discogs): - header: drop the sticky bar + gradient icon, use the text-3xl heading with a flat fa-mobile-screen-button text-blue-600 icon and a .btn-secondary "Plugin" back link - recolor every indigo-* accent to blue-* (tabs, toggle, info box, selects, focus rings) - save button → .btn-primary; success/error banners → the canonical px-4 py-3 rounded-lg green/red style --- storage/plugins/mobile-api/views/settings.php | 76 ++++++++----------- 1 file changed, 33 insertions(+), 43 deletions(-) diff --git a/storage/plugins/mobile-api/views/settings.php b/storage/plugins/mobile-api/views/settings.php index 90db7f0e..2c18bf68 100644 --- a/storage/plugins/mobile-api/views/settings.php +++ b/storage/plugins/mobile-api/views/settings.php @@ -84,47 +84,38 @@ $tabDevicesUrl = htmlspecialchars(url('/admin/plugins/' . $resolvedId . '/settings') . '?tab=devices', ENT_QUOTES, 'UTF-8'); ?> -
+
- -
-
-
-
-
- -
-
-

- -

-

- -

-
-
- - - - -
+ +
+
+

+ + +

+

+ +

+ + + +
-
+
-
- -

+
+ +
- - 'bg-indigo-100 text-indigo-800','corporate'=>'bg-amber-100 text-amber-800','family'=>'bg-pink-100 text-pink-800']; ?> + 'bg-blue-100 text-blue-800','corporate'=>'bg-amber-100 text-amber-800','family'=>'bg-pink-100 text-pink-800']; ?>

diff --git a/storage/plugins/digital-library/views/admin-form-fields.php b/storage/plugins/digital-library/views/admin-form-fields.php index 96a9bb8b..8265305d 100644 --- a/storage/plugins/digital-library/views/admin-form-fields.php +++ b/storage/plugins/digital-library/views/admin-form-fields.php @@ -12,12 +12,12 @@ $currentAudioUrl = $book['audio_url'] ?? ''; ?> -

-

- +
+

+

-

+

@@ -221,7 +221,7 @@ class="btn btn-primary flex items-center justify-center gap-2 w-full md:w-auto"> const link = document.createElement('a'); link.href = displayLinkUrl; link.target = '_blank'; - link.className = 'text-xs text-purple-600 hover:underline'; + link.className = 'text-xs text-blue-600 hover:underline'; link.textContent = ; wrapper.appendChild(link); diff --git a/storage/plugins/discogs/views/settings.php b/storage/plugins/discogs/views/settings.php index afefb17a..5779136c 100644 --- a/storage/plugins/discogs/views/settings.php +++ b/storage/plugins/discogs/views/settings.php @@ -42,11 +42,11 @@ $pluginsRoute = htmlspecialchars(url('/admin/plugins'), ENT_QUOTES, 'UTF-8'); ?> -
+

- + Discogs -

@@ -89,14 +89,14 @@ -

+

-
+
- class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> + class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
@@ -62,7 +62,7 @@ class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500">
- class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> + class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
- class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> + class="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500">
@@ -94,7 +94,7 @@ class="w-4 h-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500"> name="anna_domain" list="anna_domain_options" value="" - class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-indigo-500 focus:border-indigo-500" + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-blue-500 focus:border-blue-500" autocomplete="off" spellcheck="false" maxlength="253"> @@ -111,7 +111,7 @@ class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-ind name="zlib_domain" list="zlib_domain_options" value="" - class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-indigo-500 focus:border-indigo-500" + class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-blue-500 focus:border-blue-500" autocomplete="off" spellcheck="false" maxlength="253"> diff --git a/storage/plugins/mobile-api/views/settings.php b/storage/plugins/mobile-api/views/settings.php index 2c18bf68..cd9f58e6 100644 --- a/storage/plugins/mobile-api/views/settings.php +++ b/storage/plugins/mobile-api/views/settings.php @@ -84,7 +84,7 @@ $tabDevicesUrl = htmlspecialchars(url('/admin/plugins/' . $resolvedId . '/settings') . '?tab=devices', ENT_QUOTES, 'UTF-8'); ?> -
+
@@ -145,7 +145,7 @@ class="pb-3 text-sm font-medium border-b-2 transition-colors "> -
+

@@ -181,7 +181,7 @@ class="underline">/api/v1/docs

-
+

@@ -232,7 +232,7 @@ class="block w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray

-
+

@@ -246,7 +246,7 @@ class="block w-full rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray -

+

diff --git a/storage/plugins/ncip-server/views/partners.php b/storage/plugins/ncip-server/views/partners.php index 2ffad5bb..74ad97ae 100644 --- a/storage/plugins/ncip-server/views/partners.php +++ b/storage/plugins/ncip-server/views/partners.php @@ -14,7 +14,7 @@
-
+
@@ -51,7 +51,7 @@ class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-700 text-whit

- +

@@ -64,7 +64,7 @@ class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-700 text-whit " - class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500"> + class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
+ class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
+ class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
+ class="w-full px-3 py-2 text-sm bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500">
diff --git a/storage/plugins/ncip-server/views/transactions.php b/storage/plugins/ncip-server/views/transactions.php index 607d6fb5..b7dd9dd0 100644 --- a/storage/plugins/ncip-server/views/transactions.php +++ b/storage/plugins/ncip-server/views/transactions.php @@ -79,7 +79,7 @@ class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-700 text-whit - + From bb299ffea5f5e1744cba86607b8e5347933e2ee0 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 25 Jun 2026 13:15:55 +0200 Subject: [PATCH 3/6] =?UTF-8?q?fix(ui):=20unify=20all=20on/off=20toggles?= =?UTF-8?q?=20to=20the=20canonical=20gray=E2=86=92dark=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every toggle now matches the catalogue-mode reference (gray w-11 h-6 track, white h-5 w-5 dot, #111827 when on, 20px slide): - settings API toggle: replaced the oversized OFF/ON black switch (peer + custom CSS) with the canonical toggle-checkbox/toggle-bg/toggle-dot markup; the generic .toggle-checkbox driver already on the settings page renders it, and the form auto-submit on change is preserved - events visibility toggle: restyled its CSS-only switch from 60x32 green to the canonical 44x24 dark dimensions/colors - bulk-enrich switch: on-state blue → gray-900 (markup + JS), matching the rest Rebuilt public/assets/main.css. --- app/Views/admin/bulk-enrich.php | 6 +++--- app/Views/events/index.php | 25 ++++++++++++------------ app/Views/settings/advanced-tab.php | 24 ++++------------------- public/assets/main.css | 30 ----------------------------- 4 files changed, 20 insertions(+), 65 deletions(-) diff --git a/app/Views/admin/bulk-enrich.php b/app/Views/admin/bulk-enrich.php index c72361ac..2b896cff 100644 --- a/app/Views/admin/bulk-enrich.php +++ b/app/Views/admin/bulk-enrich.php @@ -83,7 +83,7 @@

diff --git a/public/assets/main.css b/public/assets/main.css index 6144e611..e374468e 100644 --- a/public/assets/main.css +++ b/public/assets/main.css @@ -3827,9 +3827,6 @@ select { .left-0 { left: 0px; } -.left-0\.5 { - left: 0.125rem; -} .left-1\/2 { left: 50%; } @@ -3851,9 +3848,6 @@ select { .top-0 { top: 0px; } -.top-0\.5 { - top: 0.125rem; -} .top-1\/2 { top: 50%; } @@ -4709,9 +4703,6 @@ select { .border-2 { border-width: 2px; } -.border-4 { - border-width: 4px; -} .border-b { border-bottom-width: 1px; } @@ -5973,11 +5964,6 @@ select { --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.shadow-inner { - --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} .shadow-lg { --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); @@ -7673,22 +7659,6 @@ table.dataTable thead td { .group:hover .group-hover\:text-primary { color: var(--color-primary); } -.peer:checked ~ .peer-checked\:translate-x-9 { - --tw-translate-x: 2.25rem; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} -.peer:checked ~ .peer-checked\:bg-gray-900 { - --tw-bg-opacity: 1; - background-color: rgb(17 24 39 / var(--tw-bg-opacity, 1)); -} -.peer:checked ~ .peer-checked\:bg-white { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); -} -.peer:checked ~ .peer-checked\:text-gray-900 { - --tw-text-opacity: 1; - color: rgb(17 24 39 / var(--tw-text-opacity, 1)); -} .dark\:divide-gray-700:is(.dark *) > :not([hidden]) ~ :not([hidden]) { --tw-divide-opacity: 1; border-color: rgb(55 65 81 / var(--tw-divide-opacity, 1)); From 50807e86372100a94f64ecdfab1be8c3dbadb470 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 25 Jun 2026 14:38:04 +0200 Subject: [PATCH 4/6] fix(ui): unify advanced-tab section headers + add toggles E2E regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - advanced-tab: convert the two odd-one-out section headers (Sitemap XML, API Pubblica) from the icon-in-box + text-lg style to the canonical inline-icon text-xl used by every other settings section, so titles match in size and share the same left edge on mobile (added max-sm:!px-0/!border-0 so the header goes flush like the body). The API section stays collapsible (toggleApiSection + chevron preserved) - new tests/toggles-all.spec.js: turns every restyled toggle ON and asserts it reflects ON — shared .toggle-checkbox driver paints the track dark, the API toggle auto-submits and persists, privacy toggles persist on save, the events visibility toggle flips, and the bulk-enrich AJAX switch flips. Restores each toggle so the run is idempotent --- app/Views/settings/advanced-tab.php | 34 +++--- tests/toggles-all.spec.js | 176 ++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+), 20 deletions(-) create mode 100644 tests/toggles-all.spec.js diff --git a/app/Views/settings/advanced-tab.php b/app/Views/settings/advanced-tab.php index d0516a6a..3dbe08d7 100644 --- a/app/Views/settings/advanced-tab.php +++ b/app/Views/settings/advanced-tab.php @@ -544,16 +544,12 @@ class="toggle-checkbox sr-only">
-
-
- - - -
-

-

-
-
+
+

+ + +

+

@@ -687,16 +683,14 @@ class="inline-flex items-center gap-2 px-5 py-3 rounded-xl bg-gray-900 text-whit ?>
-
-
-
- - - -
-

-

-
+
+
+
+

+ + +

+

diff --git a/tests/toggles-all.spec.js b/tests/toggles-all.spec.js new file mode 100644 index 00000000..07682d47 --- /dev/null +++ b/tests/toggles-all.spec.js @@ -0,0 +1,176 @@ +// @ts-check +// Regression for the toggle restyle (canonical gray→dark .toggle-checkbox switch, +// the auto-submit API toggle, the events visibility toggle, and the bulk-enrich +// switch). Turns each toggle ON and asserts it actually reflects the ON state — +// the shared .toggle-checkbox driver paints the track dark, forms auto-submit, +// and the AJAX switch flips. Restores every toggle so the run is idempotent. +const { test, expect } = require('@playwright/test'); + +const BASE = process.env.E2E_BASE_URL || 'http://localhost:8081'; +const ADMIN_EMAIL = process.env.E2E_ADMIN_EMAIL || ''; +const ADMIN_PASS = process.env.E2E_ADMIN_PASS || ''; + +const ON_BG = 'rgb(17, 24, 39)'; // #111827 — canonical "on" track colour +const OFF_BG = 'rgb(229, 231, 235)'; // #e5e7eb — canonical "off" track colour + +test.skip( + !ADMIN_EMAIL || !ADMIN_PASS, + 'E2E credentials not configured (set E2E_ADMIN_EMAIL, E2E_ADMIN_PASS)', +); + +/** + * Inline background-color the shared driver writes onto a canonical toggle's + * track (input's next sibling). Read the inline style, not the computed one: + * `transition-colors` animates the computed value over 300ms, so the computed + * read lags; the inline value is set synchronously by the driver. + */ +async function trackBg(page, id) { + return page.evaluate((tid) => { + const input = document.getElementById(tid); + if (!input || !input.nextElementSibling) return null; + return input.nextElementSibling.style.backgroundColor; + }, id); +} + +/** + * Flip a canonical sr-only .toggle-checkbox. The visible track/dot overlap and + * intercept pointer events on a 1px sr-only input, so set `checked` and dispatch + * `change` directly — the exact event a real click fires, which the shared + * driver (and any onchange handler) listens for. + */ +async function setToggle(page, id, on) { + const input = page.locator(`#${id}`); + if ((await input.isChecked()) !== on) { + await input.evaluate((el, val) => { + el.checked = val; + el.dispatchEvent(new Event('change', { bubbles: true })); + }, on); + } + await expect(input).toBeChecked({ checked: on }); +} + +/** Flip a toggle whose onchange auto-submits a form; waits for the navigation. */ +async function flipAndSubmit(page, id, on) { + if ((await page.locator(`#${id}`).isChecked()) === on) return; + await Promise.all([ + page.waitForNavigation({ timeout: 15000 }), + page.locator(`#${id}`).evaluate((el, val) => { + el.checked = val; + el.dispatchEvent(new Event('change', { bubbles: true })); + }, on), + ]); +} + +test.describe.serial('All toggles turn ON after the restyle', () => { + /** @type {import('@playwright/test').BrowserContext} */ + let context; + /** @type {import('@playwright/test').Page} */ + let page; + + test.beforeAll(async ({ browser }) => { + context = await browser.newContext(); + page = await context.newPage(); + await page.goto(`${BASE}/accedi`); + await page.fill('input[name="email"]', ADMIN_EMAIL); + await page.fill('input[name="password"]', ADMIN_PASS); + await page.locator('button[type="submit"]').click(); + await page.waitForURL(/admin/, { timeout: 15000 }); + }); + + test.afterAll(async () => { + await context.close(); + }); + + // The shared .toggle-checkbox driver: turning a toggle on must paint the track + // dark. Verified on every advanced-form toggle WITHOUT submitting (force_https + // would redirect the HTTP test to HTTPS). State is reset before leaving. + test('Advanced: every .toggle-checkbox reflects ON via the shared driver', async () => { + await page.goto(`${BASE}/admin/settings?tab=advanced`); + // force_https / private_mode are plain checkboxes, not switch toggles — excluded. + const ids = ['llms_txt_enabled', 'catalogue_mode']; + for (const id of ids) { + const before = await page.locator(`#${id}`).isChecked(); + await setToggle(page, id, true); + expect(await trackBg(page, id)).toBe(ON_BG); + await setToggle(page, id, false); + expect(await trackBg(page, id)).toBe(OFF_BG); + // leave as found + if (before) await setToggle(page, id, true); + } + }); + + // The API toggle auto-submits its own form on change and the new state must + // survive the reload. + test('Advanced: API toggle auto-submits and persists ON', async () => { + await page.goto(`${BASE}/admin/settings?tab=advanced`); + const wasOn = await page.locator('#api_enabled').isChecked(); + + await flipAndSubmit(page, 'api_enabled', true); + await page.goto(`${BASE}/admin/settings?tab=advanced`); + await expect(page.locator('#api_enabled')).toBeChecked(); + expect(await trackBg(page, 'api_enabled')).toBe(ON_BG); + + // Restore original state + await flipAndSubmit(page, 'api_enabled', wasOn); + }); + + // Privacy toggles persist via the "Salva Privacy Policy" form submit. + test('Privacy: cookie/analytics/marketing toggles persist ON', async () => { + await page.goto(`${BASE}/admin/settings?tab=privacy`); + const ids = ['cookie_banner_enabled', 'show_analytics', 'show_marketing']; + const before = {}; + for (const id of ids) before[id] = await page.locator(`#${id}`).isChecked(); + + for (const id of ids) await setToggle(page, id, true); + await page.locator('button[type="submit"]:has-text("Salva Privacy")').first().click(); + await expect(page.locator('.bg-green-50, .swal2-icon-success').first()) + .toBeVisible({ timeout: 10000 }); + + await page.goto(`${BASE}/admin/settings?tab=privacy`); + for (const id of ids) await expect(page.locator(`#${id}`)).toBeChecked(); + + // Restore original states + for (const id of ids) await setToggle(page, id, before[id]); + await page.locator('button[type="submit"]:has-text("Salva Privacy")').first().click(); + await expect(page.locator('.bg-green-50, .swal2-icon-success').first()) + .toBeVisible({ timeout: 10000 }); + }); + + // Per-event visibility toggle (auto-submits). Skips when no event exists. + test('Events: visibility toggle flips', async () => { + await page.goto(`${BASE}/admin/cms/events`); + const first = () => page.locator('input.toggle-checkbox').first(); + if ((await first().count()) === 0) { + test.skip(true, 'No events to toggle'); + return; + } + const before = await first().isChecked(); + const flip = () => Promise.all([ + page.waitForNavigation({ timeout: 15000 }), + first().evaluate((el) => { + el.checked = !el.checked; + el.dispatchEvent(new Event('change', { bubbles: true })); + }), + ]); + await flip(); + await page.goto(`${BASE}/admin/cms/events`); + expect(await first().isChecked()).toBe(!before); + await flip(); // restore + }); + + // Bulk-enrich is a button[role=switch] flipped via AJAX. Skips when absent. + test('Bulk-enrich: switch turns ON', async () => { + await page.goto(`${BASE}/admin/books/bulk-enrich`); + const sw = page.locator('#toggle-enrichment'); + if ((await sw.count()) === 0) { + test.skip(true, 'No bulk-enrich toggle on this page'); + return; + } + const wasOn = (await sw.getAttribute('aria-checked')) === 'true'; + await sw.click(); + await expect(sw).toHaveAttribute('aria-checked', String(!wasOn), { timeout: 10000 }); + // Restore + await sw.click(); + await expect(sw).toHaveAttribute('aria-checked', String(wasOn), { timeout: 10000 }); + }); +}); From c2796c4a89b85972877b31b423084e40eba5fdc8 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 25 Jun 2026 15:06:24 +0200 Subject: [PATCH 5/6] fix(mobile): lock body scroll while the mobile sidebar/overlay is open Opening the mobile sidebar showed the overlay but the content behind it (e.g. the settings page) still scrolled. `overflow:hidden` on alone does not stop touch-drag scrolling on iOS Safari, so pin the body with position:fixed at the captured scroll offset while the menu is open, and restore the exact scroll position on close. Guarded so closes that never locked (desktop, nav-link clicks) don't jump the page to the top. --- app/Views/layout.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/Views/layout.php b/app/Views/layout.php index 25da22ff..53620f79 100644 --- a/app/Views/layout.php +++ b/app/Views/layout.php @@ -996,19 +996,40 @@ function initializeMobileMenu() { const closeMobileMenuButton = document.getElementById('close-mobile-menu'); const mobileMenuOverlay = document.getElementById('mobile-menu-overlay'); const sidebar = document.getElementById('sidebar'); + let scrollLockY = 0; function openMobileMenu() { sidebar.classList.remove('-translate-x-full'); sidebar.classList.add('translate-x-0'); mobileMenuOverlay.classList.remove('hidden'); + // Lock the page behind the overlay. `overflow:hidden` on alone is + // not enough on iOS Safari — touch-drag still scrolls the content under + // the overlay — so pin the body in place and restore on close. + scrollLockY = window.scrollY || document.documentElement.scrollTop || 0; document.body.classList.add('overflow-hidden'); + document.body.style.position = 'fixed'; + document.body.style.top = `-${scrollLockY}px`; + document.body.style.left = '0'; + document.body.style.right = '0'; + document.body.style.width = '100%'; } function closeMobileMenu() { sidebar.classList.add('-translate-x-full'); sidebar.classList.remove('translate-x-0'); mobileMenuOverlay.classList.add('hidden'); + const wasLocked = document.body.style.position === 'fixed'; document.body.classList.remove('overflow-hidden'); + document.body.style.position = ''; + document.body.style.top = ''; + document.body.style.left = ''; + document.body.style.right = ''; + document.body.style.width = ''; + // Only restore scroll when we actually locked (avoid jumping to top when + // close fires on desktop / nav-link clicks where no lock was applied). + if (wasLocked) { + window.scrollTo(0, scrollLockY); + } } if (mobileMenuButton) { From fb05cc765b7349e5edfa2ab49467f24d1d9944a3 Mon Sep 17 00:00:00 2001 From: fabiodalez-dev Date: Thu, 25 Jun 2026 15:14:16 +0200 Subject: [PATCH 6/6] fix(ui): center the sidebar quick-stats numbers and labels The "Libri"/"Prestiti" stat cards in the sidebar had their number and label left-aligned; center them inside each column (text-center). --- app/Views/layout.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Views/layout.php b/app/Views/layout.php index 53620f79..a721fafd 100644 --- a/app/Views/layout.php +++ b/app/Views/layout.php @@ -447,12 +447,12 @@ class="flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 group-hov
-
+
-
-
+
-