feat: "Heute" start page with one next step, clear task states and motion levels

Start page (AuDHD-oriented, per the agreed "Heute zuerst" layout):
- One highlighted next step: started work first (easy re-entry after an
  interruption), then overdue, due today, then urgent undated tasks.
  Primary action "Erledigt", secondary "Beginnen", link to the related
  colony/apiary/queen.
- "Außerdem heute": at most five further tasks with a large honey-cell
  checkbox; undated automatic suggestions are included so a queenless
  colony does not sit unseen in the full list.
- Completing is the one authored motion moment: the cell fills, the tick
  draws, the row folds away and the next step arrives with a short focus
  pull. Every completion can be undone from the toast (10 s), restoring
  the previous status.
- Status is shown as icon plus word (never colour alone); tasks created by
  automation carry a "Vorschlag" badge, recurring and inspection tasks
  their own - a suggestion never looks like work already done.
- Friendly empty, loading and error states; key figures moved into a
  compact overview below; overdue tasks are no longer listed twice.

Motion: per-device setting Voll / Dezent / Aus in the profile (default
Voll); the OS reduce-motion setting always lowers Voll to Dezent.
New 44 px "touch" button size for gloves at the apiary; themed text
selection and caret.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-27 13:00:44 +02:00
1 parent 5c2ea7bf1e
commit f9374a135a
14 files changed
+1104 -112

No files matched your search

@@ -0,0 +1,68 @@
"use client";
import { useCallback, useState } from "react";
/**
* How much the interface moves. A per-device preference (localStorage), not
* an account setting: the same person may want calm motion on the phone at
* the apiary and full feedback on the desktop.
*
* - full: feedback, transitions and the task-completion moment.
* - subtle: no spatial movement, only short fades and colour changes.
* - off: state changes happen instantly.
*
* The operating system's "reduce motion" setting always wins over "full"
* (see globals.css), so this only ever lowers motion further.
*/
export type MotionLevel = "full" | "subtle" | "off";
export const MOTION_LEVELS: MotionLevel[] = ["full", "subtle", "off"];
export const DEFAULT_MOTION_LEVEL: MotionLevel = "full";
const STORAGE_KEY = "apicore.motion";
function isMotionLevel(value: unknown): value is MotionLevel {
return value === "full" || value === "subtle" || value === "off";
}
export function readMotionLevel(): MotionLevel {
try {
const stored = window.localStorage.getItem(STORAGE_KEY);
return isMotionLevel(stored) ? stored : DEFAULT_MOTION_LEVEL;
} catch {
// Private mode / blocked storage: fall back to the default.
return DEFAULT_MOTION_LEVEL;
}
}
export function applyMotionLevel(level: MotionLevel): void {
document.documentElement.dataset.motion = level;
}
/** Effective level, including the operating system's reduce-motion setting. */
export function effectiveMotionLevel(level: MotionLevel = readMotionLevel()): MotionLevel {
const systemReduces =
typeof window.matchMedia === "function" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
return systemReduces && level === "full" ? "subtle" : level;
}
export function useMotionPreference(): [MotionLevel, (level: MotionLevel) => void] {
// Lazy read: the app shell renders only on the client after login, and the
// document attribute itself is applied once in Providers.
const [level, setLevelState] = useState<MotionLevel>(() =>
typeof window === "undefined" ? DEFAULT_MOTION_LEVEL : readMotionLevel(),
);
const setLevel = useCallback((next: MotionLevel) => {
setLevelState(next);
applyMotionLevel(next);
try {
window.localStorage.setItem(STORAGE_KEY, next);
} catch {
// Not persisted - still applies for this visit.
}
}, []);
return [level, setLevel];
}