mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-22 05:19:54 +08:00
Sidebar: 11 separator groups become 5, with each module a collapsible folder that auto-opens on the active page. 61 always-visible rows drop to 16. Groups mirror the app's own nav (Chats/Workspace/Workflows) rather than inventing a taxonomy; Enterprise and Self-Hosting are hoisted out of Platform. Chrome: register the `hover-hover` variant, without which every @sim/emcn hover state silently compiled to nothing; restore the sidebar's Geist font stack; add 11 emcn tokens that were falling back to currentColor; adopt the named type scale; align row geometry, hover tokens and group labels with the app. Rename: mothership/ -> chat/ with redirects for the old URLs. Asset paths, the @mothership.sim.ai domain and the `mothership` log-trigger enum value are deliberately left alone -- they are CDN objects, a real domain, and a live product value. Also removes the page-type badge, drops the "Next" heading from the ToC, and lets FAQ rows open independently so expanding one no longer shifts the page.
109 lines
2.4 KiB
TypeScript
109 lines
2.4 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import { cn, getAssetUrl } from '@/lib/utils'
|
|
import { Lightbox } from './lightbox'
|
|
|
|
interface VideoProps {
|
|
src: string
|
|
className?: string
|
|
autoPlay?: boolean
|
|
loop?: boolean
|
|
muted?: boolean
|
|
playsInline?: boolean
|
|
enableLightbox?: boolean
|
|
width?: number
|
|
height?: number
|
|
}
|
|
|
|
export function Video({
|
|
src,
|
|
className = 'w-full',
|
|
autoPlay = true,
|
|
loop = true,
|
|
muted = true,
|
|
playsInline = true,
|
|
enableLightbox = true,
|
|
width,
|
|
height,
|
|
}: VideoProps) {
|
|
const videoRef = useRef<HTMLVideoElement>(null)
|
|
const startTimeRef = useRef(0)
|
|
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
|
|
const [isInView, setIsInView] = useState(false)
|
|
|
|
useEffect(() => {
|
|
const el = videoRef.current
|
|
if (!el) return
|
|
|
|
if (typeof IntersectionObserver === 'undefined') {
|
|
setIsInView(true)
|
|
return
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting) {
|
|
setIsInView(true)
|
|
observer.disconnect()
|
|
}
|
|
},
|
|
{ rootMargin: '200px' }
|
|
)
|
|
observer.observe(el)
|
|
return () => observer.disconnect()
|
|
}, [])
|
|
|
|
const openLightbox = () => {
|
|
startTimeRef.current = videoRef.current?.currentTime ?? 0
|
|
setIsLightboxOpen(true)
|
|
}
|
|
|
|
const video = (
|
|
<video
|
|
ref={videoRef}
|
|
autoPlay={isInView && autoPlay}
|
|
loop={loop}
|
|
muted={muted}
|
|
playsInline={playsInline}
|
|
preload='none'
|
|
width={width}
|
|
height={height}
|
|
className={cn(
|
|
'overflow-hidden rounded-xl border border-[var(--border)] outline-none focus:outline-none',
|
|
enableLightbox && 'cursor-pointer transition-opacity group-hover:opacity-[0.97]',
|
|
className
|
|
)}
|
|
src={isInView ? getAssetUrl(src) : undefined}
|
|
/>
|
|
)
|
|
|
|
return (
|
|
<>
|
|
{enableLightbox ? (
|
|
<button
|
|
type='button'
|
|
onClick={openLightbox}
|
|
aria-label={`Open ${src} in media viewer`}
|
|
className='group contents'
|
|
>
|
|
{video}
|
|
</button>
|
|
) : (
|
|
video
|
|
)}
|
|
|
|
{enableLightbox && (
|
|
<Lightbox
|
|
isOpen={isLightboxOpen}
|
|
onClose={() => setIsLightboxOpen(false)}
|
|
src={src}
|
|
alt={`Video: ${src}`}
|
|
type='video'
|
|
startTime={startTimeRef.current}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|