auth ui: prevent flash of manager-only controls on load

Anonymous visitors briefly saw the manager-only filters (Brand/Manager/Status),
checkboxes and appearance menu on refresh, because applyRoleUi() hides them only
after the async Keycloak check-sso resolves.

Fix: mark those elements .manager-only and hide them pre-paint via a .role-pending
class set on <html> by the inline head script, gated by a render-blocking CSS rule
(html.role-pending .manager-only { display:none }). applyRoleUi() drops
.role-pending once the role is known and then applies the final per-element state.
Default is now fail-closed (hidden until confirmed manager), so anonymous never
flashes them; managers see the controls appear a moment after load.
This commit is contained in:
aaverbitskiy 2026-08-14 11:29:51 +00:00
parent 56760fbcff
commit 341bc42778
3 changed files with 17 additions and 5 deletions

View File

@ -9,6 +9,10 @@
// hidden — force light regardless of stored/OS preference. To re-enable,
// restore the localStorage/prefers-color-scheme logic and unhide the toggle.
document.documentElement.setAttribute('data-theme', 'light');
// Role is unknown until Keycloak check-sso resolves (async). Start in the
// "pending" state that hides manager-only controls, so anonymous visitors
// never see a flash of them before applyRoleUi() runs.
document.documentElement.classList.add('role-pending');
</script>
</head>
<body>
@ -47,7 +51,7 @@
<div class="dropdown-panel" id="city-panel"></div>
</div>
</div>
<div class="field">
<div class="field manager-only">
<label>Бренд</label>
<div class="dropdown" id="brand-dropdown">
<button type="button" class="dropdown-btn" id="brand-btn">
@ -67,7 +71,7 @@
<div class="dropdown-panel" id="dimension-panel"></div>
</div>
</div>
<div class="field">
<div class="field manager-only">
<label>Менеджер</label>
<div class="dropdown" id="manager-dropdown">
<button type="button" class="dropdown-btn" id="manager-btn">
@ -85,7 +89,7 @@
<label for="date-end">Дата окончания (до)</label>
<input type="date" id="date-end" />
</div>
<div class="field">
<div class="field manager-only">
<label>Статус</label>
<div class="dropdown" id="status-dropdown">
<button type="button" class="dropdown-btn" id="status-btn">
@ -109,7 +113,7 @@
<input type="range" id="zoom-slider" min="0" max="9" step="1" value="3" />
</div>
</div>
<div class="checks">
<div class="checks manager-only">
<div class="field checkbox-field">
<label for="show-brand"><input type="checkbox" id="show-brand" checked /> Бренд</label>
</div>
@ -128,7 +132,7 @@
</div>
<!-- Appearance ("+") button, pinned to the header's bottom-right corner -->
<div class="decor-wrap">
<div class="decor-wrap manager-only">
<button type="button" class="decor-btn" id="decor-btn" aria-label="Оформление" data-tip="Оформление">+</button>
<div class="decor-menu" id="decor-menu"></div>
</div>

View File

@ -59,6 +59,9 @@ const el = {
// sense for the full-data (manager) tier so the anonymous view stays clean.
function applyRoleUi(): void {
const manager = isManager();
// Role resolved — leave the pre-paint "pending" state; from here explicit
// per-element display below is the source of truth.
document.documentElement.classList.remove('role-pending');
const hideField = (node: Element | null): void => {
const f = node?.closest('.field') as HTMLElement | null;
if (f) f.style.display = manager ? '' : 'none';

View File

@ -313,6 +313,11 @@ input::placeholder { color: var(--text-muted); }
}
.theme-toggle:hover { border-color: var(--border-strong); background: var(--surface); }
/* Anti-flash: until the role is resolved (Keycloak check-sso), hide every
manager-only control so anonymous visitors never see them flash on load.
applyRoleUi() drops .role-pending and then sets each element's final state. */
html.role-pending .manager-only { display: none !important; }
/* Login / logout button — pinned to the header's top-right corner */
.auth-wrap { position: absolute; right: 18px; top: 12px; z-index: 3; }