feat(web): Kalender-Widget zeigt je Kalender einen kleinen Kreis am Tag; Changelog
- Plaketten-Wrapper calendar-day-badges rendert buildDayBadges(day.events) statt der alten Einzel-Plakette - Ein Kalender: unveraendert eine Plakette in Kalenderfarbe/Akzentfarbe (Tests 3/3b/3c bleiben gruen) - Zwei/drei Kalender: je ein kleiner Kreis pro Kalender in Startreihenfolge (Test 3d) - Vier oder mehr Kalender: zwei Kreise plus grauer Restkreis bg-muted-foreground/text-background mit Summe (Test 3e) - CHANGELOG-Stichpunkt erweitert (mehrere Kalender am selben Tag) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
|
|||||||
### Geändert
|
### Geändert
|
||||||
|
|
||||||
- Kalenderquellen: Adressfeld zeigt je Typ ein Beispiel; bei Exchange EWS Hinweis auf `/EWS/Exchange.asmx`
|
- Kalenderquellen: Adressfeld zeigt je Typ ein Beispiel; bei Exchange EWS Hinweis auf `/EWS/Exchange.asmx`
|
||||||
- Kalender-Widget: Plakette am Tag in der Farbe des Kalenders
|
- Kalender-Widget: Plakette am Tag in der Farbe des Kalenders; mehrere Kalender am selben Tag zeigen je einen kleinen Kreis
|
||||||
|
|
||||||
### Entfernt
|
### Entfernt
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,102 @@ describe('CalendarWidget', () => {
|
|||||||
expect(within(tooltip).getAllByTestId('tooltip-color-dot')).toHaveLength(1);
|
expect(within(tooltip).getAllByTestId('tooltip-color-dot')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Test 3d: zwei Kalender am selben Tag zeigen zwei kleine Kreise in Startreihenfolge', async () => {
|
||||||
|
mockFetchEvents.mockResolvedValue([
|
||||||
|
ev('Lunch', new Date(2026, 6, 20, 10, 0), new Date(2026, 6, 20, 11, 0), {
|
||||||
|
sourceId: 's2',
|
||||||
|
color: '#4060c4',
|
||||||
|
}),
|
||||||
|
ev('Kaffee', new Date(2026, 6, 20, 14, 0), new Date(2026, 6, 20, 15, 0), {
|
||||||
|
sourceId: 's2',
|
||||||
|
color: '#4060c4',
|
||||||
|
}),
|
||||||
|
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0), {
|
||||||
|
sourceId: 's1',
|
||||||
|
color: '#c44040',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { CalendarWidget } = await import('./calendar-widget');
|
||||||
|
render(<CalendarWidget instanceId="cal-3d" config={{}} isEditMode={false} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Juli 2026')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const day20 = document.querySelector('[data-date="2026-07-20"]') as HTMLElement;
|
||||||
|
const badges = within(day20).getAllByTestId('calendar-day-count');
|
||||||
|
expect(badges).toHaveLength(2);
|
||||||
|
expect(badges[0]).toHaveTextContent('1');
|
||||||
|
expect(badges[0]).toHaveStyle({ backgroundColor: '#c44040' });
|
||||||
|
expect(badges[1]).toHaveTextContent('2');
|
||||||
|
expect(badges[1]).toHaveStyle({ backgroundColor: '#4060c4' });
|
||||||
|
for (const badge of badges) {
|
||||||
|
expect(badge).toHaveClass('text-white');
|
||||||
|
expect(badge).not.toHaveClass('bg-primary');
|
||||||
|
expect(badge).toHaveClass('h-[clamp(8px,2.4cqw,12px)]');
|
||||||
|
}
|
||||||
|
expect(within(day20).queryByTestId('calendar-day-count-rest')).toBeNull();
|
||||||
|
expect(within(day20).getByTestId('calendar-day-badges').childElementCount).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Test 3e: vier Kalender am selben Tag zeigen zwei Kreise plus grauen Restkreis mit Summe', async () => {
|
||||||
|
mockFetchEvents.mockResolvedValue([
|
||||||
|
ev('e1', new Date(2026, 6, 21, 8, 0), new Date(2026, 6, 21, 9, 0), {
|
||||||
|
sourceId: 's1',
|
||||||
|
color: '#111111',
|
||||||
|
}),
|
||||||
|
ev('e2a', new Date(2026, 6, 21, 9, 0), new Date(2026, 6, 21, 9, 30), {
|
||||||
|
sourceId: 's2',
|
||||||
|
color: '#222222',
|
||||||
|
}),
|
||||||
|
ev('e2b', new Date(2026, 6, 21, 9, 30), new Date(2026, 6, 21, 10, 0), {
|
||||||
|
sourceId: 's2',
|
||||||
|
color: '#222222',
|
||||||
|
}),
|
||||||
|
ev('e3', new Date(2026, 6, 21, 10, 0), new Date(2026, 6, 21, 11, 0), {
|
||||||
|
sourceId: 's3',
|
||||||
|
color: undefined,
|
||||||
|
}),
|
||||||
|
ev('e4a', new Date(2026, 6, 21, 11, 0), new Date(2026, 6, 21, 12, 0), {
|
||||||
|
sourceId: 's4',
|
||||||
|
color: '#444444',
|
||||||
|
}),
|
||||||
|
ev('e4b', new Date(2026, 6, 21, 12, 0), new Date(2026, 6, 21, 13, 0), {
|
||||||
|
sourceId: 's4',
|
||||||
|
color: '#444444',
|
||||||
|
}),
|
||||||
|
ev('e4c', new Date(2026, 6, 21, 13, 0), new Date(2026, 6, 21, 14, 0), {
|
||||||
|
sourceId: 's4',
|
||||||
|
color: '#444444',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { CalendarWidget } = await import('./calendar-widget');
|
||||||
|
render(<CalendarWidget instanceId="cal-3e" config={{}} isEditMode={false} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Juli 2026')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const day21 = document.querySelector('[data-date="2026-07-21"]') as HTMLElement;
|
||||||
|
expect(within(day21).getByTestId('calendar-day-badges').childElementCount).toBe(3);
|
||||||
|
|
||||||
|
const badges = within(day21).getAllByTestId('calendar-day-count');
|
||||||
|
expect(badges).toHaveLength(2);
|
||||||
|
expect(badges[0]).toHaveTextContent('1');
|
||||||
|
expect(badges[0]).toHaveStyle({ backgroundColor: '#111111' });
|
||||||
|
expect(badges[1]).toHaveTextContent('2');
|
||||||
|
expect(badges[1]).toHaveStyle({ backgroundColor: '#222222' });
|
||||||
|
|
||||||
|
const restBadge = within(day21).getByTestId('calendar-day-count-rest');
|
||||||
|
expect(restBadge).toHaveTextContent('4');
|
||||||
|
expect(restBadge).toHaveClass('bg-muted-foreground');
|
||||||
|
expect(restBadge).toHaveClass('text-background');
|
||||||
|
expect((restBadge as HTMLElement).style.backgroundColor).toBe('');
|
||||||
|
expect(restBadge).not.toHaveClass('text-white');
|
||||||
|
});
|
||||||
|
|
||||||
it('Test 4: Tooltip erscheint per Portal im body und zeigt bis zu 5 Termine plus Hinweis', async () => {
|
it('Test 4: Tooltip erscheint per Portal im body und zeigt bis zu 5 Termine plus Hinweis', async () => {
|
||||||
mockFetchEvents.mockResolvedValue([
|
mockFetchEvents.mockResolvedValue([
|
||||||
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
ev('Team Meeting', new Date(2026, 6, 20, 9, 0), new Date(2026, 6, 20, 10, 0)),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { fetchEvents, fetchSources } from '@/lib/calendar-api';
|
|||||||
import type { CalendarEvent } from '@/lib/calendar-api';
|
import type { CalendarEvent } from '@/lib/calendar-api';
|
||||||
import {
|
import {
|
||||||
buildCalendarDays,
|
buildCalendarDays,
|
||||||
|
buildDayBadges,
|
||||||
computeFetchWindow,
|
computeFetchWindow,
|
||||||
formatEventDate,
|
formatEventDate,
|
||||||
formatEventTime,
|
formatEventTime,
|
||||||
@@ -30,9 +31,11 @@ const TOOLTIP_EDGE_PX = 4;
|
|||||||
*
|
*
|
||||||
* Oben ein Monatsraster (Nav-Zeile Zurueck/Monat/Weiter, Wochentagskopf,
|
* Oben ein Monatsraster (Nav-Zeile Zurueck/Monat/Weiter, Wochentagskopf,
|
||||||
* 42 Zellen ab Montag, Faehrmonatstage gedaempft, heutiger Tag hervorgehoben,
|
* 42 Zellen ab Montag, Faehrmonatstage gedaempft, heutiger Tag hervorgehoben,
|
||||||
* Zaehl-Plakette an Tagen mit Terminen in der Farbe des Kalenders des
|
* Zaehl-Plakette an Tagen mit Terminen in der Farbe des Kalenders (ein
|
||||||
* fruehesten Termins des Tages (sonst Akzentfarbe), Tooltip beim Ueberfahren
|
* Kalender am Tag); bei mehreren Kalendern am selben Tag je ein kleiner
|
||||||
* mit Farbpunkt je Zeile (quick-260916-jvj)), darunter
|
* Kreis pro Kalender (max. drei, danach zwei plus grauer Restkreis mit
|
||||||
|
* Summe, Logik in `buildDayBadges`, quick-260916-k2z), Tooltip beim
|
||||||
|
* Ueberfahren mit Farbpunkt je Zeile (quick-260916-jvj)), darunter
|
||||||
* der Block "Naechste Termine". Beide Bloecke lassen sich unabhaengig
|
* der Block "Naechste Termine". Beide Bloecke lassen sich unabhaengig
|
||||||
* ein-/ausblenden (Config `showMonth`/`maxEvents`, siehe calendar-month.ts).
|
* ein-/ausblenden (Config `showMonth`/`maxEvents`, siehe calendar-month.ts).
|
||||||
*
|
*
|
||||||
@@ -219,6 +222,8 @@ export function CalendarWidget({ config }: WidgetProps) {
|
|||||||
<div className="grid grid-cols-7 gap-px">
|
<div className="grid grid-cols-7 gap-px">
|
||||||
{days.map((day) => {
|
{days.map((day) => {
|
||||||
const hasEvents = day.events.length > 0;
|
const hasEvents = day.events.length > 0;
|
||||||
|
const badges = buildDayBadges(day.events);
|
||||||
|
const single = badges.length === 1;
|
||||||
const cellClass = [
|
const cellClass = [
|
||||||
'widgetNoDrag relative flex min-h-[clamp(16px,5.5cqh,40px)] items-start rounded bg-muted/50 px-1 py-0.5 text-[clamp(9px,2.4cqw,13px)] leading-none',
|
'widgetNoDrag relative flex min-h-[clamp(16px,5.5cqh,40px)] items-start rounded bg-muted/50 px-1 py-0.5 text-[clamp(9px,2.4cqw,13px)] leading-none',
|
||||||
day.inCurrentMonth ? 'text-foreground' : 'text-muted-foreground/60',
|
day.inCurrentMonth ? 'text-foreground' : 'text-muted-foreground/60',
|
||||||
@@ -243,25 +248,40 @@ export function CalendarWidget({ config }: WidgetProps) {
|
|||||||
onMouseLeave={() => setHover(null)}
|
onMouseLeave={() => setHover(null)}
|
||||||
>
|
>
|
||||||
<span>{day.date.getDate()}</span>
|
<span>{day.date.getDate()}</span>
|
||||||
{hasEvents &&
|
{hasEvents && (
|
||||||
(() => {
|
// Wrapper traegt die Positionierung, die einzelnen Kreise nicht
|
||||||
const badgeColor = day.events[0]?.color;
|
// mehr (mehrere Kreise stehen sonst uebereinander).
|
||||||
const badgeClass = [
|
<span
|
||||||
'absolute bottom-px right-px flex h-[clamp(10px,3cqw,16px)] min-w-[clamp(10px,3cqw,16px)] items-center justify-center rounded-full px-0.5 text-[clamp(7px,1.8cqw,10px)] font-semibold leading-none',
|
data-testid="calendar-day-badges"
|
||||||
badgeColor ? 'text-white' : 'bg-primary text-primary-foreground',
|
className="absolute bottom-px right-px flex items-end gap-px"
|
||||||
]
|
>
|
||||||
.filter(Boolean)
|
{badges.map((badge) => {
|
||||||
.join(' ');
|
const badgeClass = [
|
||||||
return (
|
'flex items-center justify-center rounded-full px-0.5 font-semibold leading-none',
|
||||||
<span
|
single
|
||||||
data-testid="calendar-day-count"
|
? 'h-[clamp(10px,3cqw,16px)] min-w-[clamp(10px,3cqw,16px)] text-[clamp(7px,1.8cqw,10px)]'
|
||||||
className={badgeClass}
|
: 'h-[clamp(8px,2.4cqw,12px)] min-w-[clamp(8px,2.4cqw,12px)] text-[clamp(6px,1.5cqw,8px)]',
|
||||||
style={badgeColor ? { backgroundColor: badgeColor } : undefined}
|
badge.rest
|
||||||
>
|
? 'bg-muted-foreground text-background'
|
||||||
{day.events.length}
|
: badge.color
|
||||||
</span>
|
? 'text-white'
|
||||||
);
|
: 'bg-primary text-primary-foreground',
|
||||||
})()}
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ');
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={badge.key}
|
||||||
|
data-testid={badge.rest ? 'calendar-day-count-rest' : 'calendar-day-count'}
|
||||||
|
className={badgeClass}
|
||||||
|
style={badge.color && !badge.rest ? { backgroundColor: badge.color } : undefined}
|
||||||
|
>
|
||||||
|
{badge.count}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user