feat: holographic 3D hive scene and Futtertasche (Beute L3, first cut)

- 3D dependencies (three, react-three-fiber, drei, postprocessing,
  motion), exact versions, supply-chain checked; scene loaded only on
  demand on the colony page (283 kB gzip), SVG stays as fallback
- glass hologram per owner design 2026-10-16: true Liebig Zander
  shapes (grips, rebate, Wanderboden foot), cool blue edge glow,
  click any part -> it flies to the front while the rest stays as a
  faint ghost; focus shows content (feed levels, Bausperre, pockets);
  frames rise on hover and take their type look (colour + comb pattern)
- full-bleed card with frosted overlays and glass buttons
- Futtertasche (FRAME_FEEDER): new part kind occupying frame slots
  with a fill level, catalog row, validator rules in both ports,
  outline, sheets, follow-ups, settings

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 c608f5d405
commit 6e13236d49
100 files changed
+9432 -209

No files matched your search

@@ -167,13 +167,29 @@ describe("HiveCard", () => {
expect(toggle).toHaveAttribute("aria-pressed", "true");
});
it("keeps the outline from growing the card: it scrolls in an absolutely placed child", async () => {
it("keeps the outline from growing the card: it scrolls in a panel placed over the stage", async () => {
renderCard();
await userEvent.click(await screen.findByRole("button", { name: "Aufbau als Liste" }));
const scroll = screen.getByTestId("hive-outline-scroll");
expect(scroll).toHaveClass("sm:absolute", "sm:inset-0", "sm:overflow-y-auto");
expect(scroll.parentElement).toHaveClass("sm:relative", "sm:w-72");
expect(scroll).toHaveClass("overflow-y-auto");
// The panel fills the space above the button bar inside an absolutely placed column,
// so the stage (and the row) never grows and the buttons stay reachable.
const column = scroll.parentElement!.parentElement!;
expect(column).toHaveClass("order-1", "min-h-0", "flex-1");
expect(column.parentElement).toHaveClass("absolute", "inset-0");
const buttons = screen.getByRole("button", { name: "Aufbau als Liste" }).closest(".order-2");
expect(buttons?.parentElement).toBe(column.parentElement);
});
it("shows title and facts as a frosted overlay and the actions as glass buttons on the stage", async () => {
renderCard();
await screen.findByRole("img", { name: /Beute mit/ });
expect(screen.getByText("Beute")).toBeInTheDocument();
expect(screen.getByText("2 Zargen · 6 Brutwaben · Futter 5 l · Bausperre drin")).toBeInTheDocument();
const list = screen.getByRole("button", { name: "Aufbau als Liste" });
expect(list).toHaveTextContent("Liste");
expect(screen.getByRole("button", { name: "Verlauf" })).toHaveClass("backdrop-blur-md");
});
it("points „Aufbau als Liste“ at the outline", async () => {