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,56 @@
import { describe, expect, it } from "vitest";
import { deriveHiveFacts } from "./facts";
import { hiveFactsText, shortGermanDay, sourceLabel } from "./summary";
import { frames, inlineCatalog, part } from "./test-utils";
import type { HiveState } from "./types";
const catalog = inlineCatalog();
describe("shortGermanDay", () => {
it("writes 2026-10-12 as 12.10.", () => {
expect(shortGermanDay("2026-10-12")).toBe("12.10.");
});
});
describe("sourceLabel", () => {
it("names the sources the way the beekeeper knows them", () => {
expect(sourceLabel("CARD")).toBe("Karte");
expect(sourceLabel("EDITOR")).toBe("Umbau");
expect(sourceLabel("INSPECTION")).toBe("Durchsicht");
expect(sourceLabel("INIT")).toBe("Angelegt");
});
});
describe("hiveFactsText", () => {
it("lists boxes, brood, feed and the floor insert", () => {
const state: HiveState = {
schemaVersion: 1,
stack: [
part("f", "floor", { inserts: [part("fi", "floor-insert")] }),
part("b1", "box", { frames: frames(3, "BROOD") }),
part("b2", "box", { frames: frames(3, "BROOD") }),
part("fe", "feeder", { state: { syrupLiters: 3, fondantKg: 0 } }),
part("fe2", "feeder", { state: { syrupLiters: 2, fondantKg: 0 } }),
],
};
expect(hiveFactsText(deriveHiveFacts(state, catalog))).toBe(
"2 Zargen · 6 Brutwaben · Futter 5 l · Bausperre drin",
);
});
it("uses singular forms and leaves out what is not there", () => {
const state: HiveState = {
schemaVersion: 1,
stack: [part("b1", "box", { frames: frames(1, "BROOD") })],
};
expect(hiveFactsText(deriveHiveFacts(state, catalog))).toBe("1 Zarge · 1 Brutwabe");
});
it("shows acid of an evaporator", () => {
const state: HiveState = {
schemaVersion: 1,
stack: [part("b1", "box", { frames: frames(1), inserts: [part("ev", "evaporator", { state: { acidMl: 40 } })] })],
};
expect(hiveFactsText(deriveHiveFacts(state, catalog))).toContain("Säure 40 ml");
});
});