fix: Feinschliff nach Browserprüfung (Sprachnotiz-Text, offline Volk, Beute-SVG)
Backend Image / build-and-push (push) Successful in 33s
Frontend Image / build-and-push (push) Failing after 17m8s

- „Gesagt wurde:“ zeigt an, dass der Text aufklappbar ist
- /voice/new nennt offline das Volk aus den Offline-Paketen
- Einstellungen: Überschrift „Sprachnotizen“ ohne „in Vorbereitung“
- Beute-SVG (role=img) ohne fokussierbare Kinder (axe nested-
  interactive); Tastaturweg bleibt die Gliederung. Dabei behoben: der
  Editor fokussierte nach dem Entfernen die SVG-Form statt der
  Listenzeile.

Gates: Frontend lint, typecheck, 3110 Tests, build.

Refs #27

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 13:33:38 +02:00
1 parent 6c33a6de3b
commit 1763d953b6
9 files changed
+76 -23

No files matched your search

@@ -68,8 +68,9 @@ function neighbourIds(state: HiveState, id: string): string[] {
return [...(after[0] ? [after[0]] : []), ...(before[0] ? [before[0]] : [])];
}
/** The row button in the stack list (the SVG shapes carry the same data attribute). */
function rowControl(id: string): HTMLElement | null {
return document.querySelector<HTMLElement>(`[data-part-id="${CSS.escape(id)}"]`);
return document.querySelector<HTMLElement>(`button[data-part-id="${CSS.escape(id)}"]`);
}
/** The history button that still works, else the stack heading. */
@@ -74,12 +74,10 @@ describe("HiveSvg", () => {
expect(container.querySelectorAll('[tabindex="0"]')).toHaveLength(0);
});
it("makes parts buttons with tabIndex -1 when onSelectPart is given", () => {
it("selects parts by pointer when onSelectPart is given (the outline is the keyboard path)", () => {
const onSelectPart = vi.fn();
const { container } = renderSvg(<HiveSvg layout={layout} onSelectPart={onSelectPart} />);
const group = container.querySelector('g[data-part-id="b2"]')!;
expect(group).toHaveAttribute("role", "button");
expect(group).toHaveAttribute("tabindex", "-1");
fireEvent.click(group);
expect(onSelectPart).toHaveBeenCalledWith("b2");
});
@@ -121,12 +119,22 @@ describe("HiveSvg", () => {
const onSelectFrame = vi.fn();
const { container } = renderSvg(<HiveSvg layout={layout} onSelectFrame={onSelectFrame} />, store);
const frame = container.querySelector("[data-frame-slot='2']")!;
expect(frame).toHaveAttribute("role", "button");
expect(frame).toHaveAttribute("tabindex", "-1");
fireEvent.click(frame);
expect(onSelectFrame).toHaveBeenCalledWith("b1", 2);
});
it("stays a pure image with handlers: no focusable or interactive descendants (axe nested-interactive)", () => {
const store = createHiveViewStore();
store.openBox("b1");
const { container } = renderSvg(
<HiveSvg layout={layout} onSelectPart={vi.fn()} onSelectFrame={vi.fn()} />,
store,
);
const image = container.querySelector("[role=img]")!;
expect(image).not.toBeNull();
expect(image.querySelectorAll("[tabindex], [role=button]")).toHaveLength(0);
});
it("uses currentColor based strokes and no hard-coded colours", () => {
const { container } = renderSvg(<HiveSvg layout={layout} />);
expect(container.innerHTML).not.toMatch(/#[0-9a-f]{3,6}\b|rgb\(/i);
@@ -1,7 +1,7 @@
"use client";
import { useId } from "react";
import type { KeyboardEvent, ReactNode } from "react";
import type { ReactNode } from "react";
import { FRAME_TYPE_LABELS, PART_KIND_LABELS } from "@/lib/hive/labels";
import type { FrameBox, HiveLayout, PartBox, Vec3 } from "@/lib/hive/layout";
import type { FrameType } from "@/lib/hive/types";
@@ -107,11 +107,9 @@ const HOVER_COLOR = "data-[hovered=true]:text-holo-edge-strong print:data-[hover
const edge = "fill-none stroke-current [vector-effect:non-scaling-stroke]";
function activate(event: KeyboardEvent, action: () => void): void {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
action();
}
// The <svg> is one image (role="img"): its shapes take pointer input only and
// are neither focusable nor buttons (axe nested-interactive). The keyboard and
// screen-reader path to every part and frame is the outline tree next to it.
export interface HiveSvgProps {
layout: HiveLayout;
@@ -141,10 +139,7 @@ function PartShape({ part, dashed, name, hovered, onSelect, onHover }: PartShape
];
const interactive = onSelect
? {
role: "button" as const,
tabIndex: -1,
onClick: () => onSelect(part.id),
onKeyDown: (event: KeyboardEvent) => activate(event, () => onSelect(part.id)),
onPointerEnter: () => onHover(part.id),
onPointerLeave: () => onHover(null),
}
@@ -187,10 +182,7 @@ function FrameShape({ frame, width, patternPrefix, hovered, onSelect, onHover }:
const ref = { boxId: frame.boxId, slot: frame.slot };
const interactive = onSelect
? {
role: "button" as const,
tabIndex: -1,
onClick: () => onSelect(frame.boxId, frame.slot),
onKeyDown: (event: KeyboardEvent) => activate(event, () => onSelect(frame.boxId, frame.slot)),
onPointerEnter: () => onHover(ref),
onPointerLeave: () => onHover(null),
}
@@ -120,6 +120,13 @@ describe("VoiceCaptureBox", () => {
expect(details?.querySelector("b")).toBeNull();
});
it("a READY note keeps the transcript in the page and says the panel can be opened", () => {
renderBox();
expect(screen.getByText("Königin gesehen, vier Brutwaben.")).toBeInTheDocument();
expect(screen.getByText(/zum Lesen öffnen/)).toBeInTheDocument();
});
it("lists what could not be assigned without a button", () => {
renderBox({ unclear: ["Etwas zur Futtermenge, Menge nicht genannt"] });
@@ -132,9 +132,12 @@ export function VoiceCaptureBox({
) : null}
{capture.transcript ? (
<details open={textOnly} className="rounded-lg border border-border bg-card p-3">
<details open={textOnly} className="group rounded-lg border border-border bg-card p-3">
<summary className="flex min-h-11 cursor-pointer items-center text-sm font-medium">
Gesagt wurde:
<span>Gesagt wurde:</span>
<span className="ml-2 font-normal text-muted-foreground group-open:hidden">
(zum Lesen öffnen)
</span>
</summary>
<p className="mt-2 whitespace-pre-wrap break-words text-sm select-text">{capture.transcript}</p>
<div className="mt-2 flex flex-wrap items-center gap-3">