feat: hive card with SVG, outline, edit sheets and catalog settings (Beute L2)

- lib/hive: frontend port of the hive domain (ops, validator, facts,
  layout, follow-ups, describe texts, reason texts) running the backend
  fixtures, API client
- hive card on the colony page (same height as Stammdaten): SVG view,
  accessible outline tree, frame/part sheets with optimistic save,
  conflict sheet, history with past states, init from template or
  another colony, inspection draft mode
- follow-up toasts prefilling feeding and treatment forms (treatment
  end only with an open treatment)
- settings page "Beutensysteme" (hive-systems:manage)
- NumberStepper decimal steps, reactive motion level hook, holo tokens
- spec: owner design decision 2026-10-16 (glass hologram, focus mode)

The SVG view is the fallback; the 3D scene follows in L3.

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 09:12:03 +02:00
1 parent 0091b3d28b
commit c608f5d405
87 files changed
+9493 -43

No files matched your search

@@ -0,0 +1,79 @@
import { describe, expect, it } from "vitest";
import { describeHiveOps } from "./describe";
import { inlineCatalog, part, templateState } from "./test-utils";
import type { HiveOp, HiveState } from "./types";
describe("describeHiveOps", () => {
const catalog = inlineCatalog();
function stateWithInsert(): HiveState {
const state = templateState();
state.stack[0].inserts = [part("fi", "floor-insert")];
return state;
}
it("describes removed inserts and added parts", () => {
const ops: HiveOp[] = [
{ op: "removePart", id: "fi" },
{ op: "addPart", part: part("b2", "box"), at: 2 },
];
expect(describeHiveOps(ops, stateWithInsert(), catalog)).toBe("Bausperre entfernt, Zarge hinzugefügt");
});
it("uses the label of a part when it has one", () => {
const state = templateState();
state.stack[1].label = "Honigraum 1";
expect(describeHiveOps([{ op: "removePart", id: "b" }], state, catalog)).toBe("Honigraum 1 entfernt");
expect(describeHiveOps([{ op: "movePart", id: "b", to: 2 }], state, catalog)).toBe("Honigraum 1 verschoben");
});
it("describes insert ops", () => {
expect(
describeHiveOps([{ op: "addInsert", hostId: "f", part: part("fi", "floor-insert") }], templateState(), catalog),
).toBe("Bausperre eingelegt");
expect(
describeHiveOps([{ op: "addInsert", hostId: "b", part: part("e", "evaporator") }], templateState(), catalog),
).toBe("Verdunster eingesetzt");
});
it("describes setState with unit and option labels", () => {
const state = templateState();
state.stack.splice(2, 0, part("fe", "feeder"));
expect(describeHiveOps([{ op: "setState", id: "fe", key: "syrupLiters", value: 2.5 }], state, catalog)).toBe(
"Flüssigfutter 2,5 l",
);
expect(describeHiveOps([{ op: "setState", id: "l", key: "feedHole", value: "OPEN" }], state, catalog)).toBe(
"Futterloch offen",
);
});
it("describes setFrame, setFrames and setLabel", () => {
const state = templateState();
expect(describeHiveOps([{ op: "setFrame", boxId: "b", slot: 4, type: "BROOD" }], state, catalog)).toBe(
"Rähmchen 4 in Zarge: Brutwabe",
);
expect(describeHiveOps([{ op: "setFrames", boxId: "b", frames: [] }], state, catalog)).toBe(
"Rähmchen in Zarge neu gesetzt",
);
expect(describeHiveOps([{ op: "setLabel", id: "b", label: "Brutraum" }], state, catalog)).toBe(
"Zarge heißt jetzt „Brutraum“",
);
});
it("resolves parts added earlier in the same batch", () => {
const ops: HiveOp[] = [
{ op: "addPart", part: { ...part("n", "box"), label: "Aufsatz" }, at: 2 },
{ op: "setFrame", boxId: "n", slot: 1, type: "HONEY" },
];
expect(describeHiveOps(ops, templateState(), catalog)).toBe(
"Aufsatz hinzugefügt, Rähmchen 1 in Aufsatz: Honigwabe",
);
});
it("shortens more than three ops", () => {
const ops: HiveOp[] = [1, 2, 3, 4, 5].map((slot) => ({ op: "setFrame", boxId: "b", slot, type: "BROOD" }));
expect(describeHiveOps(ops, templateState(), catalog)).toBe(
"Rähmchen 1 in Zarge: Brutwabe, Rähmchen 2 in Zarge: Brutwabe, Rähmchen 3 in Zarge: Brutwabe und 2 weitere",
);
});
});