Files
Dune/components/kwisatz-inspector.tsx
T
Quintine 66c41b2af5 Add Kwisatz Haderach component inspection
Connect an original companion face and enlarged rules to the private Atreides panel and battle composer, authorized full-plan inspection, public revealed plans and an internal reference topic. Private loss progress and availability come only from the existing owner projection; plan components receive only inclusion. Rules calculations, save shape, AI strategy and mode gates remain unchanged.

Add focused projection, sealed/revealed rendering, JSON restoration, private-status and asset regressions. All 4950 offline tests, types, lint and production build pass. Independent privacy/rules review found no material findings. Browser checks verify reference opening, loaded artwork, Escape/Close focus return, mobile bounds and keyboard scrolling to the final rule. The focusable scroll region has a documented narrow lint exception. Earlier image-convention and focusability lint failures are retained in private evidence. HTTP tests are unnecessary for this presentation-only change.

All 1373 rooms, 2883 seats and original auxiliary rows remain unchanged; SQLite integrity and foreign keys pass. Existing server reused without restart. Screenshot capture, complete live Advanced browser acceptance and final manufactured-component/art verification remain open. No rules gate is opened by this checkpoint.

Private evidence: 20260921-kwisatz-inspection.
Verified source tree: 1181471eed47b86390882999a2ffce0db6a44b1379c7e8f7fc406bef39a94179
2026-09-21 09:00:51 +10:00

74 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog';
import { Eye, X } from 'lucide-react';
import Image from 'next/image';
import { Button } from './ui/button';
import { Dialog, DialogClose, DialogDescription, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from './ui/dialog';
import { KWISATZ_RULES, kwisatzAvailability, type KwisatzBattleContext, type KwisatzDisplayState } from '@/game/kwisatz-display';
export function KwisatzCardFace() {
return <figure aria-label="Kwisatz Haderach companion card" className="mx-auto my-0 w-full max-w-sm overflow-hidden rounded-xl border-2 border-[#96815a] bg-[#252b25] text-[#f5e8c9]">
<figcaption className="p-4 text-center font-serif text-2xl">Kwisatz Haderach</figcaption>
<Image unoptimized src="/art/leaders/kwisatz-haderach-v1.png" alt="Original portrait of Kwisatz Haderach, with blue eyes and a green desert mantle" width={1254} height={1254} className="aspect-square w-full object-cover" />
<div className="space-y-2 p-4 text-base leading-7">
<p className="m-0 font-semibold">Atreides · Advanced game · +2 strength</p>
<p className="m-0">A battle companion for a leader or Cheap Hero. The bonus counts if the accompanying leader survives; that leader cannot turn traitor.</p>
<p className="m-0">Available after seven Atreides battle-force losses. One territory per turn.</p>
</div>
</figure>;
}
export function KwisatzPrivateStatus({ state, battle, usedTerritoryName }: {
state: KwisatzDisplayState;
battle?: KwisatzBattleContext;
usedTerritoryName?: string;
}) {
return <section aria-label="Your private Kwisatz Haderach status" className="space-y-2 rounded-lg border border-[#647555] bg-[#243023] p-4 text-base leading-7">
<h3 className="m-0 font-semibold">Your private status</h3>
<p className="m-0">{state.losses} of 7 battle losses{state.active ? ' · awakened' : ''}</p>
<progress aria-label="Atreides battle losses toward Kwisatz Haderach" max={7} value={Math.min(7, Math.max(0, state.losses))} className="h-3 w-full accent-[#c9ad70]" />
<p className="m-0">{kwisatzAvailability(state, battle)}</p>
{state.usedAt && <p className="m-0">Used this turn in {usedTerritoryName ?? state.usedAt}.</p>}
</section>;
}
/** State is supplied only from me.kwisatz; reference inspection has no live state. */
export function KwisatzInspector({ state, battle, usedTerritoryName, context = 'reference' }: {
state?: KwisatzDisplayState;
battle?: KwisatzBattleContext;
usedTerritoryName?: string;
context?: 'reference' | 'plan';
}) {
const label = state ? 'private status' : context === 'plan' ? 'plan component' : 'rules reference';
return <Dialog>
<DialogTrigger render={<Button variant="outline" className="min-h-11 max-w-full whitespace-normal motion-reduce:transition-none" />} aria-label={`Inspect Kwisatz Haderach · ${label}`}>
<Eye aria-hidden="true" />Inspect Kwisatz Haderach
</DialogTrigger>
<DialogPortal>
<DialogOverlay className="bg-black/75 motion-reduce:animate-none motion-reduce:transition-none" />
<DialogPrimitive.Popup className="fixed top-1/2 left-1/2 z-50 flex max-h-[calc(100dvh-1rem)] w-[calc(100%-1rem)] max-w-2xl -translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-2xl border border-[#8e7952] bg-[#141814] text-[#eeeae0] shadow-2xl outline-none">
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-[#414536] px-4 py-3">
<span className="text-sm font-semibold text-[#ddbc77]">{state ? 'Private component inspection' : context === 'plan' ? 'Included in this plan' : 'Rules reference'}</span>
<DialogClose render={<Button variant="outline" className="min-h-11 min-w-11 motion-reduce:transition-none" />}><X aria-hidden="true" />Close</DialogClose>
</div>
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- Keyboard users need to focus this independently scrolling rules region. */}
<section aria-label="Kwisatz Haderach details" tabIndex={0} className="min-h-0 space-y-5 overflow-y-auto overscroll-contain p-5 focus-visible:outline-2 focus-visible:outline-[#ddbc77] sm:p-6">
<DialogTitle className="m-0 font-serif text-2xl text-[#fff3d6]">Kwisatz Haderach</DialogTitle>
<DialogDescription className="m-0 text-base leading-7 text-[#d9d4c7]">{state ? 'Your battle-loss count and availability are private. Opening this view does not reveal them.' : context === 'plan' ? 'Kwisatz Haderach is included in this plan you are allowed to inspect. No private loss count or current availability is shown.' : 'The Atreides battle companion for the Advanced game. This reference does not show any game’s private loss count or battle plan.'}</DialogDescription>
{state && <KwisatzPrivateStatus state={state} battle={battle} usedTerritoryName={usedTerritoryName} />}
<KwisatzCardFace />
<ol className="m-0 list-decimal space-y-3 pl-5 text-base leading-7">{KWISATZ_RULES.map(rule => <li key={rule}>{rule}</li>)}</ol>
</section>
</DialogPrimitive.Popup>
</DialogPortal>
</Dialog>;
}
export function KwisatzReferenceCard() {
return <section aria-label="Kwisatz Haderach component" className="space-y-4">
<KwisatzCardFace />
<KwisatzInspector />
</section>;
}