From 59a17565294926aff3a88dacf18e614a7df015e1 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 8 Sep 2026 23:08:26 +0200 Subject: [PATCH] style(admin): lay the admin panel out in two columns and match the banner buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The User Defaults and Notifications tabs were one narrow column of cards on a page that has no max width, so the right two thirds stayed empty while the page scrolled for ages. Both now use the two-column grid the Settings tab already has, grouped by subject rather than by height. User Defaults splits into "Default User Settings" (colour mode, units, time format, currency, booking codes) and a "Map" card holding template, keys, routing base, preview and engine. The columns are deliberately uneven — the left one never needs more than its widest option row, the right one holds four fields whose values are 60-character tile URLs. Notifications keeps the three channel switches together on the left, because they all write the same notification_channels list, and puts the operator's own targets on the right. The admin preference matrix moves in with them instead of running full width underneath: it decides which of those channels each admin-only event goes out over. Its label column gets minmax(0,1fr) so it wraps rather than overflowing in the narrower column. The transit provider was the one native { - const next = e.target.value === 'google' ? 'google' : 'transitous' + onChange={async value => { + // A native select stayed silent when the option already selected was + // picked again; this one reports every pick, and a no-op PUT is still + // a write on an audited settings route. + if (value === transitProvider) return + const next = value === 'google' ? 'google' : 'transitous' const previous = transitProvider setTransitProviderState(next) try { @@ -565,11 +573,13 @@ export default function AdminSettingsTab({ admin, t }: AdminSettingsTabProps): R setTransitGoogleKeySource(saved.googleKeySource) } catch { setTransitProviderState(previous) } }} - className="mt-2 w-full px-3 py-2 border border-edge rounded-lg text-sm bg-surface-input text-content focus:ring-2 focus:ring-accent focus:border-transparent" - > - - - + options={[ + { value: 'transitous', label: t('admin.transitProvider.transitous') }, + { value: 'google', label: t('admin.transitProvider.google') }, + ]} + size="sm" + style={{ marginTop: 8 }} + />

{transitProvider === 'google' ? t('admin.transitProvider.googleHint') : t('admin.transitProvider.transitousHint')}

diff --git a/client/src/pages/admin/AdminUpdateBanner.tsx b/client/src/pages/admin/AdminUpdateBanner.tsx index 79c042d2c..eb20ad6c8 100644 --- a/client/src/pages/admin/AdminUpdateBanner.tsx +++ b/client/src/pages/admin/AdminUpdateBanner.tsx @@ -10,7 +10,7 @@ interface AdminUpdateBannerProps { } // The "new version available" banner shown at the top of the admin page. -// Purely presentational — extracted from AdminPage with identical markup. +// Purely presentational — extracted from AdminPage. export default function AdminUpdateBanner({ updateInfo, t, onHowTo }: AdminUpdateBannerProps): React.ReactElement { return (
@@ -25,23 +25,28 @@ export default function AdminUpdateBanner({ updateInfo, t, onHowTo }: AdminUpdat

+ {/* Both controls carry the same box metrics — padding, text size, icon, and a + border the filled one only needs to stop the outlined one from ending up + 2px shorter. Two buttons side by side read as one group, so a size gap + reads as a mistake rather than as hierarchy; the fill, the colour and the + weight are what mark the primary action. */}
{updateInfo.release_url && ( - + {t('admin.update.button')} )}