// app.jsx — アプリ本体(ヘッダー / ルーティング / フッター / Tweaks)
const { useState: useStateA, useEffect: useEffectA } = React;
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "wamodern",
"accent": "#6d7a3c",
"fontSize": 16,
"radius": "default"
}/*EDITMODE-END*/;
const THEME_LABELS = { wamodern: "和モダン", natural: "ナチュラル", database: "データベース" };
function Header({ view, onNav, query, setQuery, onSearch }) {
const [menu, setMenu] = useStateA(false);
return (
);
}
function Footer({ onNav, onJump }) {
const J = (id) => (e) => { e.preventDefault(); onJump(id); };
const toList = (e) => { e.preventDefault(); onNav("list"); };
return (
);
}
function App() {
const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
const [view, setView] = useStateA("top");
const [listFilters, setListFilters] = useStateA(null);
const [modal, setModal] = useStateA(null);
const [query, setQuery] = useStateA("");
const [activeQuery, setActiveQuery] = useStateA("");
const [, forceTick] = useStateA(0);
// 管理画面でデータが変わったら公開サイトも更新(別タブ・同タブ両対応)
useEffectA(() => {
const refresh = () => { window.PRODUCTS = loadProducts(); forceTick((n) => n + 1); };
window.addEventListener("storage", refresh);
window.addEventListener("products-changed", refresh);
window.addEventListener("masters-changed", refresh);
return () => {
window.removeEventListener("storage", refresh);
window.removeEventListener("products-changed", refresh);
window.removeEventListener("masters-changed", refresh);
};
}, []);
// テーマ適用
useEffectA(() => {
document.documentElement.setAttribute("data-theme", t.theme);
document.documentElement.style.setProperty("--base-fz", t.fontSize + "px");
if (t.accent) {
document.documentElement.style.setProperty("--olive", t.accent);
// 濃色は accent をやや暗く
document.documentElement.style.setProperty("--olive-deep",
`color-mix(in srgb, ${t.accent} 78%, #1c2010)`);
}
if (t.radius === "sharp") {
document.documentElement.style.setProperty("--radius", "4px");
document.documentElement.style.setProperty("--radius-sm", "3px");
} else if (t.radius === "round") {
document.documentElement.style.setProperty("--radius", "24px");
document.documentElement.style.setProperty("--radius-sm", "16px");
} else {
document.documentElement.style.removeProperty("--radius");
document.documentElement.style.removeProperty("--radius-sm");
}
}, [t.theme, t.accent, t.fontSize, t.radius]);
const goList = (presetFilters) => {
setListFilters(presetFilters
? { variety: [], origin: [], shape: [], hardness: [], maker: [], tag: [], price: null, minRating: null, ...presetFilters }
: null);
setActiveQuery("");
setView("list");
window.scrollTo({ top: 0 });
};
const nav = (v) => { setView(v); if (v === "top") setActiveQuery(""); window.scrollTo({ top: 0 }); };
const doSearch = () => { setActiveQuery(query); setListFilters(null); setView("list"); window.scrollTo({ top: 0 }); };
const jumpTo = (id) => {
setView("top"); setActiveQuery("");
setTimeout(() => document.getElementById(id)?.scrollIntoView?.({ behavior: "smooth" }), 70);
};
return (
{view === "top"
?
:
}
{modal &&
setModal(null)} />}
setTweak("theme", v)} />
setTweak("accent", v)} />
setTweak("radius", v)} />
setTweak("fontSize", v)} />
);
}
// マウント関数。CMSモード(__CMS_MODE)のときは、データ取得後に bootstrap から呼ぶ。
window.mountApp = function () {
ReactDOM.createRoot(document.getElementById("root")).render();
};
if (!window.__CMS_MODE) {
window.mountApp();
}