From 23c62fb1e6c62db015d729c350874d8f65c01916 Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Sat, 16 May 2026 20:01:48 +0200 Subject: [PATCH] fixed blinking --- .../src/components/react/admin/Editor.tsx | 14 +--- .../src/components/react/admin/Settings.tsx | 19 +---- frontend/src/lib/confirm.ts | 15 +++- frontend/src/styles/global.css | 77 ++++++++++++++++--- 4 files changed, 80 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/react/admin/Editor.tsx b/frontend/src/components/react/admin/Editor.tsx index 635c0f3..7c905cc 100644 --- a/frontend/src/components/react/admin/Editor.tsx +++ b/frontend/src/components/react/admin/Editor.tsx @@ -8,7 +8,7 @@ import { search, searchKeymap } from '@codemirror/search'; import { closeBrackets } from '@codemirror/autocomplete'; import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api'; import type { Asset } from '../../../lib/types'; -import { confirmDialog } from '../../../lib/confirm'; +import { confirmDialog, notify } from '../../../lib/confirm'; const AssetManager = lazy(() => import('./AssetManager')); @@ -90,7 +90,6 @@ export default function Editor({ editSlug }: Props) { const [tagsInput, setTagsInput] = useState(''); const [draft, setDraft] = useState(false); const [originalSlug, setOriginalSlug] = useState(editSlug || ''); - const [alert, setAlert] = useState<{ msg: string; type: 'success' | 'error' } | null>(null); const [showModal, setShowModal] = useState(false); const [showPreview, setShowPreview] = useState(false); const [mobileView, setMobileView] = useState<'edit' | 'preview'>('edit'); @@ -111,9 +110,7 @@ export default function Editor({ editSlug }: Props) { } function showAlertMsg(msg: string, type: 'success' | 'error') { - setAlert({ msg, type }); - window.scrollTo({ top: 0, behavior: 'smooth' }); - setTimeout(() => setAlert(null), 5000); + notify(msg, type); } const updatePreview = useCallback(async () => { @@ -410,13 +407,6 @@ export default function Editor({ editSlug }: Props) { return ( <> - {alert && ( -
- {alert.msg} -
- )} {/* Actions bar */}
diff --git a/frontend/src/components/react/admin/Settings.tsx b/frontend/src/components/react/admin/Settings.tsx index 5c391ee..894702c 100644 --- a/frontend/src/components/react/admin/Settings.tsx +++ b/frontend/src/components/react/admin/Settings.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { getConfig, updateConfig, ApiError } from '../../../lib/api'; +import { notify } from '../../../lib/confirm'; import type { SiteConfig, ContactLink } from '../../../lib/types'; const CONTACT_KINDS: { value: string; label: string; placeholder: string }[] = [ @@ -13,8 +14,6 @@ const CONTACT_KINDS: { value: string; label: string; placeholder: string }[] = [ export default function Settings() { const [config, setConfig] = useState>({}); - const [alert, setAlert] = useState<{ msg: string; type: 'success' | 'error' } | null>(null); - useEffect(() => { getConfig() .then(setConfig) @@ -22,8 +21,7 @@ export default function Settings() { }, []); function showAlert(msg: string, type: 'success' | 'error') { - setAlert({ msg, type }); - setTimeout(() => setAlert(null), 5000); + notify(msg, type); } function update(key: K, value: SiteConfig[K]) { @@ -62,19 +60,6 @@ export default function Settings() { return (
- {alert && ( -
- {alert.msg} -
- )} -

Identity

diff --git a/frontend/src/lib/confirm.ts b/frontend/src/lib/confirm.ts index a7f81aa..cb4dfb5 100644 --- a/frontend/src/lib/confirm.ts +++ b/frontend/src/lib/confirm.ts @@ -117,15 +117,22 @@ export function confirmDialog(opts: ConfirmOptions): Promise { }); } -/** Transient bottom-center toast. Replaces window.alert for failures. */ +/** Transient hovering toast at the top of the viewport. Replaces + * window.alert and the old inline save banners. */ export function notify(message: string, tone: 'error' | 'success' = 'error') { document.querySelector('.toast[data-notify]')?.remove(); const el = document.createElement('div'); - el.className = `toast${tone === 'error' ? ' toast--error' : ''}`; + el.className = `toast toast--${tone}`; el.dataset.notify = ''; el.setAttribute('role', tone === 'error' ? 'alert' : 'status'); el.textContent = message; - el.addEventListener('click', () => el.remove()); + + const dismiss = () => { + if (el.classList.contains('toast--out')) return; + el.classList.add('toast--out'); + window.setTimeout(() => el.remove(), 220); + }; + el.addEventListener('click', dismiss); document.body.appendChild(el); - window.setTimeout(() => el.remove(), 4500); + window.setTimeout(dismiss, 4500); } diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 16f38d3..e4b4c0e 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1360,7 +1360,7 @@ select.topbar-control.theme-select { /* Toast */ .toast { position: fixed; - bottom: 1.5rem; + top: 1.25rem; left: 50%; transform: translateX(-50%); background: var(--mantle); @@ -1368,17 +1368,34 @@ select.topbar-control.theme-select { color: var(--rosewater); padding: 0.65rem 1.1rem; border-radius: 1px; - box-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.45); + box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.55); font-family: var(--font-display); font-style: italic; font-size: 0.9rem; z-index: 200; - animation: toast-in 0.2s ease; + cursor: pointer; + animation: toast-in 0.22s cubic-bezier(0.2, 0.7, 0.2, 1); } @keyframes toast-in { - from { opacity: 0; transform: translate(-50%, 8px); } + from { opacity: 0; transform: translate(-50%, -10px); } to { opacity: 1; transform: translate(-50%, 0); } } +.toast--out { + animation: toast-out 0.2s ease forwards; +} +@keyframes toast-out { + from { opacity: 1; transform: translate(-50%, 0); } + to { opacity: 0; transform: translate(-50%, -10px); } +} +/* Success variant — parallels .toast--error. */ +.toast--success { + border-left: 3px solid var(--green); + color: var(--rosewater); +} +.toast--success::before { + content: "✓ "; + color: var(--green); +} /* Salon grid spans driven by --col-span custom prop (avoids Tailwind dynamic class issue). */ @media (min-width: 768px) { @@ -2023,7 +2040,7 @@ html.cybersigil body::after { 0 0 10px var(--sky), 0 -3px 0 color-mix(in srgb, var(--mauve) 70%, transparent), 0 3px 0 color-mix(in srgb, var(--teal) 60%, transparent); - animation: cs-tear 8.5s steps(1, jump-none) infinite; + animation: cs-tear 8.5s linear infinite; } .cybersigil .cs-fx-tear::after { content: ""; @@ -2046,7 +2063,7 @@ html.cybersigil body::after { -webkit-mask: var(--cs-corner) center / contain no-repeat; mask: var(--cs-corner) center / contain no-repeat; filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sky) 45%, transparent)); - animation: cs-flicker 7s steps(1, jump-none) infinite; + animation: cs-flicker 7s linear infinite; } .cybersigil .cs-fx-corner--tl { top: 0; left: 0; } .cybersigil .cs-fx-corner--tr { top: 0; right: 0; transform: scaleX(-1); animation-delay: -1.7s; } @@ -2118,7 +2135,8 @@ html.cybersigil body::after { /* Nameplate = the system handle. `> ` prompt + live block caret. */ .cybersigil .nameplate-title::before { - content: "> "; + content: ">"; + margin-right: 0.4em; color: var(--sky); -webkit-text-fill-color: var(--sky); } @@ -2131,7 +2149,7 @@ html.cybersigil body::after { vertical-align: -0.12em; background: var(--mauve); box-shadow: 0 0 8px color-mix(in srgb, var(--mauve) 70%, transparent); - animation: cs-blink 1.05s steps(1, jump-none) infinite; + animation: cs-blink 1.05s steps(2, jump-none) infinite; } .cybersigil .nameplate-subtitle { font-family: var(--font-sans); @@ -2535,7 +2553,7 @@ html.cybersigil body::after { .cybersigil .back-link::after { content: "_"; margin-left: -0.1em; - animation: cs-blink 1.05s steps(1, jump-none) infinite; + animation: cs-blink 1.05s steps(2, jump-none) infinite; } .cybersigil .back-link:hover, .cybersigil .back-link:focus-visible { @@ -2569,7 +2587,7 @@ html.cybersigil body::after { content: "_"; margin-left: 0.18em; opacity: 0.85; - animation: cs-blink 1.05s steps(1, jump-none) infinite; + animation: cs-blink 1.05s steps(2, jump-none) infinite; } /* Icon-only / collapsed controls have no room for the `>` prompt + blink * caret — they overflow the 2rem square on phones. Drop the pseudo when @@ -2845,7 +2863,7 @@ html.cybersigil body::after { content: "_"; color: var(--mauve); font-family: var(--font-sans); - animation: cs-blink 1.05s steps(1, jump-none) infinite; + animation: cs-blink 1.05s steps(2, jump-none) infinite; } .cybersigil .search-result [class*="line-clamp"] { font-family: var(--font-sans) !important; @@ -2916,7 +2934,7 @@ html.cybersigil body::after { .cybersigil .asset-drop-title::after { content: "_"; color: var(--mauve); - animation: cs-blink 1.05s steps(1, jump-none) infinite; + animation: cs-blink 1.05s steps(2, jump-none) infinite; } .cybersigil .asset-empty { @@ -3006,6 +3024,41 @@ html.cybersigil body::after { text-shadow: -1px 0 0 var(--sky), 1px 0 0 var(--mauve); } +/* Toast — a terminal status line printed at the top of the tube. */ +.cybersigil .toast { + background: color-mix(in srgb, var(--crust) 92%, transparent); + border: 1px solid var(--sky); + border-radius: 0; + color: var(--sky); + font-family: var(--font-sans); + font-style: normal; + font-size: 0.74rem; + letter-spacing: 0.12em; + text-transform: uppercase; + box-shadow: + 3px 3px 0 0 var(--mauve), + 0 0 22px -6px color-mix(in srgb, var(--sky) 45%, transparent); +} +.cybersigil .toast--success { + border-color: var(--sky); + color: var(--sky); +} +.cybersigil .toast--success::before { + content: "> OK\00a0\00a0"; + color: var(--sky); +} +.cybersigil .toast--error { + border-color: var(--red); + color: var(--red); + box-shadow: + 3px 3px 0 0 var(--mauve), + 0 0 22px -6px color-mix(in srgb, var(--red) 50%, transparent); +} +.cybersigil .toast--error::before { + content: "> ERR\00a0\00a0"; + color: var(--red); +} + /* ─── Theme keyframes ─── */ @keyframes cs-blink { 0%, 49% { opacity: 1; }