feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s

- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
This commit is contained in:
Bryan1029384756
2026-04-14 09:02:14 -05:00
parent 9ef839938e
commit b7a4cf4ce8
376 changed files with 52619 additions and 167641 deletions

View File

@@ -0,0 +1,133 @@
/**
* PiPOverlay — floating voice-call status widget shown when the user
* is connected to a voice channel but has navigated to a different
* channel / DM / route. Presents the channel name, a "Return to
* channel" button, and a disconnect button. The widget is draggable
* via pointer events.
*
* The new UI's screen-share video PiP required live LiveKit track
* binding from a MobX store; our Convex/React voice context doesn't
* expose that the same way, so this is a simpler status widget
* focused on the common case: "I'm in a call, I want to get back."
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { PhoneDisconnect, ArrowRight } from '@phosphor-icons/react';
import { useVoice } from '../../contexts/VoiceContext';
import styles from './PiPOverlay.module.css';
interface Box {
x: number;
y: number;
}
const WIDTH = 260;
const HEIGHT = 96;
function clamp(v: number, min: number, max: number): number {
return Math.max(min, Math.min(max, v));
}
export function PiPOverlay() {
const navigate = useNavigate();
const location = useLocation();
const voice = useVoice() as any;
const [box, setBox] = useState<Box>(() => ({
x: Math.max(16, window.innerWidth - WIDTH - 24),
y: Math.max(16, window.innerHeight - HEIGHT - 24),
}));
const dragRef = useRef<{ startX: number; startY: number; startBox: Box } | null>(null);
useEffect(() => {
const handleMove = (e: PointerEvent) => {
const drag = dragRef.current;
if (!drag) return;
const dx = e.clientX - drag.startX;
const dy = e.clientY - drag.startY;
setBox({
x: clamp(drag.startBox.x + dx, 0, window.innerWidth - WIDTH),
y: clamp(drag.startBox.y + dy, 0, window.innerHeight - HEIGHT),
});
};
const handleUp = () => {
dragRef.current = null;
};
window.addEventListener('pointermove', handleMove);
window.addEventListener('pointerup', handleUp);
return () => {
window.removeEventListener('pointermove', handleMove);
window.removeEventListener('pointerup', handleUp);
};
}, []);
const startDrag = useCallback(
(e: React.PointerEvent) => {
// Don't initiate a drag when the pointer lands on a button.
const target = e.target as HTMLElement;
if (target.closest('button')) return;
e.preventDefault();
dragRef.current = {
startX: e.clientX,
startY: e.clientY,
startBox: { ...box },
};
},
[box],
);
const activeChannelId: string | null = voice?.activeChannelId ?? null;
const activeChannelName: string | null = voice?.activeChannelName ?? null;
// Hide the overlay when not in a call or when the user is already
// looking at the active voice channel.
if (!activeChannelId) return null;
const onActiveChannelRoute =
location.pathname.includes(`/${activeChannelId}`);
if (onActiveChannelRoute) return null;
const handleReturn = () => {
navigate(`/channels/home/${activeChannelId}`);
};
const handleDisconnect = () => {
voice?.disconnect?.();
};
return (
<div
className={styles.container}
style={{
transform: `translate(${box.x}px, ${box.y}px)`,
width: WIDTH,
height: HEIGHT,
}}
onPointerDown={startDrag}
>
<div className={styles.pipContent}>
<div className={styles.pipLabel}>In voice</div>
<div className={styles.pipChannel}>
#{activeChannelName || 'channel'}
</div>
<div className={styles.pipActions}>
<button
type="button"
className={styles.pipPrimary}
onClick={handleReturn}
aria-label="Return to channel"
>
<ArrowRight size={14} weight="bold" />
<span>Return</span>
</button>
<button
type="button"
className={styles.pipDanger}
onClick={handleDisconnect}
aria-label="Disconnect"
>
<PhoneDisconnect size={14} weight="bold" />
</button>
</div>
</div>
</div>
);
}