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