Files
ApiCore/ApiCore-Frontend/src/components/hive/hive-focus-bar.tsx
T
Nick SzaunigandClaude Opus 5.5 9f6ab4ea3f refactor: Beute-Frontend vereinfacht (findPart, subVec3, seededRandom)
Gemeinsame Helfer statt vier Kopien der Teilsuche, zwei Vektor-
Subtraktionen und zwei Zufallsgeneratoren; hasPartSheet an einer
Stelle; ungenutzte setOpacity und points-Ref entfernt. Verhalten
unverändert (1050 Tests).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-16 10:16:35 +02:00

103 lines
4.2 KiB
TypeScript

"use client";
import { useEffect } from "react";
import { ArrowLeft, SlidersHorizontal } from "lucide-react";
import { findPart } from "@/lib/hive/find-part";
import type { HiveCatalog, HiveState } from "@/lib/hive/types";
import { GLASS_PILL, GLASS_SURFACE } from "./hive-glass";
import { formatAmount } from "./hive-scene/scene-model";
// DOM overlay of focus mode (spec 9.4): glass „Zurück“ pill, the part's name
// and its state as small glass chips, „Ändern“ for parts with a sheet.
// Esc leaves focus mode unless a dialog (sheet) has the keyboard.
export interface FocusChip {
key: string;
text: string;
}
/** Chips for the focused part: frame count of a box, every state field with its value. */
export function focusChips(state: HiveState, catalog: HiveCatalog, id: string): FocusChip[] {
const part = findPart(state, id);
const type = part ? catalog.partTypes.get(part.partTypeId) : undefined;
if (!part || !type) return [];
const chips: FocusChip[] = [];
if (type.kind === "BOX") {
const frames = (part.frames ?? []).filter((frame) => frame.type !== "NONE").length;
chips.push({ key: "frames", text: frames === 1 ? "1 Rähmchen" : `${frames} Rähmchen` });
}
for (const field of type.stateSchema) {
const raw = part.state[field.key];
if (field.kind === "level") {
const value = typeof raw === "number" ? raw : 0;
chips.push({ key: field.key, text: `${field.label} ${formatAmount(value, field.unit)} von ${formatAmount(field.max, field.unit)}` });
} else {
const index = Math.max(0, field.options.indexOf(String(raw)));
chips.push({ key: field.key, text: `${field.label}: ${field.optionLabels[index] ?? field.options[index]}` });
}
}
const inserts = (part.inserts ?? []).map((insert) => catalog.partTypes.get(insert.partTypeId)?.kind);
if (type.kind === "FLOOR") chips.push({ key: "insert", text: inserts.includes("FLOOR_INSERT") ? "Bausperre drin" : "ohne Bausperre" });
return chips;
}
export interface HiveFocusBarProps {
/** Part in focus mode; nothing is rendered without one. */
focusedId: string | null;
name: string;
chips: FocusChip[];
/** Shows „Ändern“ (the part has a sheet and the user may edit). */
canEdit: boolean;
onEdit: () => void;
onBack: () => void;
}
export function HiveFocusBar({ focusedId, name, chips, canEdit, onEdit, onBack }: HiveFocusBarProps) {
useEffect(() => {
if (!focusedId) return;
const onKey = (event: KeyboardEvent) => {
if (event.key !== "Escape" || event.defaultPrevented) return;
// A sheet handles its own Esc.
if (event.target instanceof Element && event.target.closest("[role='dialog'], [role='alertdialog']")) return;
onBack();
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [focusedId, onBack]);
if (!focusedId) return null;
// Fades in (150 ms); only at motion `full` it also slides down (motion-full variant, spec 9.8).
return (
<div
key={focusedId}
className="pointer-events-none flex flex-col items-start gap-2 duration-150 animate-in fade-in-0 motion-full:duration-300 motion-full:slide-in-from-top-2"
>
<div className="flex max-w-full items-center gap-3">
<button type="button" onClick={onBack} className={`pointer-events-auto ${GLASS_PILL}`}>
<ArrowLeft aria-hidden className="size-4" />
Zurück
</button>
<p className="truncate text-lg font-semibold text-holo-label-fg drop-shadow-[0_1px_8px_rgb(0_0_0/0.6)]">{name}</p>
</div>
{chips.length > 0 || canEdit ? (
<ul className="flex max-w-full flex-wrap gap-1.5" aria-label={`Zustand von ${name}`}>
{chips.map((chip) => (
<li key={chip.key} className={`rounded-full px-3 py-1 text-xs font-medium ${GLASS_SURFACE}`}>
{chip.text}
</li>
))}
{canEdit ? (
<li>
<button type="button" onClick={onEdit} className={`pointer-events-auto ${GLASS_PILL}`}>
<SlidersHorizontal aria-hidden className="size-4" />
Ändern
</button>
</li>
) : null}
</ul>
) : null}
</div>
);
}