mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-31 15:37:49 +00:00
theme: transition the tokens themselves — @property makes the fade real
The element-level transition blanket never could work here: the theme lives in custom properties consumed through color-mix() inside GRADIENTS (background-image is not animatable), inherited colors, and per-frame inline writes. The tokens are now registered as typed <color> @property rules and the .theme-anim class transitions the VARIABLES on the root over 0.75s — every consumer re-resolves each animated frame, gradients and scrims included, with zero per-element transitions to fight hover states or the ruler's per-frame color writes. A forced reflow commits the transition property before the token flip, and watchThemeColors now derives twice (at flip and after settle) so WebGL color targets land on the true final values instead of a mid-fade sample. Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
This commit is contained in:
1 parent
5af1afdc0a
commit
c886121dff
3 files changed
+57
-15
No files matched your search
@@ -124,19 +124,47 @@ body {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Theme switch cross-fade: the toggle stamps .theme-anim on <html> for
|
||||
~0.8s so every themed property (text, backgrounds, hairlines, SVG fills
|
||||
— the wordmark's logo-A included) transitions together over 0.75s, then
|
||||
lifts the blanket so hover states and per-frame color writes stay
|
||||
instant. */
|
||||
html.theme-anim,
|
||||
html.theme-anim * {
|
||||
/* Theme switch cross-fade. The tokens are registered as typed <color>
|
||||
custom properties so the VARIABLES THEMSELVES can transition: when the
|
||||
toggle stamps .theme-anim on <html>, the root transitions every token
|
||||
over 0.75s and every consumer — utilities, borders, SVG fills,
|
||||
color-mix() strings, and gradients (background-image is not animatable,
|
||||
so element-level transitions could never cover the scrims and pools) —
|
||||
re-resolves each animated frame. No per-element transitions exist, so
|
||||
hover states and per-frame color writes stay instant throughout. */
|
||||
@property --bg { syntax: "<color>"; inherits: true; initial-value: #f2f1ee; }
|
||||
@property --bg-raised { syntax: "<color>"; inherits: true; initial-value: #faf9f7; }
|
||||
@property --bg-sunken { syntax: "<color>"; inherits: true; initial-value: #e9e8e4; }
|
||||
@property --ink { syntax: "<color>"; inherits: true; initial-value: #101014; }
|
||||
@property --ink-strong { syntax: "<color>"; inherits: true; initial-value: #000000; }
|
||||
@property --muted { syntax: "<color>"; inherits: true; initial-value: rgba(16, 16, 20, 0.62); }
|
||||
@property --faint { syntax: "<color>"; inherits: true; initial-value: rgba(16, 16, 20, 0.42); }
|
||||
@property --line { syntax: "<color>"; inherits: true; initial-value: rgba(16, 16, 20, 0.16); }
|
||||
@property --line-strong { syntax: "<color>"; inherits: true; initial-value: rgba(16, 16, 20, 0.4); }
|
||||
@property --accent { syntax: "<color>"; inherits: true; initial-value: #2d81ff; }
|
||||
@property --accent-ink { syntax: "<color>"; inherits: true; initial-value: #1659c4; }
|
||||
@property --invert-bg { syntax: "<color>"; inherits: true; initial-value: #101014; }
|
||||
@property --invert-fg { syntax: "<color>"; inherits: true; initial-value: #f2f1ee; }
|
||||
@property --danger { syntax: "<color>"; inherits: true; initial-value: #b3261e; }
|
||||
@property --scanline { syntax: "<color>"; inherits: true; initial-value: rgba(16, 16, 20, 0.03); }
|
||||
|
||||
html.theme-anim {
|
||||
transition:
|
||||
background-color 0.75s ease,
|
||||
color 0.75s ease,
|
||||
border-color 0.75s ease,
|
||||
fill 0.75s ease,
|
||||
stroke 0.75s ease;
|
||||
--bg 0.75s ease,
|
||||
--bg-raised 0.75s ease,
|
||||
--bg-sunken 0.75s ease,
|
||||
--ink 0.75s ease,
|
||||
--ink-strong 0.75s ease,
|
||||
--muted 0.75s ease,
|
||||
--faint 0.75s ease,
|
||||
--line 0.75s ease,
|
||||
--line-strong 0.75s ease,
|
||||
--accent 0.75s ease,
|
||||
--accent-ink 0.75s ease,
|
||||
--invert-bg 0.75s ease,
|
||||
--invert-fg 0.75s ease,
|
||||
--danger 0.75s ease,
|
||||
--scanline 0.75s ease;
|
||||
}
|
||||
|
||||
/* Signature interaction: label blocks invert on hover/active. */
|
||||
|
||||
@@ -41,6 +41,9 @@ export default function ThemeToggle({ className }: { className?: string }) {
|
||||
() => root.classList.remove("theme-anim"),
|
||||
800,
|
||||
);
|
||||
// Commit the transition property in its own style pass BEFORE the
|
||||
// token flip — same-recalc changes can skip the transition entirely.
|
||||
void root.offsetWidth;
|
||||
root.setAttribute("data-theme", next);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, next);
|
||||
|
||||
@@ -67,17 +67,28 @@ export function watchThemeColors(
|
||||
onChange: (colors: ThemeColors) => void,
|
||||
): () => void {
|
||||
const derive = () => onChange(deriveThemeColors());
|
||||
// Theme flips animate the tokens over 0.75s (see globals.css), so a
|
||||
// derive at flip time reads MID-transition values. Deliver both: the
|
||||
// immediate read (consumers that lerp start moving at once) and a final
|
||||
// read after the fade settles so targets land on the true colors.
|
||||
let settleTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
const deriveTwice = () => {
|
||||
derive();
|
||||
clearTimeout(settleTimer);
|
||||
settleTimer = setTimeout(derive, 850);
|
||||
};
|
||||
derive();
|
||||
|
||||
const observer = new MutationObserver(derive);
|
||||
const observer = new MutationObserver(deriveTwice);
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ["data-theme"],
|
||||
});
|
||||
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
mq.addEventListener("change", derive);
|
||||
mq.addEventListener("change", deriveTwice);
|
||||
return () => {
|
||||
clearTimeout(settleTimer);
|
||||
observer.disconnect();
|
||||
mq.removeEventListener("change", derive);
|
||||
mq.removeEventListener("change", deriveTwice);
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user