feat: add participant scheduling with iTIP invitations and inline calendar invitation banner

Add organizer/attendee UI with RSVP, contact autocomplete for participants,
scheduling messages, and inline calendar invitation banner in email viewer
with auto-detect .ics attachments, RSVP/import to calendar, and cancellation display.
This commit is contained in:
Matthieu MALVACHE
2026-02-17 01:09:57 +01:00
committed by Matthieu MALVACHE
parent 74763b384d
commit b65b2f26df
30 changed files with 2352 additions and 137 deletions
+27 -19
View File
@@ -131,29 +131,37 @@ export function transformColorForDarkMode(colorString: string): string {
const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
if (luminance < 0.4) {
const invR = 255 - rgb.r;
const invG = 255 - rgb.g;
const invB = 255 - rgb.b;
if (luminance >= 0.6) return colorString;
const boost = 1.3;
const r = Math.min(255, Math.round(invR * boost));
const g = Math.min(255, Math.round(invG * boost));
const b = Math.min(255, Math.round(invB * boost));
const blendFactor = 0.85 - (luminance / 0.6) * 0.55;
return rgb.a !== undefined ? `rgba(${r}, ${g}, ${b}, ${rgb.a})` : `rgb(${r}, ${g}, ${b})`;
const r = Math.min(255, Math.round(rgb.r + (255 - rgb.r) * blendFactor));
const g = Math.min(255, Math.round(rgb.g + (255 - rgb.g) * blendFactor));
const b = Math.min(255, Math.round(rgb.b + (255 - rgb.b) * blendFactor));
return rgb.a !== undefined ? `rgba(${r}, ${g}, ${b}, ${rgb.a})` : `rgb(${r}, ${g}, ${b})`;
}
export function transformBgColorForDarkMode(colorString: string): string {
const rgb = parseColor(colorString);
if (!rgb) return colorString;
if (rgb.a !== undefined && rgb.a < 0.1) {
return colorString;
}
if (luminance >= 0.4 && luminance < 0.6) {
const factor = 1.5;
const r = Math.min(255, Math.round(rgb.r + (255 - rgb.r) * factor * 0.4));
const g = Math.min(255, Math.round(rgb.g + (255 - rgb.g) * factor * 0.4));
const b = Math.min(255, Math.round(rgb.b + (255 - rgb.b) * factor * 0.4));
const luminance = getLuminance(rgb.r, rgb.g, rgb.b);
return rgb.a !== undefined ? `rgba(${r}, ${g}, ${b}, ${rgb.a})` : `rgb(${r}, ${g}, ${b})`;
}
if (luminance < 0.2) return colorString;
return colorString;
const blendFactor = Math.min(0.9, (luminance - 0.2) * 1.125);
const darkR = 30, darkG = 31, darkB = 38;
const r = Math.max(0, Math.round(rgb.r + (darkR - rgb.r) * blendFactor));
const g = Math.max(0, Math.round(rgb.g + (darkG - rgb.g) * blendFactor));
const b = Math.max(0, Math.round(rgb.b + (darkB - rgb.b) * blendFactor));
return rgb.a !== undefined ? `rgba(${r}, ${g}, ${b}, ${rgb.a})` : `rgb(${r}, ${g}, ${b})`;
}
export function transformInlineStyles(cssText: string, theme: 'light' | 'dark'): string {
@@ -180,7 +188,7 @@ export function transformInlineStyles(cssText: string, theme: 'light' | 'dark'):
if (property === 'background-color') {
const hasImportant = value.includes('!important');
const colorValue = value.replace('!important', '').trim();
const transformed = transformColorForDarkMode(colorValue);
const transformed = transformBgColorForDarkMode(colorValue);
return `${property}: ${transformed}${hasImportant ? ' !important' : ''}`;
}
@@ -189,7 +197,7 @@ export function transformInlineStyles(cssText: string, theme: 'light' | 'dark'):
if (colorMatch) {
const hasImportant = value.includes('!important');
const originalColor = colorMatch[0];
const transformed = transformColorForDarkMode(originalColor);
const transformed = transformBgColorForDarkMode(originalColor);
const newValue = value.replace(originalColor, transformed);
return `${property}: ${newValue.replace('!important', '').trim()}${hasImportant ? ' !important' : ''}`;
}