/* ============ BUSINESS — categorized works marquee (replaces old Intro slot) ============ Each lane pulls works whose `cat` tags match the lane's tag list. A lane only scrolls when its items actually overflow the viewport; if they fit, it stays static. */ const BUSINESS_LANES = [ { label: "システム開発", tags: ["システム開発"], dir: "rtl" }, { label: "イベント予約や決済アプリ開発", tags: ["イベント予約", "決済"], dir: "ltr" }, { label: "寮のアプリ開発", tags: ["寮管理", "寮アプリ"], dir: "rtl" }, ]; /* DeviceFrame's phone/hardware/tablet/doc variants are fixed-px in demo.css already; only "desktop" uses width:100% internally, so it alone needs an explicit parent width. */ function laneFrameStyle(device) { return device === "desktop" ? { width: 380 } : undefined; } /* Real demo screens skip DeviceFrame (see below) and instead sit in a work-thumb box — give that box the device's own aspect ratio so it hugs the mockup tightly instead of centering it inside a generic wide 4:3 frame with dead space (and a mismatched hover area). */ const LANE_DEMO_ASPECT = { phone: "300/620", tablet: "4/3", desktop: "16/10", hardware: "5/3", doc: "1/1.41" }; function laneDemoStyle(device) { return { aspectRatio: LANE_DEMO_ASPECT[device] || LANE_DEMO_ASPECT.phone }; } /* Backdrop thumbnails are a plain cropped screenshot, not the full WorkPrototypeThumb — that component draws the same thick phone-bezel chrome used for the big grid cards, which is way oversized for a 132px strip thumb (fat border, huge corner radius, spilling past the box). A flat cover-fit image crops cleanly to the tiny frame. */ function LaneBackVisual({ w, idx }) { const cfg = w.demoConfig || {}; const screens = w.demo && Array.isArray(cfg.screens) ? orderScreens(cfg.screens, cfg.initial) : []; const shot = screens[0] && screens[0].image; if (shot) return {w.title; return ; } /* One "featured" work gets the big device mockup up front; the rest of the lane's works pass by as flat thumbnails in a backdrop strip behind it. The strip always sweeps the FULL width of the stage — entering from one edge, crossing past the other, then looping — regardless of item count. The classic "double the content, shift by half its own width" filmstrip loop only reads as motion once there's enough content to overflow the stage; with just one or two items it stays parked near the stage's left edge — right where the feature mockup already sits — so it never visibly clears it or crosses the open space beside it. */ function Lane({ label, works, dir, alt, onFocusWork }) { const viewportRef = useRef(null); const trackRef = useRef(null); const [travel, setTravel] = useState({ start: 0, end: 0, duration: 22 }); const feature = works[0]; const backdrop = works.slice(1); const hasBackdrop = backdrop.length > 0; useLayoutEffect(() => { const vp = viewportRef.current, track = trackRef.current; if (!vp || !track || !hasBackdrop) return; const speedPxPerSec = 40; const measure = () => { const containerW = vp.clientWidth; const contentW = track.scrollWidth; setTravel({ start: containerW, end: -contentW, duration: Math.max(8, (containerW + contentW) / speedPxPerSec) }); }; measure(); const ro = new ResizeObserver(measure); ro.observe(vp); ro.observe(track); return () => ro.disconnect(); }, [hasBackdrop, backdrop.length]); if (!works.length) { return (

{label}

00 WORKS

準備中

); } const open = (id) => onFocusWork(id); const keyOpen = (e, id) => { if (e.key === "Enter") onFocusWork(id); }; return (
{hasBackdrop && (
{backdrop.map((w, i) => (
open(w.id)} role="button" tabIndex={0} onKeyDown={(e) => keyOpen(e, w.id)} >
))}
)}
open(feature.id)} role="button" tabIndex={0} onKeyDown={(e) => keyOpen(e, feature.id)} >
{feature.demo ? ( /* WorkPrototypeThumb already draws its own phone/tablet/desktop chrome around real demo screens — wrapping it in DeviceFrame here would double it up, so it just gets the same work-thumb positioning box the Works grid cards use. */
) : ( )}

{label}

{feature.title}{feature.en}

{String(works.length).padStart(2, "0")} WORKS
); } function BusinessSection({ onFocusWork }) { const all = (window.HASHBIT_DATA && window.HASHBIT_DATA.works) || []; const lanes = BUSINESS_LANES.map(l => ({ ...l, works: all.filter(w => (w.cat || []).some(c => l.tags.includes(c))) })); return (
02 Business
HASHBIT / 事業内容
{lanes.map((l, i) => ( {i > 0 && } ))}
); } window.BusinessSection = BusinessSection;