fixed blinking
This commit is contained in:
@@ -8,7 +8,7 @@ import { search, searchKeymap } from '@codemirror/search';
|
|||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api';
|
import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api';
|
||||||
import type { Asset } from '../../../lib/types';
|
import type { Asset } from '../../../lib/types';
|
||||||
import { confirmDialog } from '../../../lib/confirm';
|
import { confirmDialog, notify } from '../../../lib/confirm';
|
||||||
|
|
||||||
const AssetManager = lazy(() => import('./AssetManager'));
|
const AssetManager = lazy(() => import('./AssetManager'));
|
||||||
|
|
||||||
@@ -90,7 +90,6 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
const [tagsInput, setTagsInput] = useState('');
|
const [tagsInput, setTagsInput] = useState('');
|
||||||
const [draft, setDraft] = useState(false);
|
const [draft, setDraft] = useState(false);
|
||||||
const [originalSlug, setOriginalSlug] = useState(editSlug || '');
|
const [originalSlug, setOriginalSlug] = useState(editSlug || '');
|
||||||
const [alert, setAlert] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
|
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const [showPreview, setShowPreview] = useState(false);
|
const [showPreview, setShowPreview] = useState(false);
|
||||||
const [mobileView, setMobileView] = useState<'edit' | 'preview'>('edit');
|
const [mobileView, setMobileView] = useState<'edit' | 'preview'>('edit');
|
||||||
@@ -111,9 +110,7 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showAlertMsg(msg: string, type: 'success' | 'error') {
|
function showAlertMsg(msg: string, type: 'success' | 'error') {
|
||||||
setAlert({ msg, type });
|
notify(msg, type);
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
setTimeout(() => setAlert(null), 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatePreview = useCallback(async () => {
|
const updatePreview = useCallback(async () => {
|
||||||
@@ -410,13 +407,6 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{alert && (
|
|
||||||
<div className={`p-4 rounded-lg mb-6 text-sm font-semibold text-center backdrop-blur-sm shadow-lg ${
|
|
||||||
alert.type === 'success' ? 'bg-green/15 border border-green/30' : 'bg-red/15 border border-red/30'
|
|
||||||
}`} style={{ color: 'var(--text)' }}>
|
|
||||||
{alert.msg}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions bar */}
|
{/* Actions bar */}
|
||||||
<div className="flex flex-wrap gap-4 mb-6">
|
<div className="flex flex-wrap gap-4 mb-6">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { getConfig, updateConfig, ApiError } from '../../../lib/api';
|
import { getConfig, updateConfig, ApiError } from '../../../lib/api';
|
||||||
|
import { notify } from '../../../lib/confirm';
|
||||||
import type { SiteConfig, ContactLink } from '../../../lib/types';
|
import type { SiteConfig, ContactLink } from '../../../lib/types';
|
||||||
|
|
||||||
const CONTACT_KINDS: { value: string; label: string; placeholder: string }[] = [
|
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() {
|
export default function Settings() {
|
||||||
const [config, setConfig] = useState<Partial<SiteConfig>>({});
|
const [config, setConfig] = useState<Partial<SiteConfig>>({});
|
||||||
const [alert, setAlert] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getConfig()
|
getConfig()
|
||||||
.then(setConfig)
|
.then(setConfig)
|
||||||
@@ -22,8 +21,7 @@ export default function Settings() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
function showAlert(msg: string, type: 'success' | 'error') {
|
function showAlert(msg: string, type: 'success' | 'error') {
|
||||||
setAlert({ msg, type });
|
notify(msg, type);
|
||||||
setTimeout(() => setAlert(null), 5000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function update<K extends keyof SiteConfig>(key: K, value: SiteConfig[K]) {
|
function update<K extends keyof SiteConfig>(key: K, value: SiteConfig[K]) {
|
||||||
@@ -62,19 +60,6 @@ export default function Settings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-10">
|
<form onSubmit={handleSubmit} className="space-y-10">
|
||||||
{alert && (
|
|
||||||
<div
|
|
||||||
className={`p-4 text-sm font-display italic text-center border ${
|
|
||||||
alert.type === 'success'
|
|
||||||
? 'bg-[var(--green)]/15 text-[var(--green)] border-[var(--green)]/30'
|
|
||||||
: 'bg-[var(--red)]/15 text-[var(--red)] border-[var(--red)]/30'
|
|
||||||
}`}
|
|
||||||
style={{ borderRadius: 1 }}
|
|
||||||
>
|
|
||||||
{alert.msg}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<section className="space-y-6">
|
<section className="space-y-6">
|
||||||
<h2 className="font-display italic text-2xl text-[var(--text)] border-l-2 border-[var(--mauve)] pl-4">Identity</h2>
|
<h2 className="font-display italic text-2xl text-[var(--text)] border-l-2 border-[var(--mauve)] pl-4">Identity</h2>
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
|||||||
@@ -117,15 +117,22 @@ export function confirmDialog(opts: ConfirmOptions): Promise<boolean> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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') {
|
export function notify(message: string, tone: 'error' | 'success' = 'error') {
|
||||||
document.querySelector('.toast[data-notify]')?.remove();
|
document.querySelector('.toast[data-notify]')?.remove();
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.className = `toast${tone === 'error' ? ' toast--error' : ''}`;
|
el.className = `toast toast--${tone}`;
|
||||||
el.dataset.notify = '';
|
el.dataset.notify = '';
|
||||||
el.setAttribute('role', tone === 'error' ? 'alert' : 'status');
|
el.setAttribute('role', tone === 'error' ? 'alert' : 'status');
|
||||||
el.textContent = message;
|
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);
|
document.body.appendChild(el);
|
||||||
window.setTimeout(() => el.remove(), 4500);
|
window.setTimeout(dismiss, 4500);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1360,7 +1360,7 @@ select.topbar-control.theme-select {
|
|||||||
/* Toast */
|
/* Toast */
|
||||||
.toast {
|
.toast {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 1.5rem;
|
top: 1.25rem;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
background: var(--mantle);
|
background: var(--mantle);
|
||||||
@@ -1368,17 +1368,34 @@ select.topbar-control.theme-select {
|
|||||||
color: var(--rosewater);
|
color: var(--rosewater);
|
||||||
padding: 0.65rem 1.1rem;
|
padding: 0.65rem 1.1rem;
|
||||||
border-radius: 1px;
|
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-family: var(--font-display);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
z-index: 200;
|
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 {
|
@keyframes toast-in {
|
||||||
from { opacity: 0; transform: translate(-50%, 8px); }
|
from { opacity: 0; transform: translate(-50%, -10px); }
|
||||||
to { opacity: 1; transform: translate(-50%, 0); }
|
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). */
|
/* Salon grid spans driven by --col-span custom prop (avoids Tailwind dynamic class issue). */
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
@@ -2023,7 +2040,7 @@ html.cybersigil body::after {
|
|||||||
0 0 10px var(--sky),
|
0 0 10px var(--sky),
|
||||||
0 -3px 0 color-mix(in srgb, var(--mauve) 70%, transparent),
|
0 -3px 0 color-mix(in srgb, var(--mauve) 70%, transparent),
|
||||||
0 3px 0 color-mix(in srgb, var(--teal) 60%, 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 {
|
.cybersigil .cs-fx-tear::after {
|
||||||
content: "";
|
content: "";
|
||||||
@@ -2046,7 +2063,7 @@ html.cybersigil body::after {
|
|||||||
-webkit-mask: var(--cs-corner) center / contain no-repeat;
|
-webkit-mask: var(--cs-corner) center / contain no-repeat;
|
||||||
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));
|
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--tl { top: 0; left: 0; }
|
||||||
.cybersigil .cs-fx-corner--tr { top: 0; right: 0; transform: scaleX(-1); animation-delay: -1.7s; }
|
.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. */
|
/* Nameplate = the system handle. `> ` prompt + live block caret. */
|
||||||
.cybersigil .nameplate-title::before {
|
.cybersigil .nameplate-title::before {
|
||||||
content: "> ";
|
content: ">";
|
||||||
|
margin-right: 0.4em;
|
||||||
color: var(--sky);
|
color: var(--sky);
|
||||||
-webkit-text-fill-color: var(--sky);
|
-webkit-text-fill-color: var(--sky);
|
||||||
}
|
}
|
||||||
@@ -2131,7 +2149,7 @@ html.cybersigil body::after {
|
|||||||
vertical-align: -0.12em;
|
vertical-align: -0.12em;
|
||||||
background: var(--mauve);
|
background: var(--mauve);
|
||||||
box-shadow: 0 0 8px color-mix(in srgb, var(--mauve) 70%, transparent);
|
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 {
|
.cybersigil .nameplate-subtitle {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
@@ -2535,7 +2553,7 @@ html.cybersigil body::after {
|
|||||||
.cybersigil .back-link::after {
|
.cybersigil .back-link::after {
|
||||||
content: "_";
|
content: "_";
|
||||||
margin-left: -0.1em;
|
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:hover,
|
||||||
.cybersigil .back-link:focus-visible {
|
.cybersigil .back-link:focus-visible {
|
||||||
@@ -2569,7 +2587,7 @@ html.cybersigil body::after {
|
|||||||
content: "_";
|
content: "_";
|
||||||
margin-left: 0.18em;
|
margin-left: 0.18em;
|
||||||
opacity: 0.85;
|
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
|
/* Icon-only / collapsed controls have no room for the `>` prompt + blink
|
||||||
* caret — they overflow the 2rem square on phones. Drop the pseudo when
|
* caret — they overflow the 2rem square on phones. Drop the pseudo when
|
||||||
@@ -2845,7 +2863,7 @@ html.cybersigil body::after {
|
|||||||
content: "_";
|
content: "_";
|
||||||
color: var(--mauve);
|
color: var(--mauve);
|
||||||
font-family: var(--font-sans);
|
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"] {
|
.cybersigil .search-result [class*="line-clamp"] {
|
||||||
font-family: var(--font-sans) !important;
|
font-family: var(--font-sans) !important;
|
||||||
@@ -2916,7 +2934,7 @@ html.cybersigil body::after {
|
|||||||
.cybersigil .asset-drop-title::after {
|
.cybersigil .asset-drop-title::after {
|
||||||
content: "_";
|
content: "_";
|
||||||
color: var(--mauve);
|
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 {
|
.cybersigil .asset-empty {
|
||||||
@@ -3006,6 +3024,41 @@ html.cybersigil body::after {
|
|||||||
text-shadow: -1px 0 0 var(--sky), 1px 0 0 var(--mauve);
|
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 ─── */
|
/* ─── Theme keyframes ─── */
|
||||||
@keyframes cs-blink {
|
@keyframes cs-blink {
|
||||||
0%, 49% { opacity: 1; }
|
0%, 49% { opacity: 1; }
|
||||||
|
|||||||
Reference in New Issue
Block a user