a04ecf7a19
- Voice messages: MediaRecorder → encrypted attachment, custom waveform player via OfflineAudioContext, 60s limit + live mic-level meter - Offline message queue: localStorage outbox, exponential backoff retries, optimistic pending bubble with retry/discard - Delivery indicator: message_deliveries table + RLS (reciprocal receipts), ✓ / ✓✓ / ✓✓-blue tick states, group-aware (all members must ack) - Per-participant volume slider in calls via right-click tile menu, persisted to localStorage, applied to attached audio elements - Group call scaling: grid up to 12 tiles with pagination, active-speaker auto-promotion in fullscreen - Push notifications scaffolding: service worker, VAPID subscription registration, notify-push edge function skeleton - Backup recovery code: 24-char base32 code (~120 bits entropy) as alternative decrypt path, restore UI with mode toggle - Admin panel: conversations list, audit log (admin_audit_log table + admin_log_action RPC), audit entry on user flag toggle - Search v2: sender filter, attachment-only toggle, date range - Reactions pop animation (scale 0.4→1.15→1 on count change) - Message list windowing (150 default, expand via IntersectionObserver) - Stub cleanup: removed dead ScreenshareStub from CallParticipantTile Fixes: - Focus-triggered flicker: dropped window.focus listeners in three spots, throttled visibilitychange/online wake-refreshes to 30s, keep existing data visible during background re-syncs (no more spinner on every click) - Voice attachment audio element collapsed to 0px on peer side — now forces 280px min-width on bubble Migrations (push required): 20260421000001_message_deliveries.sql 20260421000002_admin_audit_log.sql Server TODO: VAPID keys + notify-push edge function deploy
131 lines
4.3 KiB
JavaScript
131 lines
4.3 KiB
JavaScript
/** @type {import('tailwindcss').Config} */
|
|
export default {
|
|
darkMode: 'class',
|
|
content: [
|
|
'./index.html',
|
|
'./src/**/*.{ts,tsx}',
|
|
'../../packages/ui-web/src/**/*.{ts,tsx}',
|
|
],
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: [
|
|
'Inter',
|
|
'ui-sans-serif',
|
|
'system-ui',
|
|
'-apple-system',
|
|
'Segoe UI',
|
|
'Roboto',
|
|
'Helvetica Neue',
|
|
'Arial',
|
|
'sans-serif',
|
|
],
|
|
display: ['Outfit', 'Inter', 'system-ui', 'sans-serif'],
|
|
mono: [
|
|
'JetBrains Mono',
|
|
'ui-monospace',
|
|
'SFMono-Regular',
|
|
'Menlo',
|
|
'Monaco',
|
|
'Consolas',
|
|
'monospace',
|
|
],
|
|
},
|
|
colors: {
|
|
ink: {
|
|
950: '#050507',
|
|
900: '#0A0A0F',
|
|
800: '#111118',
|
|
700: '#1A1A24',
|
|
600: '#252533',
|
|
500: '#3A3A4D',
|
|
},
|
|
brand: {
|
|
50: '#EEF0FF',
|
|
100: '#DDE0FF',
|
|
200: '#BDC2FF',
|
|
300: '#9198FF',
|
|
400: '#6D73FF',
|
|
500: '#4F46E5',
|
|
600: '#4338CA',
|
|
700: '#3730A3',
|
|
800: '#312E81',
|
|
900: '#1E1B4B',
|
|
},
|
|
// Clean-Rail design tokens. RGB-tuple form so alpha modifiers
|
|
// (e.g. bg-surface/50) work under both themes.
|
|
surface: 'rgb(var(--bg-rgb) / <alpha-value>)',
|
|
'surface-2': 'rgb(var(--bg-2-rgb) / <alpha-value>)',
|
|
'surface-3': 'rgb(var(--bg-3-rgb) / <alpha-value>)',
|
|
fg: 'rgb(var(--fg-rgb) / <alpha-value>)',
|
|
'fg-muted': 'rgb(var(--fg-muted-rgb) / <alpha-value>)',
|
|
accent: 'rgb(var(--accent-rgb) / <alpha-value>)',
|
|
'accent-fg': 'rgb(var(--accent-fg-rgb) / <alpha-value>)',
|
|
// Line has baked-in theme-specific alpha — direct var, no modifier.
|
|
line: 'var(--line)',
|
|
},
|
|
animation: {
|
|
'blob-a': 'blob 22s ease-in-out infinite',
|
|
'blob-b': 'blob 28s ease-in-out infinite reverse',
|
|
'fade-in': 'fadeIn 400ms ease-out',
|
|
'slide-up': 'slideUp 350ms ease-out',
|
|
'pulse-ring': 'pulseRing 2s ease-out infinite',
|
|
'audio-pulse': 'audioPulse 1.3s ease-out infinite',
|
|
'live-dot': 'liveDotPulse 1.5s ease-in-out infinite',
|
|
'slide-in-call': 'slideInCall 0.4s cubic-bezier(0.22,1,0.36,1)',
|
|
'fs-hint': 'fsHintFade 3.5s ease-out forwards',
|
|
'reaction-pop': 'reactionPop 220ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
},
|
|
keyframes: {
|
|
blob: {
|
|
'0%, 100%': { transform: 'translate(0px, 0px) scale(1)' },
|
|
'33%': { transform: 'translate(40px, -60px) scale(1.1)' },
|
|
'66%': { transform: 'translate(-30px, 30px) scale(0.9)' },
|
|
},
|
|
fadeIn: {
|
|
from: { opacity: '0' },
|
|
to: { opacity: '1' },
|
|
},
|
|
slideUp: {
|
|
from: { opacity: '0', transform: 'translateY(8px)' },
|
|
to: { opacity: '1', transform: 'translateY(0)' },
|
|
},
|
|
pulseRing: {
|
|
'0%': { transform: 'scale(0.85)', opacity: '0.6' },
|
|
'100%': { transform: 'scale(1.25)', opacity: '0' },
|
|
},
|
|
audioPulse: {
|
|
'0%': { transform: 'scale(1)', opacity: '0.8' },
|
|
'100%': { transform: 'scale(1.3)', opacity: '0' },
|
|
},
|
|
liveDotPulse: {
|
|
'0%, 100%': { opacity: '1' },
|
|
'50%': { opacity: '0.4' },
|
|
},
|
|
slideInCall: {
|
|
from: { opacity: '0', transform: 'translateY(20px)' },
|
|
to: { opacity: '1', transform: 'translateY(0)' },
|
|
},
|
|
fsHintFade: {
|
|
'0%': { opacity: '0', transform: 'translate(-50%, -6px)' },
|
|
'15%, 75%': { opacity: '1', transform: 'translate(-50%, 0)' },
|
|
'100%': { opacity: '0', transform: 'translate(-50%, -6px)' },
|
|
},
|
|
reactionPop: {
|
|
'0%': { opacity: '0', transform: 'scale(0.4)' },
|
|
'70%': { opacity: '1', transform: 'scale(1.15)' },
|
|
'100%': { opacity: '1', transform: 'scale(1)' },
|
|
},
|
|
},
|
|
boxShadow: {
|
|
glow: '0 0 40px -8px rgba(99, 102, 241, 0.35)',
|
|
'call-card': '0 20px 60px rgba(0,0,0,0.1)',
|
|
'call-card-dark': '0 20px 60px rgba(0,0,0,0.5)',
|
|
'pip-call': '0 12px 40px rgba(0,0,0,0.2), 0 0 0 3px rgba(79,70,229,0.15)',
|
|
'accept-btn': '0 4px 14px rgba(22,163,74,0.3)',
|
|
},
|
|
},
|
|
},
|
|
plugins: [],
|
|
};
|