Files
cinny/src/app/components/RoomSkeleton.tsx
T
Lotus Bot 3e9ca27761 feat: skeleton loaders, Sentry source maps, auto-deploy via webhook
RoomSkeleton: shimmer skeleton matching Room header/timeline/input layout,
  used as Suspense fallback for all three Room routes (home/direct/space)

Sentry source maps: @sentry/vite-plugin uploads 72 hidden source map files
  to Sentry on each build then deletes them from dist — stack traces now show
  real file/line numbers instead of minified bundle positions.
  Auth token loaded from /etc/lotus-deploy.env (not in git).

Auto-deploy: webhook receiver on port 9001, nginx proxies
  /hooks/lotus-deploy, HMAC-SHA256 verified, triggers on lotus branch push.
  Deploy script: git reset --hard + npm ci + npm run build + rsync to webroot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 20:30:44 -04:00

108 lines
4.0 KiB
TypeScript

import React, { useId } from 'react';
const MESSAGES = [
{ showAvatar: true, lines: [{ w: '55%' }, { w: '35%' }] },
{ showAvatar: false, lines: [{ w: '72%' }] },
{ showAvatar: false, lines: [{ w: '48%' }, { w: '60%' }] },
{ showAvatar: true, lines: [{ w: '80%' }] },
{ showAvatar: false, lines: [{ w: '40%' }] },
{ showAvatar: true, lines: [{ w: '65%' }, { w: '50%' }, { w: '30%' }] },
{ showAvatar: false, lines: [{ w: '58%' }] },
{ showAvatar: true, lines: [{ w: '45%' }] },
{ showAvatar: false, lines: [{ w: '70%' }, { w: '25%' }] },
];
export function RoomSkeleton() {
const id = useId().replace(/:/g, '');
const shimmerKeyframes = `
@keyframes shimmer-${id} {
0% { background-position: -400px 0; }
100% { background-position: 400px 0; }
}
`;
const shimmer = {
background: 'linear-gradient(90deg, var(--skeleton-base) 25%, var(--skeleton-shine) 50%, var(--skeleton-base) 75%)',
backgroundSize: '800px 100%',
animation: `shimmer-${id} 1.6s ease-in-out infinite`,
borderRadius: '4px',
} as React.CSSProperties;
return (
<>
<style>{shimmerKeyframes}</style>
<div
style={{
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
height: '100%',
overflow: 'hidden',
// CSS vars resolve against both light and dark themes automatically
'--skeleton-base': 'color-mix(in srgb, currentColor 8%, transparent)',
'--skeleton-shine': 'color-mix(in srgb, currentColor 15%, transparent)',
} as React.CSSProperties}
>
{/* Header — matches PageHeader size="600" (56px) */}
<div
style={{
height: '56px',
borderBottom: '1px solid color-mix(in srgb, currentColor 10%, transparent)',
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '0 16px',
flexShrink: 0,
}}
>
{/* Avatar */}
<div style={{ ...shimmer, width: '32px', height: '32px', borderRadius: '50%', flexShrink: 0 }} />
{/* Room name */}
<div style={{ ...shimmer, width: '140px', height: '16px' }} />
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* Icon buttons */}
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
<div style={{ ...shimmer, width: '24px', height: '24px', borderRadius: '4px' }} />
</div>
{/* Timeline */}
<div style={{ flex: 1, overflowY: 'hidden', padding: '16px 0' }}>
{MESSAGES.map((msg, i) => (
// eslint-disable-next-line react/no-array-index-key
<div key={i} style={{ display: 'flex', gap: '12px', padding: '4px 16px', alignItems: 'flex-start', marginBottom: msg.showAvatar ? '8px' : '2px' }}>
{/* Avatar — only shown on first message in a group */}
<div style={{ width: '36px', flexShrink: 0 }}>
{msg.showAvatar && (
<div style={{ ...shimmer, width: '36px', height: '36px', borderRadius: '50%' }} />
)}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', flex: 1 }}>
{/* Username on first in group */}
{msg.showAvatar && (
<div style={{ ...shimmer, width: '90px', height: '12px', marginBottom: '2px' }} />
)}
{msg.lines.map((line, j) => (
// eslint-disable-next-line react/no-array-index-key
<div key={j} style={{ ...shimmer, width: line.w, height: '14px' }} />
))}
</div>
</div>
))}
</div>
{/* Input bar */}
<div
style={{
borderTop: '1px solid color-mix(in srgb, currentColor 10%, transparent)',
padding: '12px 16px',
flexShrink: 0,
}}
>
<div style={{ ...shimmer, width: '100%', height: '44px', borderRadius: '8px' }} />
</div>
</div>
</>
);
}