/* ============ Works — grid, filter, detail ============ */
function Placeholder({ label = "SCREEN", k = 0 }) {
return (
);
}
function DeviceFrame({ type = "phone", children, label }) {
if (type === "hardware") {
return (
{Array.from({length:18}).map((_,i)=>)}
PWR
);
}
if (type === "desktop") {
return (
);
}
if (type === "doc") {
return (
);
}
if (type === "tablet") {
return ;
}
return (
);
}
function orderScreens(screens, initialId) {
if (!screens.length) return [];
const first = initialId || screens[0].id;
const initial = screens.find(s => s.id === first) || screens[0];
return [initial, ...screens.filter(s => s !== initial)];
}
function WorkPrototypeThumb({ w, idx }) {
const cfg = w.demoConfig || {};
const flowScreens = Array.isArray(cfg.screens)
? orderScreens(cfg.screens, cfg.initial).map((s, i) => ({
id: s.id || `screen-${i}`,
name: s.name || s.id || `Screen ${i + 1}`,
image: s.image || "",
kind: "image"
}))
: [];
const legacyMap = window.DEMO_SCREENS || {};
const legacyKeys = Object.keys(legacyMap);
const legacyScreens = w.demo && !flowScreens.length && legacyKeys.length
? orderScreens(legacyKeys.map(k => ({ id: k, ...legacyMap[k] })), cfg.initial || "home").map(s => ({ ...s, kind: "component" }))
: [];
const screens = flowScreens.length ? flowScreens : legacyScreens;
const [pos, setPos] = useState(0);
useEffect(() => { setPos(0); }, [w.id]);
useEffect(() => {
if (!w.demo || screens.length < 2) return;
const delay = 2800 + (idx % 3) * 320;
const timer = setInterval(() => setPos(p => (p + 1) % screens.length), delay);
return () => clearInterval(timer);
}, [w.demo, screens.length, idx]);
if (!w.demo || !screens.length) {
return ;
}
const cur = screens[pos % screens.length];
const device = cfg.device || w.device || "phone";
const Comp = cur.Comp;
return (
{device === "desktop" &&
}
{cur.kind === "component" && Comp
?
: cur.image
?

:
}
{screens.length > 1 &&
{screens.map((s, i) => )}
}
);
}
function WorkCard({ w, idx, onOpen }) {
const [ref, seen] = useInView();
const cats = Array.isArray(w.cat) ? w.cat : [];
const href = `/works?id=${encodeURIComponent(w.id || idx + 1)}`;
const open = (e) => {
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1) return;
e.preventDefault();
onOpen(w);
};
return (
ID:{String(w.id || "work").slice(0,6).toUpperCase()}
{String(w.device || "phone").toUpperCase()}
OPEN →
{w.demo &&
LIVE DEMO}
{w.year}
{w.title || "Untitled"}{w.en || w.id}
{w.desc}
{cats.map(c => {c})}
);
}
function WorksSection({ onOpen, sel, setSel }) {
const data = (window.HASHBIT_DATA && window.HASHBIT_DATA.works) || [];
const cats = ["All", ...Array.from(new Set(data.flatMap(w => Array.isArray(w.cat) ? w.cat : [])))];
const list = sel === "All" ? data : data.filter(w => w.cat.includes(sel));
return (
03 Works
{String(data.length).padStart(2,"0")} PROJECTS / 2023—2025
{cats.map(c => (
))}
{list.map((w, i) => )}
);
}
/* SEO: swap /description/canonical/JSON-LD for the open work, restore them on close */
function useWorkSEO(w) {
useEffect(() => {
if (!w) return;
const defaultTitle = document.title;
const metaDesc = document.querySelector('meta[name="description"]');
const defaultDesc = metaDesc ? metaDesc.getAttribute("content") : "";
const seoTitle = w.seoTitle || `${w.title}${w.en ? `「${w.en}」` : ""} | HASHBIT株式会社`;
const seoDesc = w.seoDescription || w.catch || w.desc || w.overview || defaultDesc;
const url = `${window.location.origin}/works?id=${encodeURIComponent(w.id)}`;
document.title = seoTitle;
if (metaDesc) metaDesc.setAttribute("content", seoDesc);
let canonical = document.querySelector('link[rel="canonical"]');
const hadCanonical = !!canonical;
if (!canonical) {
canonical = document.createElement("link");
canonical.setAttribute("rel", "canonical");
document.head.appendChild(canonical);
}
canonical.setAttribute("href", url);
const ld = document.createElement("script");
ld.type = "application/ld+json";
ld.id = "work-jsonld";
ld.textContent = JSON.stringify({
"@context": "https://schema.org",
"@type": "Product",
name: w.title,
alternateName: w.en,
description: seoDesc,
category: (w.cat || []).join(", "),
brand: { "@type": "Organization", name: "HASHBIT株式会社", url: window.location.origin },
url
});
document.head.appendChild(ld);
return () => {
document.title = defaultTitle;
if (metaDesc) metaDesc.setAttribute("content", defaultDesc);
if (!hadCanonical && canonical) canonical.remove();
document.getElementById("work-jsonld")?.remove();
};
}, [w]);
}
function InfoBlock({ label, children, mono }) {
return (
);
}
function WorkDetail({ w, onClose, go }) {
useWorkSEO(w);
useEffect(() => {
document.body.style.overflow = "hidden";
const onKey = (e) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", onKey);
return () => { document.body.style.overflow = ""; window.removeEventListener("keydown", onKey); };
}, []);
return (
{w.en.toUpperCase()} / {w.year}
{(w.cat || []).map(c => {c})}
{w.title}
{w.en}
{w.catch}
CLIENT{w.client}
PERIOD{w.period}
{w.overview}
{(w.features || []).map(f => - →{f}
)}
+ Contact
同じような仕組みを、つくりませんか。
);
}
Object.assign(window, { Placeholder, DeviceFrame, WorksSection, WorkDetail });