feat(web): Dashboard mit Begruessung, Befehlsleiste und ruhigeren Kacheln

Begruessung nach Tageszeit mit Datum, Befehlsleiste oben rechts statt
schwebendem Knopf, einheitliche Kachel-Kopfzeilen mit Symbol, Kalender
mit Pfeilknoepfen, runden Tagen und Terminpunkten, Favoriten als
36-px-Zeilen bzw. App-Starter-Kacheln, Notiz-Kaestchen im Akzent,
leerer Zustand mit drei Vorschlaegen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-26 13:09:05 +02:00
parent 228112014c
commit b06a2d1066
18 changed files with 484 additions and 135 deletions
@@ -1,5 +1,6 @@
'use client';
import { WidgetIcon } from './widget-icon';
import { type FormEvent, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslations } from 'next-intl';
import { updateWidgetConfig } from '@/lib/dashboard-api';
@@ -322,6 +323,7 @@ export function FavoritesWidget({
{/* Header: optional title (quick-260916-iex) — mirrors note-widget.tsx look */}
{showHeader && (
<div className="widget-head">
<WidgetIcon type="favorites" />
{isEditMode ? (
<input
type="text"
@@ -396,10 +398,11 @@ export function FavoritesWidget({
viewMode === 'grid' ? (
<div
data-testid="favorites-grid"
className="grid grid-cols-3 gap-2"
className="grid grid-cols-[repeat(auto-fill,minmax(76px,1fr))] gap-1"
>
{sortedFavorites.map((fav, index) => (
<FavoriteTile
launcher
key={fav.id}
fav={fav}
isEditMode={isEditMode}
@@ -429,7 +432,7 @@ export function FavoritesWidget({
) : (
<div
data-testid="favorites-list"
className="flex flex-col gap-1"
className="flex flex-col gap-0.5"
>
{sortedFavorites.map((fav, index) => (
<FavoriteTile
@@ -558,10 +561,16 @@ function getDirectFaviconSrc(url: string): string | null {
function FavoriteIcon({
fav,
getFallbackLetter,
large = false,
}: {
fav: FavoriteLink;
getFallbackLetter: (title: string) => string;
/** Kachelansicht: 40-px-Feld mit 24-px-Symbol, sonst 24-px-Feld mit 16-px-Symbol. */
large?: boolean;
}) {
const box = large ? 'h-10 w-10 rounded-lg' : 'h-6 w-6 rounded-md';
const img = large ? 'h-6 w-6' : 'h-4 w-4';
const px = large ? 24 : 16;
const hasServerIcon = Boolean(fav.iconUrl) || Boolean(fav.uploadedIconMime);
const proxySrc = hasServerIcon
? `/api-proxy/favorites/${encodeURIComponent(fav.id)}/icon?v=${fav.iconVersion ?? 0}`
@@ -572,10 +581,10 @@ function FavoriteIcon({
);
return (
<div className="relative flex-shrink-0 w-5 h-5">
<div className={`relative flex shrink-0 items-center justify-center bg-muted ${box}`}>
<span
data-testid={`letter-fallback-${fav.id}`}
className="absolute inset-0 flex items-center justify-center text-xs font-semibold bg-muted rounded"
className={`absolute inset-0 flex items-center justify-center font-semibold text-muted-foreground ${large ? 'text-base' : 'text-xs'}`}
>
{getFallbackLetter(fav.title)}
</span>
@@ -585,10 +594,10 @@ function FavoriteIcon({
src={proxySrc}
alt=""
aria-hidden="true"
width={20}
height={20}
width={px}
height={px}
loading="lazy"
className="absolute inset-0 w-5 h-5 rounded"
className={`relative rounded-sm ${img}`}
onError={() => setStage('direct')}
/>
)}
@@ -598,11 +607,11 @@ function FavoriteIcon({
src={directSrc}
alt=""
aria-hidden="true"
width={20}
height={20}
width={px}
height={px}
loading="lazy"
referrerPolicy="no-referrer"
className="absolute inset-0 w-5 h-5 rounded"
className={`relative rounded-sm ${img}`}
onError={() => setStage('none')}
/>
)}
@@ -638,6 +647,8 @@ type FavoriteTileProps = {
canMoveDown: boolean;
onMove: (id: string, direction: 'up' | 'down') => void;
t: (key: string) => string;
/** Kachelansicht wie ein App-Starter (Symbol oben, Name darunter). */
launcher?: boolean;
};
function FavoriteTile({
@@ -663,19 +674,24 @@ function FavoriteTile({
canMoveDown,
onMove,
t,
launcher = false,
}: FavoriteTileProps) {
const isEditing = editingId === fav.id;
return (
<div className="flex flex-col">
{/* Main link row */}
<div className="flex items-center gap-2">
<div className={launcher ? 'flex flex-col items-stretch gap-1' : 'flex items-center gap-2'}>
{/* Anchor — opens in new tab, no server-side redirect (T-08-08) */}
<a
href={fav.url}
target="_blank"
rel="noreferrer"
className="flex h-8 min-w-0 flex-1 items-center gap-2.5 rounded-md px-1.5 transition-colors hover:bg-muted widgetNoDrag"
className={
launcher
? 'flex min-w-0 flex-col items-center gap-1.5 rounded-lg px-1 py-2 text-center transition-colors hover:bg-muted widgetNoDrag'
: 'flex h-9 min-w-0 flex-1 items-center gap-2.5 rounded-md px-1.5 transition-colors hover:bg-muted widgetNoDrag'
}
onClick={(e) => isEditMode && e.preventDefault()} // block navigation in edit mode
>
{/* Icon container with letter fallback (T-08-07 — no dangerouslySetInnerHTML) */}
@@ -683,9 +699,12 @@ function FavoriteTile({
key={`${fav.iconUrl ?? ''}|${fav.url}|${fav.iconVersion ?? 0}|${fav.uploadedIconMime ?? ''}`}
fav={fav}
getFallbackLetter={getFallbackLetter}
large={launcher}
/>
<span className="truncate text-sm">{fav.title}</span>
<span className={launcher ? 'line-clamp-2 w-full break-words text-xs leading-4' : 'truncate text-sm'}>
{fav.title}
</span>
</a>
{/* Action buttons (edit mode only, D-04) */}