fix(redesign): portal overlays + solid popup surfaces for legibility

Portal EntitySelect/MultiEntitySelect/Modal/Snackbar/EventChart/Hint to
<body> so they escape backdrop-filter ancestors. Replace translucent
glass on popups (IconPicker, IconGridSelect, SearchPalette, Snackbar)
with solid backgrounds and theme-aware light-mode override.
This commit is contained in:
2026-04-25 11:41:25 +03:00
parent 9643fe519e
commit d356e5a3ac
9 changed files with 396 additions and 229 deletions
@@ -1,6 +1,7 @@
<script lang="ts">
import MdiIcon from './MdiIcon.svelte';
import { t } from '$lib/i18n';
import { portal } from '$lib/portal';
export interface MultiEntityItem {
value: string;
@@ -110,56 +111,58 @@
</button>
</div>
<!-- Palette overlay -->
<!-- Palette overlay — portalled to <body> to escape backdrop-filter ancestors -->
{#if open}
<div class="mes-overlay" onclick={closePalette} role="presentation"></div>
<div use:portal class="mes-portal-root">
<div class="mes-overlay" onclick={closePalette} role="presentation"></div>
<div class="mes-container">
<div class="mes-search-row">
<MdiIcon name="mdiMagnify" size={18} />
<input
bind:this={inputEl}
bind:value={query}
placeholder={t('common.search')}
class="mes-input"
type="text"
autocomplete="off"
spellcheck="false"
onkeydown={handleKeydown}
/>
<span class="mes-count">{(values || []).length}/{items.length}</span>
<kbd class="mes-kbd">ESC</kbd>
</div>
<div class="mes-container">
<div class="mes-search-row">
<MdiIcon name="mdiMagnify" size={18} />
<input
bind:this={inputEl}
bind:value={query}
placeholder={t('common.search')}
class="mes-input"
type="text"
autocomplete="off"
spellcheck="false"
onkeydown={handleKeydown}
/>
<span class="mes-count">{(values || []).length}/{items.length}</span>
<kbd class="mes-kbd">ESC</kbd>
</div>
<div class="mes-list" bind:this={listEl} role="listbox">
{#if filtered.length === 0}
<div class="mes-empty">{t('common.noMatches')}</div>
{:else}
{#each filtered as item, i}
{@const checked = (values || []).includes(item.value)}
<button
class="mes-item"
class:mes-highlight={i === highlightIdx}
class:mes-checked={checked}
role="option"
aria-selected={checked}
onclick={() => toggleItem(item)}
onmouseenter={() => highlightIdx = i}
type="button"
>
<span class="mes-item-check">
<MdiIcon name={checked ? 'mdiCheckboxMarked' : 'mdiCheckboxBlankOutline'} size={16} />
</span>
{#if item.icon}
<span class="mes-item-icon"><MdiIcon name={item.icon} size={18} /></span>
{/if}
<span class="mes-item-label">{item.label}</span>
{#if item.desc}
<span class="mes-item-desc">{item.desc}</span>
{/if}
</button>
{/each}
{/if}
<div class="mes-list" bind:this={listEl} role="listbox">
{#if filtered.length === 0}
<div class="mes-empty">{t('common.noMatches')}</div>
{:else}
{#each filtered as item, i}
{@const checked = (values || []).includes(item.value)}
<button
class="mes-item"
class:mes-highlight={i === highlightIdx}
class:mes-checked={checked}
role="option"
aria-selected={checked}
onclick={() => toggleItem(item)}
onmouseenter={() => highlightIdx = i}
type="button"
>
<span class="mes-item-check">
<MdiIcon name={checked ? 'mdiCheckboxMarked' : 'mdiCheckboxBlankOutline'} size={16} />
</span>
{#if item.icon}
<span class="mes-item-icon"><MdiIcon name={item.icon} size={18} /></span>
{/if}
<span class="mes-item-label">{item.label}</span>
{#if item.desc}
<span class="mes-item-desc">{item.desc}</span>
{/if}
</button>
{/each}
{/if}
</div>
</div>
</div>
{/if}
@@ -233,32 +236,42 @@
flex-shrink: 0;
}
/* Overlay */
.mes-overlay {
/* Portal root */
.mes-portal-root {
position: fixed;
inset: 0;
z-index: 9998;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
pointer-events: none;
}
.mes-overlay {
position: absolute;
inset: 0;
pointer-events: auto;
background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(8px) saturate(120%);
-webkit-backdrop-filter: blur(8px) saturate(120%);
}
/* Palette container */
/* Palette container — solid background for legibility */
.mes-container {
position: fixed;
pointer-events: auto;
position: absolute;
top: min(20vh, 120px);
left: 50%;
transform: translateX(-50%);
z-index: 9999;
width: min(460px, 90vw);
z-index: 1;
width: min(480px, 92vw);
max-height: 60vh;
background: var(--color-card);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
background: var(--mes-solid-bg);
border: 1px solid var(--color-rule-strong);
border-radius: 16px;
box-shadow: var(--shadow-card), 0 24px 48px -16px rgba(0, 0, 0, 0.55);
display: flex;
flex-direction: column;
overflow: hidden;
--mes-solid-bg: #131520;
}
:global([data-theme="light"]) .mes-container { --mes-solid-bg: #fafafe; }
.mes-search-row {
display: flex;
@@ -319,7 +332,11 @@
transition: background 0.1s;
}
.mes-item:hover, .mes-item.mes-highlight {
background: var(--color-muted);
background: rgba(255, 255, 255, 0.06);
}
:global([data-theme="light"]) .mes-item:hover,
:global([data-theme="light"]) .mes-item.mes-highlight {
background: rgba(20, 15, 60, 0.05);
}
.mes-item-check {
flex-shrink: 0;