An entry for the streamplace vod showcase
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(web): add seek indicator for video preview on hover

- Progress bar at bottom shows current preview position
- Draggable-style handle follows the seek position
- "Hover to preview" hint appears briefly on first hover
- Visual feedback makes scrubbing behavior intuitive

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+89 -1
+89 -1
apps/web/src/ui/UI4Editorial.tsx
··· 395 395 z-index: 3; 396 396 } 397 397 398 + .editorial-card-seek { 399 + position: absolute; 400 + bottom: 0; 401 + left: 0; 402 + right: 0; 403 + height: 4px; 404 + background: rgba(255,255,255,0.2); 405 + z-index: 4; 406 + opacity: 0; 407 + transition: opacity 0.2s, height 0.2s; 408 + } 409 + 410 + .editorial-card-media:hover .editorial-card-seek { 411 + opacity: 1; 412 + } 413 + 414 + .editorial-card-seek-progress { 415 + height: 100%; 416 + background: var(--accent); 417 + border-radius: 0 2px 2px 0; 418 + transition: width 0.05s linear; 419 + position: relative; 420 + } 421 + 422 + .editorial-card-seek-progress::after { 423 + content: ''; 424 + position: absolute; 425 + right: -4px; 426 + top: 50%; 427 + transform: translateY(-50%); 428 + width: 8px; 429 + height: 8px; 430 + background: #fff; 431 + border-radius: 50%; 432 + box-shadow: 0 1px 3px rgba(0,0,0,0.3); 433 + opacity: 0; 434 + transition: opacity 0.15s; 435 + } 436 + 437 + .editorial-card-media:hover .editorial-card-seek-progress::after { 438 + opacity: 1; 439 + } 440 + 441 + .editorial-card-seek-hint { 442 + position: absolute; 443 + top: 50%; 444 + left: 50%; 445 + transform: translate(-50%, -50%); 446 + padding: 0.5rem 0.75rem; 447 + background: rgba(0,0,0,0.7); 448 + color: #fff; 449 + font-size: 0.75rem; 450 + font-weight: 500; 451 + border-radius: 4px; 452 + z-index: 5; 453 + opacity: 0; 454 + transition: opacity 0.3s; 455 + pointer-events: none; 456 + white-space: nowrap; 457 + } 458 + 459 + .editorial-card-media:hover .editorial-card-seek-hint { 460 + opacity: 1; 461 + animation: fadeOutHint 2s ease-out forwards; 462 + } 463 + 464 + @keyframes fadeOutHint { 465 + 0%, 50% { opacity: 1; } 466 + 100% { opacity: 0; } 467 + } 468 + 398 469 .editorial-card-title { 399 470 font-family: 'Instrument Serif', Georgia, serif; 400 471 font-size: 1.375rem; ··· 1063 1134 1064 1135 const currentFrame = previewFrame !== null ? frames[previewFrame] : null 1065 1136 const previewTime = currentFrame ? currentFrame.start : 0 1137 + const seekProgress = previewFrame !== null && frames.length > 0 1138 + ? ((previewFrame + 1) / frames.length) * 100 1139 + : 0 1140 + const hasPreview = spriteSheet && frames.length > 0 1066 1141 1067 1142 // Calculate background position for sprite (4 cols x 2 rows grid) 1068 1143 // Using percentages works regardless of sprite resolution (160x90 or 320x180) ··· 1095 1170 ) : ( 1096 1171 <ThumbnailFallback title={video.title} /> 1097 1172 )} 1098 - {spriteSheet && frames.length > 0 && previewFrame !== null && ( 1173 + {hasPreview && previewFrame !== null && ( 1099 1174 <> 1100 1175 <div className="editorial-card-preview" style={spriteStyle} /> 1101 1176 <span className="editorial-card-preview-time"> 1102 1177 {formatDuration(previewTime * 1_000_000_000)} 1103 1178 </span> 1179 + </> 1180 + )} 1181 + {hasPreview && ( 1182 + <> 1183 + <div className="editorial-card-seek"> 1184 + <div 1185 + className="editorial-card-seek-progress" 1186 + style={{ width: `${seekProgress}%` }} 1187 + /> 1188 + </div> 1189 + {previewFrame === null && ( 1190 + <span className="editorial-card-seek-hint">Hover to preview</span> 1191 + )} 1104 1192 </> 1105 1193 )} 1106 1194 <span className="editorial-card-duration">{formatDuration(video.duration)}</span>