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,92 @@
import { CheckCircle2, Circle, ClipboardList, PlayCircle, Repeat, Sparkles, XCircle, type LucideIcon } from "lucide-react";
import { Task, TaskStatus } from "@/lib/tasks/types";
import { TASK_STATUS_LABELS } from "@/lib/tasks/labels";
import { DueState } from "@/lib/tasks/today";
import { cn } from "@/lib/utils";
const STATUS_META: Record<TaskStatus, { icon: LucideIcon; className: string }> = {
OPEN: { icon: Circle, className: "bg-muted text-foreground/80" },
IN_PROGRESS: {
icon: PlayCircle,
className: "bg-[color-mix(in_oklch,var(--chart-5)_16%,var(--card))] text-foreground",
},
DONE: { icon: CheckCircle2, className: "bg-success/15 text-foreground" },
CANCELLED: { icon: XCircle, className: "bg-muted text-muted-foreground line-through" },
};
/** Status as icon AND word - never colour alone. */
export function TaskStatusPill({ status, className }: { status: TaskStatus; className?: string }) {
const meta = STATUS_META[status];
const Icon = meta.icon;
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
meta.className,
className,
)}
>
<Icon className="size-3.5" aria-hidden />
{TASK_STATUS_LABELS[status]}
</span>
);
}
/**
* Where a task came from. An automatic task is a suggestion to look at
* something - it says nothing about work having been done.
*/
export function TaskSourceBadge({
task,
}: {
task: Pick<Task, "sourceAutomationTrigger" | "sourceTemplateId" | "sourceInspectionId">;
}) {
const source = task.sourceAutomationTrigger
? { icon: Sparkles, label: "Vorschlag", title: "Automatisch vorgeschlagen – noch nichts erledigt" }
: task.sourceTemplateId
? { icon: Repeat, label: "Wiederkehrend", title: "Aus einer wiederkehrenden Vorlage" }
: task.sourceInspectionId
? { icon: ClipboardList, label: "Aus Durchsicht", title: "Bei einer Durchsicht angelegt" }
: null;
if (!source) return null;
const Icon = source.icon;
return (
<span
title={source.title}
className="inline-flex items-center gap-1 rounded-full border border-dashed border-primary/45 px-2 py-0.5 text-xs text-foreground/80"
>
<Icon className="size-3.5 text-primary" aria-hidden />
{source.label}
</span>
);
}
const DUE_LABEL: Record<Exclude<DueState, "none">, string> = {
overdue: "Überfällig",
today: "Heute",
upcoming: "Geplant",
};
export function DueChip({ state, dueDate }: { state: DueState; dueDate: string | null }) {
if (state === "none" || !dueDate) {
return <span className="text-xs text-muted-foreground">ohne Termin</span>;
}
const date = new Date(dueDate).toLocaleDateString("de-DE", {
day: "numeric",
month: "short",
timeZone: "UTC",
});
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
state === "overdue" && "bg-warning/20 text-foreground",
state === "today" && "bg-primary/12 text-foreground",
state === "upcoming" && "bg-muted text-muted-foreground",
)}
>
{DUE_LABEL[state]}
{state !== "today" ? <span className="font-normal">· {date}</span> : null}
</span>
);
}