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,102 @@
import { deriveHiveFacts } from "@/lib/hive/facts";
import { frames, inlineCatalog, part } from "@/lib/hive/test-utils";
import type {
HiveChangeListItem,
HiveSetupView,
HiveState,
HiveSystemDto,
} from "@/lib/hive/types";
import type { Colony } from "@/lib/colonies/types";
// Shared by the hive card, init and history specs.
export const SYSTEM_ID = "sys";
export function systemDto(): HiveSystemDto {
const catalog = inlineCatalog();
return {
id: SYSTEM_ID,
name: "Zander nach Dr. Liebig",
frameWidthMm: 477,
frameHeightMm: 220,
footprintLMm: 525,
footprintWMm: 430,
template: {},
active: true,
createdAt: "2026-10-01T00:00:00.000Z",
updatedAt: "2026-10-01T00:00:00.000Z",
partTypes: [...catalog.partTypes.values()].map((row) => ({
...row,
systemId: SYSTEM_ID,
articleRef: null,
boxDepth: null,
createdAt: "2026-10-01T00:00:00.000Z",
updatedAt: "2026-10-01T00:00:00.000Z",
})),
};
}
/** Two boxes with 3 brood frames each, two feeders with 5 l together, a floor insert. */
export function sampleState(): HiveState {
return {
schemaVersion: 1,
stack: [
part("f", "floor", { inserts: [part("fi", "floor-insert")] }),
part("b1", "box", { frames: frames(10).map((frame) => (frame.slot <= 3 ? { ...frame, type: "BROOD" as const } : frame)) }),
part("b2", "box", { frames: frames(10).map((frame) => (frame.slot <= 3 ? { ...frame, type: "BROOD" as const } : frame)) }),
part("fe", "feeder", { state: { syrupLiters: 3, fondantKg: 0 } }),
part("fe2", "feeder", { state: { syrupLiters: 2, fondantKg: 0 } }),
part("r", "roof"),
],
};
}
export function setupView(state: HiveState = sampleState(), colonyId = "colony-1", version = 3): HiveSetupView {
return {
colonyId,
systemId: SYSTEM_ID,
version,
state,
facts: deriveHiveFacts(state, inlineCatalog()),
updatedAt: "2026-10-12T10:00:00.000Z",
};
}
export function colonyFixture(extra: Partial<Colony> = {}): Colony {
return {
id: "colony-1",
code: "V-01",
apiaryId: "apiary-1",
hiveType: null,
originText: null,
colonyType: "PRODUCTION_COLONY",
status: "ACTIVE",
strength: null,
occupiedFrames: null,
broodStatus: null,
feedStatus: null,
healthStatus: null,
notes: null,
currentQueenId: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
deactivatedAt: null,
...extra,
};
}
export function historyItem(extra: Partial<HiveChangeListItem> = {}): HiveChangeListItem {
return {
id: "c1",
version: 3,
occurredOn: "2026-10-12",
source: "INSPECTION",
inspectionId: "i1",
userId: "u1",
userName: "Nick",
ops: [{ op: "setState", id: "fe", key: "syrupLiters", value: 3 }],
note: null,
createdAt: "2026-10-12T12:00:00.000Z",
...extra,
};
}