fix: T4a browser check findings
Backend Image / build-and-push (push) Failing after 2m45s
Frontend Image / build-and-push (push) Successful in 5m52s

- Withdrawal wording from one backend helper everywhere ("bis
  einschließlich TT.MM. – ab TT.MM. wieder erntefähig", year when it
  crosses New Year); withdrawal-status returns lastBlockedDay and the
  ready text, the frontend no longer computes dates
- Calendar events name colony or apiary, withdrawal events carry
  product and detail; compact withdrawal rows on mobile
- One bottling warning per colony and treatment; pre-save hint
- Offline: online-only actions disabled with a reason, cached data
  shows its timestamp, open sheets block submit offline
- Mobile: no horizontal scroll at 360 px, touch targets >= 44 px,
  toasts and the add button no longer cover content
- Cleanup sheet without destructive preselection; natural sort of
  colony codes; collapsible automation rule cards; varroa list newest
  first with chart axes; text fixes

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 18:26:09 +02:00
1 parent 32c61c8e14
commit c2f71bcb03
101 files changed
+2189 -271

No files matched your search

@@ -0,0 +1,65 @@
import { afterEach, beforeAll, describe, expect, it } from "vitest";
import { act, render, screen } from "@testing-library/react";
import { toast } from "sonner";
import { Toaster } from "./sonner";
beforeAll(() => {
// jsdom has no matchMedia; Sonner's Toaster asks for the color scheme.
Object.defineProperty(window, "matchMedia", {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addEventListener: () => undefined,
removeEventListener: () => undefined,
addListener: () => undefined,
removeListener: () => undefined,
dispatchEvent: () => false,
}),
});
});
afterEach(() => {
act(() => {
toast.dismiss();
});
});
describe("Toaster", () => {
it("sits at the bottom with a phone offset above the bottom nav and the quick-add button", async () => {
const { container } = render(<Toaster />);
act(() => {
toast("Verworfen");
});
await screen.findByText("Verworfen");
const list = container.ownerDocument.querySelector("[data-sonner-toaster]") as HTMLElement;
expect(list).toHaveAttribute("data-y-position", "bottom");
// One logic for both breakpoints: the CSS variable follows the app's sm breakpoint (640 px).
expect(list.style.getPropertyValue("--mobile-offset-bottom")).toBe("var(--toast-offset-bottom)");
expect(list.style.getPropertyValue("--offset-bottom")).toBe("var(--toast-offset-bottom)");
});
it("makes the action button of every toast 44 px high", async () => {
render(<Toaster />);
act(() => {
toast("Verworfen", { action: { label: "Rückgängig", onClick: () => undefined } });
});
const action = await screen.findByRole("button", { name: "Rückgängig" });
expect(action.className).toContain("h-11!");
expect(action.className).toContain("min-w-11!");
});
it("makes the close button a 44 px target", async () => {
render(<Toaster closeButton />);
act(() => {
toast("Hinweis");
});
await screen.findByText("Hinweis");
const close = document.querySelector("[data-close-button]") as HTMLElement;
expect(close.className).toContain("size-11!");
});
});
@@ -4,6 +4,13 @@ import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
// Toasts sit at the bottom: at the top they cover the page heading and the
// notification bell. One offset for every width: --toast-offset-bottom
// (globals.css) clears the bottom nav and the quick-add button below the app's
// sm breakpoint (640 px) and the sticky save bar everywhere. Sonner switches to
// its phone layout at 600 px, so both offsets take the same variable.
const TOAST_OFFSET = { bottom: "var(--toast-offset-bottom)", left: 16, right: 16 };
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
@@ -11,6 +18,9 @@ const Toaster = ({ ...props }: ToasterProps) => {
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
position="bottom-right"
offset={TOAST_OFFSET}
mobileOffset={TOAST_OFFSET}
icons={{
success: (
<CircleCheckIcon className="size-4" />
@@ -39,6 +49,10 @@ const Toaster = ({ ...props }: ToasterProps) => {
toastOptions={{
classNames: {
toast: "cn-toast",
// Sonner's own buttons are 24 px high; a tap target needs 44 px.
actionButton: "h-11! min-h-11! min-w-11! px-4!",
closeButton: "size-11!",
cancelButton: "h-11! min-h-11! min-w-11! px-4!",
},
}}
{...props}