Dialog jako jedna vrstva, hlavicka bez rozmazani, znacka Zive odstranena

Otevreny dialog sekal video vedle portalu. Posledni rozmazana vrstva na
dashboardu byla lepici hlavicka (backdrop-blur-xl); pod prekryvem dialogu
ji prohlizec prepocitaval pri kazdem prekresleni (blikajici kurzor,
kolecko obnovy dat). Hlavicka ma plnou barvu, dialog je jeden fixed prekryv
s vycentrovanym panelem. Tecka a znacka Zive odstraneny z dashboardu
i z ukazkoveho panelu na webu, duplicitni blok prefers-reduced-motion
v index.css sloucen. Dokumentace 05, 22, 99.
This commit is contained in:
JiriUhlir
2026-09-09 20:55:13 +02:00
parent ea9387bea9
commit 5d41600578
10 changed files with 73 additions and 100 deletions
@@ -24,7 +24,6 @@ import { ErrorBoundary } from '@/components/ErrorBoundary';
import { useAuth, type AuthUser } from '@/auth/AuthContext';
import { EventStreamProvider } from '@/components/dashboard/EventStreamProvider';
import { EventToasts } from '@/components/dashboard/EventToasts';
import { LiveIndicator } from '@/components/dashboard/LiveIndicator';
import { Logo } from '@/components/layout/Logo';
import { cn } from '@/lib/cn';
import { CollectionsProvider, useAccess } from '@/lib/collections';
@@ -287,8 +286,14 @@ function DashboardShell() {
<div className="lg:pl-64">
<ImpersonationBanner />
{/* Topbar */}
<header className="sticky top-0 z-30 flex h-18 items-center gap-4 border-b border-ink-600/60 bg-ink-900/85 px-5 backdrop-blur-xl sm:px-7">
{/*
Topbar. Plna barva, zadne `backdrop-blur`. Rozmazana hlavicka pres
celou sirku byla posledni na dashboardu a pod otevrenym dialogem
(prekryv pres cely viewport lezi nad ni) prohlizec rozmazani
prepocital pri kazdem prekresleni pod prekryvem - blikajici kurzor
v poli dialogu stacil, aby vedle portalu sekalo video.
*/}
<header className="sticky top-0 z-30 flex h-18 items-center gap-4 border-b border-ink-600/60 bg-ink-900 px-5 sm:px-7">
<button
type="button"
onClick={() => setSidebarPath(location.pathname)}
@@ -311,8 +316,6 @@ function DashboardShell() {
<p className="truncate text-xs text-white/40">{accessLabel(user, access.data)}</p>
</div>
<LiveIndicator className="hidden sm:inline-flex" />
<div className="flex items-center gap-3">
<div className="hidden text-right sm:block">
<p className="text-sm font-medium text-white">{user?.name}</p>
@@ -1,46 +0,0 @@
import { useEventStream } from '@/components/dashboard/EventStreamProvider';
import { cn } from '@/lib/cn';
/*
* Pulz jen kdyz se neco deje (pripojuji, obnovuji). Stav "Zive" je vychozi
* a trva hodiny; nekonecna animace v hlavicce s `backdrop-blur` nutila
* prohlizec prepocitavat rozmazani porad dokola, a s otevrenym dialogem
* (dalsi vrstva pres celou obrazovku) to sekalo video vedle portalu.
*/
const meta = {
connecting: { label: 'Připojuji', dot: 'bg-warn-400', text: 'text-warn-400', pulse: true },
open: { label: 'Živě', dot: 'bg-ok-400', text: 'text-ok-400', pulse: false },
reconnecting: {
label: 'Obnovuji spojení',
dot: 'bg-warn-400',
text: 'text-warn-400',
pulse: true,
},
closed: { label: 'Odpojeno', dot: 'bg-white/40', text: 'text-white/45', pulse: false },
} as const;
/** Stav spojeni se streamem. Uzivatel musi poznat, ze data nejsou zive. */
export function LiveIndicator({ className }: { className?: string }) {
const { status } = useEventStream();
const current = meta[status];
return (
<span
className={cn(
'inline-flex items-center gap-1.5 rounded-full border border-ink-600/70 px-2.5 py-1 text-xs font-medium',
current.text,
className,
)}
title={
status === 'open'
? 'Dashboard dostává změny okamžitě, bez obnovování stránky.'
: 'Změny se teď nemusí projevovat okamžitě.'
}
>
<span
className={cn('size-1.5 rounded-full', current.dot, current.pulse && 'animate-pulse')}
/>
{current.label}
</span>
);
}
+1 -5
View File
@@ -270,11 +270,7 @@ function HeroPanel() {
<p className="truncate text-xs font-semibold text-white">Acme s.r.o.</p>
<p className="truncate text-[0.65rem] text-white/40">{t('panel.role')}</p>
</div>
<span className="ml-auto inline-flex items-center gap-1.5 rounded-full border border-ok-400/35 bg-ok-500/12 px-2 py-0.5 text-[0.65rem] text-ok-400">
<span className="size-1.5 rounded-full bg-ok-400" />
{t('panel.live')}
</span>
<span className="grid size-7 shrink-0 place-items-center rounded-xl bg-brand-500/20 text-[0.6rem] font-bold text-brand-200">
<span className="ml-auto grid size-7 shrink-0 place-items-center rounded-xl bg-brand-500/20 text-[0.6rem] font-bold text-brand-200">
JK
</span>
</div>
+20 -15
View File
@@ -10,10 +10,13 @@ import { cn } from '@/lib/cn';
* Pri otevreni zamkne scroll stranky a preda focus dovnitr.
*
* **Vykresluje se portalem do `document.body`, ne na miste volani.** Samo
* `position: fixed` nestaci: rodic s `backdrop-filter` (nase `.glass`, tedy
* skoro kazdy panel a karta) je pro fixed potomka containing block a dialog
* se pak vejde do te karty misto pres celou obrazovku. Bez portalu je tedy
* dialog v karte konektoru maly a nepouzitelny.
* `position: fixed` nestaci: rodic s `transform`, `filter` nebo
* `backdrop-filter` je pro fixed potomka containing block a dialog se pak
* vejde do te karty misto pres celou obrazovku. Bez portalu byl dialog
* v karte konektoru maly a nepouzitelny.
*
* Prekryv nema rozmazani a nic pod nim ho mit nesmi: rozmazana vrstva pod
* prekryvem se prepocitava pri kazdem prekresleni (i blikajici kurzor).
*/
export function Modal({
open,
@@ -73,22 +76,24 @@ export function Modal({
if (!open) return null;
return createPortal(
<div className="fixed inset-0 z-[60] flex items-start justify-center overflow-y-auto p-4 sm:items-center sm:p-6">
{/*
Prekryv bez rozmazani, schvalne. `backdrop-blur` pres celou obrazovku
znamena, ze kazda animace pod dialogem (pulzujici tecka, spinner,
obnova seznamu) prepocita rozmazani cele plochy, a to jeste pres
rozmazanou hlavicku pod nim. Vedle portalu pak sekalo video. Tmavy
prekryv s vyssi krytim dela tutez praci zadarmo.
*/}
<div className="fixed inset-0 bg-ink-950/85" onClick={onClose} aria-hidden />
/*
* Jedna vrstva: tmavy prekryv pres celou obrazovku, uprostred panel.
* Zadne rozmazani, zadna animace. Klik mimo panel zavira.
* `m-auto` na panelu ho vycentruje a u vysokeho dialogu necha horni
* okraj dosazitelny posuvnikem prekryvu.
*/
<div
className="fixed inset-0 z-[60] flex h-screen w-screen overflow-y-auto bg-ink-950/85 p-4 sm:p-6"
onClick={(event) => {
if (event.target === event.currentTarget) onClose();
}}
>
<div
role="dialog"
aria-modal="true"
aria-label={title}
className={cn(
'relative my-auto w-full max-w-3xl rounded-2xl border border-ink-600/70 bg-ink-900 shadow-2xl',
'm-auto w-full max-w-3xl rounded-2xl border border-ink-600/70 bg-ink-900 shadow-2xl',
className,
)}
>
-1
View File
@@ -77,7 +77,6 @@ export const cs = {
'panel.ago24min': 'před 24 min',
'panel.ago1h': 'před 1 hod',
'panel.ago3h': 'před 3 hod',
'panel.live': 'Živě',
// ------------------------------------------------------------ reference
'logos.title': 'Pracujeme pro firmy, které nemají čas na ruční práci',
-1
View File
@@ -77,7 +77,6 @@ export const en: Partial<Record<MessageKey, string>> = {
'panel.ago24min': '24 min ago',
'panel.ago1h': '1 hour ago',
'panel.ago3h': '3 hours ago',
'panel.live': 'Live',
// ------------------------------------------------------------ reference
'logos.title': 'Trusted by teams with no time for busywork',
+5 -12
View File
@@ -183,8 +183,10 @@
* vrstvu ve skladaci. Pri padesati kartach a zivem streamu udalosti to
* vytizilo GPU natolik, ze vedle toho sekalo prehravani videa.
*
* Rozmazani zustava tam, kde neco opravdu prekryva: lepici hlavicka
* a podklady modalu maji svoje `backdrop-blur-*` primo u sebe.
* Na dashboardu uz `backdrop-filter` nema nic: pod otevrenym dialogem
* (prekryv pres cely viewport) se kazda rozmazana vrstva prepocitava pri
* kazdem prekresleni, i kdyz jen blika kurzor. Jedine rozmazani zustalo
* v hlavicce verejneho webu, kde neni zadny stream ani dialog.
*/
@utility glass {
background-color: --alpha(var(--color-ink-800) / 70%);
@@ -225,13 +227,4 @@
*
* Nekonecne animace jsou to jedine, co na klidne strance porad zamestnava
* skladac - a prave ty jsou tady zbytne.
*/
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
*/