// app.jsx — оркестрация экранов + Tweaks
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"goldTone": "#c9a227",
"typeSpeed": 22,
"embers": 26,
"atmosphere": true
}/*EDITMODE-END*/;
// gold palettes: [gold, bright, deep, dim]
const GOLD_SETS = {
"#c9a227": ["#c9a227", "#e8c75a", "#9a7c1e", "#6f5a23"], // classic gold
"#b87333": ["#b87333", "#e0935a", "#8a4f24", "#6a3f1e"], // copper / bronze
"#a8451f": ["#c25a2e", "#e08a4a", "#8a3014", "#6a2810"], // ember
"#8a8f6b": ["#9aa07a", "#c4c79c", "#6a6e52", "#4f5340"], // verdigris
};
function EmberField({ count }) {
const motes = React.useMemo(() =>
Array.from({ length: count }).map((_, i) => ({
left: Math.random() * 100,
dur: 9 + Math.random() * 14,
delay: -Math.random() * 20,
drift: (Math.random() * 80 - 40) + "px",
size: 1.5 + Math.random() * 2.5,
op: 0.4 + Math.random() * 0.5,
})), [count]);
return (
{motes.map((m, i) => (
))}
);
}
function Atmosphere({ on, embers }) {
return (
);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [view, setView] = React.useState("loading");
const [authMode, setAuthMode] = React.useState("register");
const [seeker, setSeeker] = React.useState(null);
// apply gold tone -> CSS variables
React.useEffect(() => {
const set = GOLD_SETS[t.goldTone] || GOLD_SETS["#c9a227"];
const r = document.documentElement.style;
r.setProperty("--gold", set[0]);
r.setProperty("--gold-bright", set[1]);
r.setProperty("--gold-deep", set[2]);
r.setProperty("--gold-dim", set[3]);
}, [t.goldTone]);
const enterAuth = (mode) => { setAuthMode(mode); setView("auth"); };
const signOut = async () => {
await window.AzothApi.logout();
setSeeker(null);
setView("landing");
};
React.useEffect(() => {
const token = window.AzothApi.token();
if (!token) {
setView("landing");
return;
}
window.AzothApi.me()
.then(({ user }) => {
setSeeker({
id: user.id,
name: user.display_name || "мандрівник",
email: user.email,
isNew: false,
disciplines: ["free", "tarot", "numero"],
});
setView("chat");
})
.catch(() => {
window.AzothApi.setToken("");
setView("landing");
});
}, []);
// Safety net: if entrance animations are paused (hidden tab) or stripped,
// force revealed state shortly after each view mounts so nothing stays blank.
React.useEffect(() => {
document.documentElement.classList.remove("anims-locked");
const id = setTimeout(() => document.documentElement.classList.add("anims-locked"), 1700);
return () => clearTimeout(id);
}, [view]);
return (
{view === "loading" && (
)}
{view === "landing" && }
{view === "auth" && (
setView("landing")}
onDone={(s) => { setSeeker(s); setView(s.isNew ? "onboarding" : "chat"); }} />
)}
{view === "onboarding" && (
{ setSeeker(prev => ({ ...prev, ...s })); setView("chat"); }} />
)}
{view === "chat" && (
)}
setTweak("goldTone", v)} />
setTweak("atmosphere", v)} />
setTweak("embers", v)} />
setTweak("typeSpeed", v)} />
);
}
ReactDOM.createRoot(document.getElementById("root")).render();