feat: add UI/UX polish with navigation rail, confirm dialogs, welcome banner, and form validation

- Add NavigationRail component (desktop vertical icon sidebar + mobile bottom tab bar)
- Add ConfirmDialog with promise-based useConfirmDialog hook for async confirmation flow
- Add WelcomeBanner onboarding component (one-time display, localStorage persistence)
- Polish login form UX (shake on error, TOTP slide animation, password visibility toggle, session expired banner)
- Add inline form validation with shake animation in email composer and contacts
- Add empty state patterns for contacts (no data vs no search results with contextual actions)
- Improve toast notification system with undo action support and typed durations
- Add WCAG AA prefers-reduced-motion media query, safe area insets, sr-only live regions
- Add template settings tab and keyboard shortcut integration
- Update all 8 locale translations
This commit is contained in:
Matthieu MALVACHE
2026-02-17 02:31:50 +01:00
committed by Matthieu MALVACHE
parent 2636a88820
commit a43096485b
44 changed files with 2148 additions and 669 deletions
+10 -7
View File
@@ -41,17 +41,17 @@ function parseDuration(duration: string): number {
return totalMinutes;
}
function createEventDragPreview(title: string, color: string): HTMLElement {
function createEventDragPreview(title: string, timeRange: string, color: string): HTMLElement {
const el = document.createElement("div");
el.style.cssText = `
position: fixed; top: -9999px; left: 0;
padding: 6px 12px; border-radius: 6px;
background: ${color}40; border-left: 3px solid ${color};
color: ${color}; font-size: 12px; font-weight: 500;
max-width: 200px; white-space: nowrap; overflow: hidden;
max-width: 240px; white-space: nowrap; overflow: hidden;
text-overflow: ellipsis; pointer-events: none; z-index: 9999;
`;
el.textContent = title;
el.textContent = `${title} \u2022 ${timeRange}`;
document.body.appendChild(el);
return el;
}
@@ -80,7 +80,7 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
}));
const displayTitle = event.title || t("events.no_title");
e.dataTransfer.setData("text/plain", displayTitle);
const preview = createEventDragPreview(displayTitle, color);
const preview = createEventDragPreview(displayTitle, timeString, color);
e.dataTransfer.setDragImage(preview, 0, 0);
requestAnimationFrame(() => preview.remove());
setIsBeingDragged(true);
@@ -135,13 +135,16 @@ export function EventCard({ event, calendar, variant, onClick, isSelected, dragg
style={{ backgroundColor: `${color}30`, borderLeft: `3px solid ${color}`, color }}
>
<div className="font-medium truncate">{event.title || t("events.no_title")}</div>
{durationMinutes > 30 && (
{!event.showWithoutTime && (
<div className="opacity-80 text-[10px]">
{timeString}
</div>
)}
{durationMinutes > 30 && getParticipantCount(event) > 0 && (
<div className="flex items-center gap-0.5 opacity-70 text-[10px]">
{getParticipantCount(event) > 0 && (
<div
className="flex items-center gap-0.5 opacity-70 text-[10px]"
title={t("participants.count", { count: getParticipantCount(event) })}
>
<Users className="w-3 h-3" />
<span>{getParticipantCount(event)}</span>
</div>