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:
@@ -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) */}
|
||||
|
||||
Reference in New Issue
Block a user