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/fraunces';
|
||||||
import '@fontsource-variable/eb-garamond';
|
import '@fontsource-variable/eb-garamond';
|
||||||
import '@fontsource-variable/jetbrains-mono';
|
import '@fontsource-variable/jetbrains-mono';
|
||||||
import ThemeSwitcher from '../components/react/ThemeSwitcher';
|
|
||||||
import Search from '../components/react/Search';
|
import Search from '../components/react/Search';
|
||||||
import LogoutButton from '../components/react/LogoutButton';
|
import LogoutButton from '../components/react/LogoutButton';
|
||||||
import EditableText from '../components/react/EditableText';
|
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} />}
|
{image && <meta name="twitter:image" content={image} />}
|
||||||
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/feed.xml" />
|
<link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/feed.xml" />
|
||||||
{siteConfig.custom_css && <style set:html={siteConfig.custom_css} />}
|
{siteConfig.custom_css && <style set:html={siteConfig.custom_css} />}
|
||||||
<script is:inline define:vars={{ defaultTheme: siteConfig.theme }}>
|
<script is:inline define:vars={{ siteTheme: siteConfig.theme }}>
|
||||||
const savedTheme = localStorage.getItem('user-theme') || defaultTheme || 'salon';
|
// Theme is owner-controlled (site config). Drop any legacy
|
||||||
document.documentElement.classList.add(savedTheme);
|
// per-visitor override so everyone sees the configured theme.
|
||||||
|
try { localStorage.removeItem('user-theme'); } catch (e) {}
|
||||||
|
document.documentElement.classList.add(siteTheme || 'salon');
|
||||||
</script>
|
</script>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
// When a page is restored from the back/forward cache (e.g. after
|
// 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>
|
<a href="/contact" class="topbar-control">Contact</a>
|
||||||
)}
|
)}
|
||||||
<Search client:idle />
|
<Search client:idle />
|
||||||
<ThemeSwitcher client:only="react" defaultTheme={siteConfig.theme} />
|
|
||||||
<span class="topbar-divider" aria-hidden="true"></span>
|
<span class="topbar-divider" aria-hidden="true"></span>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
<LogoutButton client:idle />
|
<LogoutButton client:idle />
|
||||||
|
|||||||
@@ -790,17 +790,35 @@ code, pre, kbd, samp {
|
|||||||
}
|
}
|
||||||
.plate-tag-mini {
|
.plate-tag-mini {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 18px;
|
bottom: 16px;
|
||||||
right: 18px;
|
right: 16px;
|
||||||
background: color-mix(in srgb, var(--crust) 78%, transparent);
|
background: color-mix(in srgb, var(--crust) 70%, transparent);
|
||||||
color: var(--rosewater);
|
color: var(--rosewater);
|
||||||
border: 1px solid color-mix(in srgb, var(--rosewater) 22%, transparent);
|
border: 1px solid color-mix(in srgb, var(--rosewater) 18%, transparent);
|
||||||
font-family: var(--font-display);
|
border-radius: 999px;
|
||||||
font-size: 0.7rem;
|
font-family: var(--font-sans);
|
||||||
letter-spacing: 0.16em;
|
font-size: 0.6rem;
|
||||||
padding: 3px 8px;
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
padding: 4px 11px;
|
||||||
text-transform: uppercase;
|
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 */
|
/* Nameplate — the museum-style header used in the site chrome */
|
||||||
@@ -811,19 +829,6 @@ code, pre, kbd, samp {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
position: relative;
|
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 {
|
.nameplate-title {
|
||||||
font-family: var(--font-display);
|
font-family: var(--font-display);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -831,7 +836,10 @@ code, pre, kbd, samp {
|
|||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
color: var(--text);
|
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 {
|
.nameplate-subtitle {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
@@ -1275,18 +1283,7 @@ input[type="date"] { color-scheme: light; }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nameplate — striped datamosh underline + glitch-shear burst on hover. */
|
/* Nameplate — glitch-shear burst on hover (underline removed site-wide). */
|
||||||
.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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@keyframes bc-shear {
|
@keyframes bc-shear {
|
||||||
0% { clip-path: inset(0 0 0 0); transform: translateX(0);
|
0% { clip-path: inset(0 0 0 0); transform: translateX(0);
|
||||||
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve); }
|
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve); }
|
||||||
|
|||||||
Reference in New Issue
Block a user