// Floor Plate — step 3 of the inventory drill-down. "Typical Floor Plan". // params: [towerId, floor]. Back → floors/. // // Header, back button and title are matched to the other inside screens // (FloorSelect / Inventory): a circular back button + breadcrumb + a light // serif title + top-right monogram / wordmark / availability chip. No baked // image-text — every label is real HTML. // // The selected floor's PAIR shows as two cut-out block plates (true-alpha PNGs). // Two-state interaction: // STATE 1 (blocks) — both plates visible; tap a block to choose it. // STATE 2 (units) — a floating unit-selector panel rises ON TOP of the // chosen block (the other dims). Tap a unit → its plan. // The right rail is a full-height "plate dossier": key plan + RERA carpet-area // table + typology legend (or a configuration card when a pair has no table), // sized to fill the column so the screen never reads half-empty. // Data: window.PLATE_DATA. const FP_KEYS_ID = 'uni-floorplate-keys-v10'; function ensureFpKeys() { if (typeof document === 'undefined') return; if (document.getElementById(FP_KEYS_ID)) return; const s = document.createElement('style'); s.id = FP_KEYS_ID; s.textContent = ` @keyframes fpFadeUp { from { opacity:0; transform: translateY(20px); } to { opacity:1; transform: translateY(0); } } @keyframes fpPanelIn { from { opacity:0; transform: translate(-50%,-50%) scale(0.92); } to { opacity:1; transform: translate(-50%,-50%) scale(1); } } @keyframes fpCardIn { from { opacity:0; transform: translateY(12px); } to { opacity:1; transform: translateY(0); } } /* idle "tap me" breathing glow on the block hot-zone */ @keyframes fpTapPulse { 0%,100% { box-shadow: 0 0 0 1.5px rgba(201,160,94,0.45), 0 18px 40px rgba(120,86,28,0.14), 0 0 34px rgba(201,160,94,0.24); border-color: rgba(201,160,94,0.58); } 50% { box-shadow: 0 0 0 2.5px rgba(201,160,94,0.72), 0 22px 52px rgba(120,86,28,0.20), 0 0 64px rgba(201,160,94,0.46); border-color: rgba(201,160,94,0.85); } } @keyframes fpHintBob { 0%,100% { transform: translate(-50%,0); } 50% { transform: translate(-50%,-4px); } } @keyframes fpZoneIn { from { opacity:0; transform: scale(0.93); } to { opacity:1; transform: scale(1); } } @keyframes fpPipPulse { 0%,100% { box-shadow: 0 0 0 0 rgba(201,160,94,0.42); } 50% { box-shadow: 0 0 0 7px rgba(201,160,94,0); } } @keyframes fpHlPulse { 0%,100% { box-shadow: 0 0 0 1px rgba(255,255,255,0.55), 0 0 14px rgba(201,160,94,0.45); } 50% { box-shadow: 0 0 0 1px rgba(255,255,255,0.7), 0 0 28px rgba(201,160,94,0.80); } } .fp-block { transition: transform 340ms cubic-bezier(0.22,1,0.36,1), filter 340ms ease, opacity 340ms ease; } .fp-hotzone { animation: fpTapPulse 2.6s ease-in-out infinite; transition: box-shadow 300ms ease, border-color 300ms ease, background 300ms ease, transform 280ms cubic-bezier(0.22,1,0.36,1); } .fp-hotzone.is-hot { animation: none; box-shadow: 0 0 0 2px rgba(201,160,94,0.85), 0 26px 60px rgba(120,86,28,0.24), 0 0 70px rgba(201,160,94,0.50) !important; border-color: rgba(201,160,94,0.92) !important; background: rgba(201,160,94,0.20) !important; } .fp-hotzone.is-active { animation: none; box-shadow: 0 0 0 2.5px var(--gold-deep), 0 30px 66px rgba(120,86,28,0.28) !important; border-color: var(--gold-deep) !important; background: rgba(201,160,94,0.14) !important; } .fp-plate-btn { transition: transform 220ms cubic-bezier(0.22,1,0.36,1); } .fp-plate-btn:active { transform: scale(0.985); } .fp-uc { transition: transform 180ms cubic-bezier(0.22,1,0.36,1), box-shadow 220ms ease, border-color 220ms ease, background 220ms ease; } .fp-uc:active { transform: scale(0.975); } .fp-zone { transition: background 220ms ease, border-color 220ms ease, box-shadow 220ms ease; } .fp-zone:not(:disabled):active { transform: scale(0.99); } .fp-zone-pill { transition: border-color 200ms ease, box-shadow 200ms ease, background 200ms ease; } .fp-reset { transition: background 200ms ease, color 200ms ease, border-color 200ms ease; } .kp-zoom-btn { transition: background 180ms ease, color 180ms ease, border-color 180ms ease, transform 140ms ease; } .kp-zoom-btn:hover { background: var(--gold) !important; color:#1a130a !important; border-color: var(--gold) !important; } .kp-zoom-btn:active { transform: scale(0.92); } .kp-zoom-btn:disabled { opacity:0.4; cursor:not-allowed; } `; document.head.appendChild(s); } // "You are here" highlight box per pair, as % of the shared keyplan-site base // (cropped masterplan-06 typical-floor, 1760×1800). Tuned to the labelled towers. const KEYPLAN_HL = { AB: { left:22, top:61, width:30, height:34 }, CD: { left:45, top:33, width:13, height:27 }, EF: { left:49, top:12, width:37, height:18 }, GH: { left:83, top:33, width:14, height:40 }, IJ: { left:52, top:63, width:29, height:12 }, }; // native plate aspect (w/h) per block — drives the on-screen frame. const PLATE_ASPECT = { A:1.5, B:1.49, C:1.811, D:1.804, E:1.14, F:1.14, G:1.593, H:1.568, I:1.508, J:1.707 }; // Unit "shape" zones laid directly over the plate (positions are spatial corners // in PLATE_DATA, so each unit's button sits on its real quadrant of the plan). // rect = % of the plate frame; ax/ay = which corner the selector pill anchors to. const UNIT_ZONE = { topLeft: { left:1.5, top:2, w:47, h:46.5, ax:'left', ay:'top' }, topRight: { left:51.5, top:2, w:47, h:46.5, ax:'right', ay:'top' }, bottomLeft: { left:1.5, top:51.5, w:47, h:46.5, ax:'left', ay:'bottom' }, bottomRight: { left:51.5, top:51.5, w:47, h:46.5, ax:'right', ay:'bottom' }, left: { left:1.5, top:7, w:47, h:86, ax:'left', ay:'mid' }, right: { left:51.5, top:7, w:47, h:86, ax:'right', ay:'mid' }, }; function FloorPlate() { ensureFpKeys(); const t = useLoop(); const e = clamp(t/0.5); const [route] = useRoute(); const [sel, setSel] = React.useState(null); // selected block id const [hoverB, setHoverB] = React.useState(null); // hovered block id const towerId = route.params && route.params[0]; const floor = route.params && parseInt(route.params[1], 10); const tower = TOWERS.find(tw => tw.id === towerId); React.useEffect(()=>{ setSel(null); }, [towerId, floor]); // Derive plate data defensively BEFORE any conditional return so every hook // below runs on every render (route can momentarily resolve to no tower during // a transition — a hook after an early return would change the hook count and // throw "Rendered fewer hooks than expected"). const pairCode = tower ? tower.pair.replace(/&/g,'') : ''; // 'A&B' → 'AB' const pdata = (pairCode && window.PLATE_DATA && window.PLATE_DATA[pairCode]) || null; const layouts = pdata ? pdata.layouts : []; // [{block, positions}] left→right const blockIds = layouts.map(l => l.block).slice().sort(); // KEY PLAN focus — the screen zooms into THIS pair's marking so it's the hero. const kpHl = KEYPLAN_HL[pairCode] || null; const kpCx = kpHl ? kpHl.left + kpHl.width/2 : 50; // marking centre (% of base) const kpCy = kpHl ? kpHl.top + kpHl.height/2 : 50; const kpBase = kpHl ? Math.max(1.5, Math.min(2.8, 0.62 * Math.min(100/kpHl.width, 100/kpHl.height))) : 1; const [kpZoom, setKpZoom] = React.useState(kpBase); React.useEffect(() => { setKpZoom(kpBase); }, [pairCode]); // re-frame when the block pair changes // floor-level availability across both blocks → the header chip. const summary = React.useMemo(() => { const s = { available:0, hold:0, sold:0 }; if (!Number.isFinite(floor)) return s; blockIds.forEach(b => buildUnits(b, floor).forEach(u => { s[u.status] = (s[u.status]||0) + 1; })); return s; }, [blockIds.join(''), floor]); if (!tower || !Number.isFinite(floor)) { return (
); } const flLabel = floorLabel(tower, floor); const hl = KEYPLAN_HL[pairCode] || null; // every pair now resolves to the shared site key plan const hasTable = !!(pdata && pdata.table && pdata.table.length > 0); const hasLegend = !!(pdata && pdata.typeLegend && pdata.typeLegend.length > 0); // geometry — the rail is ALWAYS present (filled with a config card when a pair // has no table) so the plates sit in a consistent left field, never adrift in // a half-empty canvas. const RAIL_W = 680; const bandRight = RAIL_W + 72 + 56; // rail width + right inset + gutter return (
{/* ── HEADER (matches FloorSelect / Inventory) ───────────────── */}
navigate('inventory')}, {label:`Tower ${towerId}`, go:()=>navigate(`floors/${towerId}`)}, {label:flLabel}, ]}/>
Typical Floor Plan
Block {blockIds.join(' & ')} · {flLabel}{pdata && pdata.subtitle ? ` · ${pdata.subtitle}` : ''}
navigate('home')} onKeyDown={ev=>{ if(ev.key==='Enter'||ev.key===' '){ ev.preventDefault(); navigate('home'); } }} onMouseEnter={ev=>{ ev.currentTarget.style.opacity='0.6'; ev.currentTarget.style.transform='scale(1.03)'; }} onMouseLeave={ev=>{ ev.currentTarget.style.opacity='1'; ev.currentTarget.style.transform='scale(1)'; }} style={{display:'flex', alignItems:'center', gap:22, cursor:'pointer', transformOrigin:'right center', transition:'opacity 200ms ease, transform 200ms ease'}}>
{/* ── THE TWO BLOCK PLATES (glowing buttons) ─────────────── */}
{layouts.map((lay, li) => { const id = lay.block; const positions = lay.positions; const four = !('left' in positions); const aspect = PLATE_ASPECT[id] || 1.5; const dimmed = sel && sel !== id; const active = sel === id; const hot = hoverB === id || active; const plateW = four ? 740 : 770; const plateH = Math.round(plateW / aspect); const units = Object.entries(positions) .map(([corner, num]) => ({ corner, num })) .sort((a,b)=>a.num-b.num); return (
{/* clean block label (replaces the ornate cartouche) */}
Block {id}
{units.length} {units.length===1?'RESIDENCE':'RESIDENCES'}
{/* tappable HOT-ZONE — a solid gold-glow card sized to the plate. Translucent gold fill (plan stays readable on top), a clear ring border and an idle breathing pulse so it reads as a button. */}
setSel(id)} onMouseEnter={()=>setHoverB(id)} onMouseLeave={()=>setHoverB(null)} style={{position:'relative', width:'100%', height:'100%', zIndex:1, cursor:'pointer', filter: hot ? 'drop-shadow(0 26px 42px rgba(120,86,28,0.26))' : 'drop-shadow(0 16px 28px rgba(120,86,28,0.14))', transition:'filter 340ms ease'}}> {`Block{ ev.currentTarget.style.opacity=0.15; }}/>
{/* TAP affordance chip — only while this block is idle/unselected */} {!active && (
TAP
)} {/* ── ON-PLATE UNIT ZONES — each unit shaped out on its real quadrant of the plan, with a corner selector pill. No popup: the buttons live on the map itself. ── */} {active && units.map((it, ci) => { const z = UNIT_ZONE[it.corner] || UNIT_ZONE.topLeft; const u = buildUnits(id, floor).find(x => x.pos === it.num); const st = u ? (STATUS[u.status] || STATUS.sold) : STATUS.available; const sold = u && u.status === 'sold'; const rera = (pdata && pdata.perPosition && pdata.perPosition[it.num] && pdata.perPosition[it.num].rera) || (u && u.sqft) || null; // anchor the pill to this zone's outer corner so it never covers the plan centre const pillPos = { position:'absolute' }; if (z.ax === 'left') pillPos.left = 16; else pillPos.right = 16; if (z.ay === 'top') pillPos.top = 16; else if (z.ay === 'bottom') pillPos.bottom = 16; else { pillPos.top = '50%'; pillPos.transform = 'translateY(-50%)'; } return ( ); })}
); })}
{/* ── RIGHT RAIL: full-height plate dossier ──────────────────── */}
{/* KEY PLAN — ONE shared typical-floor site base, ZOOMED INTO this pair's marking so the selected blocks are the focus. Pinch-free zoom in/out controls sit over it; the marking stays centred. */}
}/>
{/* contain-fit box that matches the base image aspect (1760×1800) */}
{`Key{ const sec = ev.currentTarget.closest('[data-keyplan]'); if (sec) sec.style.display='none'; }}/> {kpHl && (
{/* zoom in / out controls */}
{[['+', () => setKpZoom(z => Math.min(5, +(z*1.25).toFixed(3))), kpZoom >= 5], ['−', () => setKpZoom(z => Math.max(1, +(z/1.25).toFixed(3))), kpZoom <= 1]].map(([lbl, fn, dis]) => ( ))}
{/* readable caption under the key plan */}
BLOCK {blockIds.join(' & ')} · {flLabel.toUpperCase()} · YOU ARE HERE
{/* RERA CARPET AREA — equal-weight rows. Each unit/area pair gets the same generous height & type so every line reads clearly at tablet distance (data ≥24px, header ≥20px). */} {hasTable && (
UNIT NUMBER
RERA CA · SQ.FT
{pdata.table.map((r, ri) => (
{r.unitRange}
{Number(r.rera).toLocaleString('en-IN', {minimumFractionDigits:2, maximumFractionDigits:2})}
))}
)} {/* TYPOLOGY legend — equal-weight rows matching the area table's readability (label ≥24px, numbered token like the on-plate pills). */} {hasLegend && (
{pdata.typeLegend.map((tl, ti) => (
{ti+1} {tl}
))}
)} {/* CONFIGURATION — shown when a pair has no table/keyplan (e.g. C&D), so the rail carries real information instead of empty space. */} {!hasTable && (
{[ ['Blocks', blockIds.join(' & ')], ['Residences / floor', String(layouts.reduce((n,l)=>n+Object.keys(l.positions).length,0))], ['Typology', '4 BHK'], ['Level', flLabel], ['RERA carpet area', 'On request'], ].map(([k,v], i) => (
{k.toUpperCase()} {v}
))}

Detailed RERA carpet areas for Block {blockIds.join(' & ')} are issued with the allotment pack. Tap a block to view its residences.

)}
{/* ── BOTTOM HINT (under the plates) + reset ─────────────── */}
{sel ? 'CHOOSE A RESIDENCE TO VIEW ITS PLAN' : 'TAP A BLOCK TO SELECT A RESIDENCE'}
{sel && ( )}
); } // ── small section heading used inside the rail dossier ─────────────────────── function SectionLabel({ text, right }) { return (
{text}
{right || null}
); } // ── HTML north compass (replaces any baked "N" on the key-plan image) ──────── function Compass() { return (
N
); } window.FloorPlate = FloorPlate;