init elas atelier #1
@@ -1,58 +0,0 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const THEMES = [
|
||||
{ value: 'salon', label: 'Salon' },
|
||||
{ value: 'salon-noir', label: 'Salon Noir' },
|
||||
{ value: 'gothic', label: 'Gothic' },
|
||||
{ value: 'breakcore', label: 'Breakcore' },
|
||||
];
|
||||
|
||||
interface Props {
|
||||
defaultTheme?: string;
|
||||
}
|
||||
|
||||
export default function ThemeSwitcher({ defaultTheme = 'salon' }: Props) {
|
||||
const [theme, setTheme] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('user-theme') || defaultTheme;
|
||||
}
|
||||
return defaultTheme;
|
||||
});
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const isFirst = useRef(true);
|
||||
|
||||
useEffect(() => {
|
||||
const html = document.documentElement;
|
||||
THEMES.forEach(t => html.classList.remove(t.value));
|
||||
html.classList.add(theme);
|
||||
localStorage.setItem('user-theme', theme);
|
||||
|
||||
if (isFirst.current) {
|
||||
isFirst.current = false;
|
||||
return;
|
||||
}
|
||||
const label = THEMES.find(t => t.value === theme)?.label ?? theme;
|
||||
setToast(`Theme: ${label}`);
|
||||
const id = setTimeout(() => setToast(null), 1200);
|
||||
return () => clearTimeout(id);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<div className="relative inline-block text-left">
|
||||
<select
|
||||
value={theme}
|
||||
onChange={(e) => setTheme(e.target.value)}
|
||||
aria-label="Theme"
|
||||
className="topbar-control theme-select"
|
||||
>
|
||||
{THEMES.map(t => (
|
||||
<option key={t.value} value={t.value}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-[var(--subtext0)]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</div>
|
||||
{toast && <div className="toast" role="status">{toast}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,6 @@ import '../styles/global.css';
|
||||
import '@fontsource-variable/fraunces';
|
||||
import '@fontsource-variable/eb-garamond';
|
||||
import '@fontsource-variable/jetbrains-mono';
|
||||
import ThemeSwitcher from '../components/react/ThemeSwitcher';
|
||||
import Search from '../components/react/Search';
|
||||
import LogoutButton from '../components/react/LogoutButton';
|
||||
import EditableText from '../components/react/EditableText';
|
||||
@@ -77,9 +76,11 @@ const hasContact = (siteConfig.contact_links?.length ?? 0) > 0;
|
||||
{image && <meta name="twitter:image" content={image} />}
|
||||
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/feed.xml" />
|
||||
{siteConfig.custom_css && <style set:html={siteConfig.custom_css} />}
|
||||
<script is:inline define:vars={{ defaultTheme: siteConfig.theme }}>
|
||||
const savedTheme = localStorage.getItem('user-theme') || defaultTheme || 'salon';
|
||||
document.documentElement.classList.add(savedTheme);
|
||||
<script is:inline define:vars={{ siteTheme: siteConfig.theme }}>
|
||||
// Theme is owner-controlled (site config). Drop any legacy
|
||||
// per-visitor override so everyone sees the configured theme.
|
||||
try { localStorage.removeItem('user-theme'); } catch (e) {}
|
||||
document.documentElement.classList.add(siteTheme || 'salon');
|
||||
</script>
|
||||
<script is:inline>
|
||||
// When a page is restored from the back/forward cache (e.g. after
|
||||
@@ -129,7 +130,6 @@ const hasContact = (siteConfig.contact_links?.length ?? 0) > 0;
|
||||
<a href="/contact" class="topbar-control">Contact</a>
|
||||
)}
|
||||
<Search client:idle />
|
||||
<ThemeSwitcher client:only="react" defaultTheme={siteConfig.theme} />
|
||||
<span class="topbar-divider" aria-hidden="true"></span>
|
||||
{isAdmin ? (
|
||||
<LogoutButton client:idle />
|
||||
|
||||
@@ -790,17 +790,35 @@ code, pre, kbd, samp {
|
||||
}
|
||||
.plate-tag-mini {
|
||||
position: absolute;
|
||||
bottom: 18px;
|
||||
right: 18px;
|
||||
background: color-mix(in srgb, var(--crust) 78%, transparent);
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
background: color-mix(in srgb, var(--crust) 70%, transparent);
|
||||
color: var(--rosewater);
|
||||
border: 1px solid color-mix(in srgb, var(--rosewater) 22%, transparent);
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.16em;
|
||||
padding: 3px 8px;
|
||||
border: 1px solid color-mix(in srgb, var(--rosewater) 18%, transparent);
|
||||
border-radius: 999px;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.6rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.22em;
|
||||
padding: 4px 11px;
|
||||
text-transform: uppercase;
|
||||
backdrop-filter: blur(2px);
|
||||
backdrop-filter: blur(3px);
|
||||
}
|
||||
/* Breakcore: hard neon catalogue tag — sharp rect, offset shadow, glow.
|
||||
* Matches the layer's hazard-tape / hard-offset chrome language. */
|
||||
.breakcore .plate-tag-mini {
|
||||
background: var(--crust);
|
||||
color: var(--green);
|
||||
border: 1px solid var(--mauve);
|
||||
border-radius: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.14em;
|
||||
text-shadow: 0 0 6px color-mix(in srgb, var(--green) 60%, transparent);
|
||||
box-shadow:
|
||||
2px 2px 0 var(--mauve),
|
||||
0 0 14px -2px color-mix(in srgb, var(--mauve) 65%, transparent);
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
/* Nameplate — the museum-style header used in the site chrome */
|
||||
@@ -811,19 +829,6 @@ code, pre, kbd, samp {
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
}
|
||||
.nameplate::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: -6px;
|
||||
height: 2px;
|
||||
background: linear-gradient(to right,
|
||||
var(--mauve) 0%,
|
||||
var(--mauve) 35%,
|
||||
var(--surface2) 35%,
|
||||
var(--surface2) 100%);
|
||||
}
|
||||
.nameplate-title {
|
||||
font-family: var(--font-display);
|
||||
font-weight: 600;
|
||||
@@ -831,7 +836,10 @@ code, pre, kbd, samp {
|
||||
font-size: 1.6rem;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text);
|
||||
line-height: 1.12;
|
||||
/* Loose enough that italic-Fraunces descenders (g, y, p) and the
|
||||
* breakcore chromatic glow clear the line box — nothing slices them. */
|
||||
line-height: 1.2;
|
||||
padding-bottom: 0.06em;
|
||||
}
|
||||
.nameplate-subtitle {
|
||||
font-family: var(--font-sans);
|
||||
@@ -1275,18 +1283,7 @@ input[type="date"] { color-scheme: light; }
|
||||
);
|
||||
}
|
||||
|
||||
/* Nameplate — striped datamosh underline + glitch-shear burst on hover. */
|
||||
.breakcore .nameplate::after {
|
||||
height: 3px;
|
||||
bottom: -6px;
|
||||
opacity: 0.9;
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
var(--mauve) 0 6px,
|
||||
var(--green) 6px 12px,
|
||||
var(--blue) 12px 18px
|
||||
);
|
||||
}
|
||||
/* Nameplate — glitch-shear burst on hover (underline removed site-wide). */
|
||||
@keyframes bc-shear {
|
||||
0% { clip-path: inset(0 0 0 0); transform: translateX(0);
|
||||
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve); }
|
||||
|
||||
Reference in New Issue
Block a user