source for getorbyt.com getorbyt.com/
client bsky orbytapp app orbyt bluesky getorbyt orbytvideo atproto video
0
fork

Configure Feed

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

style: enhance video presentation with ambient backdrop and rounded shapes

- Introduce a new class for video media visualization in embed.css, applying rounded shapes and gradient layers for a cohesive look.
- Update video and thumbnail styles to improve layering and z-index management for better visual hierarchy.
- Implement a video ambient backdrop in shared.css to match the app's aesthetic, enhancing the overall user experience.
- Refactor related styles in post.css and profile.css to maintain consistency with the new design approach.

+197 -85
+19 -17
public/css/embed.css
··· 138 138 } 139 139 140 140 /*--- Video ---*/ 141 + .post-media-visual { 142 + position: relative; 143 + width: 100%; 144 + border-radius: var(--orbyt-media-radius); 145 + overflow: hidden; 146 + } 147 + 148 + @supports (corner-shape: squircle) { 149 + .post-media-visual { 150 + corner-shape: var(--orbyt-corner-shape, squircle); 151 + } 152 + } 153 + 141 154 .video-container { 142 155 position: relative; 143 156 width: 100%; ··· 151 164 aspect-ratio: 9 / 16; 152 165 object-fit: contain; 153 166 background-color: transparent; 167 + position: relative; 168 + z-index: 1; 154 169 } 155 170 156 171 .video-thumbnail { ··· 160 175 width: 100%; 161 176 height: 100%; 162 177 object-fit: contain; 163 - background-color: #000; 164 - z-index: 1; 178 + background-color: transparent; 179 + z-index: 2; 165 180 pointer-events: none; 166 181 } 167 182 ··· 169 184 visibility: hidden; 170 185 } 171 186 172 - /* Desktop: video sizing using CSS viewport units */ 173 187 @media screen and (min-width: 700px) { 174 - .video-container { 188 + .post-media-visual { 175 189 width: auto; 176 190 display: inline-block; 177 191 } 178 - 192 + 179 193 video { 180 194 height: min( 181 195 clamp(400px, calc(100vh - 60px), 1200px), ··· 184 198 width: auto; 185 199 aspect-ratio: 9 / 16; 186 200 object-fit: contain; 187 - border-radius: 10px; 188 - } 189 - 190 - .video-thumbnail { 191 - border-radius: 10px; 192 - } 193 - 194 - @supports (corner-shape: squircle) { 195 - video, 196 - .video-thumbnail { 197 - corner-shape: var(--orbyt-corner-shape, squircle); 198 - } 199 201 } 200 202 } 201 203 .mute-toggle-wrapper {
-9
public/css/post.css
··· 123 123 margin-left: auto; 124 124 } 125 125 126 - video { 127 - border-radius: 10px; 128 - } 129 - 130 - @supports (corner-shape: squircle) { 131 - video { 132 - corner-shape: var(--orbyt-corner-shape, squircle); 133 - } 134 - } 135 126 }
+10 -13
public/css/profile.css
··· 198 198 line-height: 0; 199 199 padding-bottom: 177.78%; 200 200 overflow: hidden; 201 + border-radius: var(--orbyt-media-radius); 202 + } 203 + 204 + @supports (corner-shape: squircle) { 205 + .post .post-image-container { 206 + corner-shape: var(--orbyt-corner-shape, squircle); 207 + } 201 208 } 202 209 .post img { 203 210 width: 100%; ··· 216 223 top: 0; 217 224 height: 100%; 218 225 object-fit: contain; 219 - background-color: var(--orbyt-chrome, var(--orbyt-chrome-fallback)); 226 + background-color: transparent; 227 + z-index: 1; 220 228 } 221 229 .post .play-button { 222 230 width: 60px; ··· 228 236 left: 0; 229 237 right: 0; 230 238 bottom: 0; 239 + z-index: 2; 231 240 background: linear-gradient( 232 241 360deg, 233 242 rgb(var(--orbyt-scrim-rgb) / 0.4) 0%, ··· 356 365 padding: 20px 10px; 357 366 } 358 367 359 - .post img { 360 - border-radius: 10px; 361 - } 362 - 363 - @supports (corner-shape: squircle) { 364 - .post img, 365 - .post-overlay { 366 - corner-shape: var(--orbyt-corner-shape, squircle); 367 - } 368 - } 369 - 370 368 .post { 371 369 padding-left: 10px; 372 370 padding-right: 10px; ··· 376 374 .post-overlay { 377 375 padding-left: 10px; 378 376 padding-right: 10px; 379 - border-radius: 0 0 10px 10px; 380 377 } 381 378 382 379 .post-overlay .caption {
+35
public/css/shared.css
··· 88 88 .mention-handle { 89 89 font-weight: 600; 90 90 } 91 + 92 + .video-ambient-backdrop { 93 + position: absolute; 94 + inset: 0; 95 + z-index: 0; 96 + pointer-events: none; 97 + overflow: hidden; 98 + border-radius: inherit; 99 + } 100 + 101 + .video-ambient-backdrop::before { 102 + content: ''; 103 + position: absolute; 104 + inset: 0; 105 + border-radius: inherit; 106 + background-image: 107 + radial-gradient( 108 + 130% 90% at var(--video-ambient-backdrop-highlight-x, 50%) var(--video-ambient-backdrop-highlight-y, 18%), 109 + rgb(243 245 254 / var(--video-ambient-backdrop-highlight-opacity, 0.07)) 0%, 110 + transparent 60% 111 + ), 112 + linear-gradient( 113 + var(--video-ambient-backdrop-angle, 180deg), 114 + var(--video-ambient-backdrop-start, #130436) 0%, 115 + var(--video-ambient-backdrop-end, #06090d) 100% 116 + ); 117 + } 118 + 119 + .video-ambient-backdrop::after { 120 + content: ''; 121 + position: absolute; 122 + inset: 0; 123 + border-radius: inherit; 124 + background: rgb(5 7 10 / var(--video-ambient-backdrop-scrim-opacity, 0.58)); 125 + }
+2
public/css/tokens.css
··· 54 54 * https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/superellipse 55 55 */ 56 56 --orbyt-corner-shape: squircle; 57 + 58 + --orbyt-media-radius: 10px; 57 59 }
+2
src/actions/index.ts
··· 2 2 import { z } from "astro/zod"; 3 3 import { fetchVideoPosts } from "../utils/bluesky-api"; 4 4 import { parseRichText } from "../utils/richtext"; 5 + import { getVideoAmbientBackdropInlineStyle } from "../utils/video-ambient-backdrop"; 5 6 6 7 export const server = { 7 8 loadMorePosts: defineAction({ ··· 16 17 postId: post.postId, 17 18 thumbnail: post.thumbnail, 18 19 captionHtml: parseRichText(post.caption), 20 + videoAmbientBackdropStyle: getVideoAmbientBackdropInlineStyle(post.thumbnail), 19 21 })); 20 22 21 23 return {
+6 -3
src/pages/@[handle].astro
··· 6 6 import { getColor } from '../utils/orbyt-api'; 7 7 import { parseRichText } from '../utils/richtext'; 8 8 import { getChromeBackgroundColor, getUiColorScheme } from '../utils/profile-colors'; 9 + import { getVideoAmbientBackdropInlineStyle } from '../utils/video-ambient-backdrop'; 9 10 import DocumentColorScheme from '../components/DocumentColorScheme.astro'; 10 11 import { ClientRouter } from 'astro:transitions'; 11 12 ··· 267 268 <div class="posts"> 268 269 {videoPosts.length > 0 ? videoPosts.map((post) => ( 269 270 <div class="post"> 270 - <div class="post-image-container"> 271 + <div class="post-image-container" style={getVideoAmbientBackdropInlineStyle(post.thumbnail)}> 272 + <div class="video-ambient-backdrop" aria-hidden="true"></div> 271 273 <img class="post-placeholder" src="/images/post/placeholder.png" alt="" /> 272 274 <img class="post-thumb" src={post.thumbnail} alt="" style={`view-transition-name: video-${post.postId}`} /> 273 275 <div class="post-overlay"> ··· 297 299 298 300 // Wrapper to work around Astro actions client typing (expects FormData, accepts object) 299 301 async function loadMorePosts(handle: string, cursor: string) { 300 - return (actions as unknown as { loadMorePosts: (input: { handle: string; cursor: string }) => Promise<{ data?: { posts?: { postId: string; thumbnail: string; captionHtml: string }[]; nextCursor?: string | null }; error?: unknown }> }) 302 + return (actions as unknown as { loadMorePosts: (input: { handle: string; cursor: string }) => Promise<{ data?: { posts?: { postId: string; thumbnail: string; captionHtml: string; videoAmbientBackdropStyle: string }[]; nextCursor?: string | null }; error?: unknown }> }) 301 303 .loadMorePosts({ handle, cursor }); 302 304 } 303 305 ··· 330 332 const postDiv = document.createElement('div'); 331 333 postDiv.className = 'post'; 332 334 postDiv.innerHTML = ` 333 - <div class="post-image-container"> 335 + <div class="post-image-container" style="${post.videoAmbientBackdropStyle}"> 336 + <div class="video-ambient-backdrop" aria-hidden="true"></div> 334 337 <img class="post-placeholder" src="/images/post/placeholder.png" alt="" /> 335 338 <img class="post-thumb" src="${post.thumbnail}" alt="" style="view-transition-name: video-${post.postId}" /> 336 339 <div class="post-overlay">
+49 -43
src/pages/@[handle]/[postId].astro
··· 5 5 import { parseRichText } from '../../utils/richtext'; 6 6 import DocumentColorScheme from '../../components/DocumentColorScheme.astro'; 7 7 import { SITE_DOCUMENT } from '../../utils/site-document-theme'; 8 + import { getVideoAmbientBackdropInlineStyle } from '../../utils/video-ambient-backdrop'; 8 9 import { ClientRouter } from 'astro:transitions'; 9 10 10 11 const { handle, postId } = Astro.params; ··· 84 85 const ogImageAlt = caption 85 86 ? (caption.length > 200 ? caption.substring(0, 200) + '...' : caption) 86 87 : `Post by @${authorHandle}`; 88 + 89 + const videoAmbientBackdropStyle = getVideoAmbientBackdropInlineStyle(thumbnail); 87 90 88 91 // Title: "post by @[handle]" 89 92 const finalOgTitle = `post by @${authorHandle}`; ··· 391 394 392 395 </div> 393 396 394 - <div id="post-overlay" class="post-overlay"> 397 + <div class="post-media-visual"> 398 + <div class="video-container" style={videoAmbientBackdropStyle}> 399 + <div class="video-ambient-backdrop" aria-hidden="true"></div> 400 + <video id="vinit" muted playsinline loop autoplay preload="auto" oncontextmenu="return false;"> 401 + </video> 402 + <img id="video-thumbnail" class="video-thumbnail" src={thumbnail} alt="" style={`view-transition-name: video-${postId}`} /> 403 + </div> 395 404 396 - <div class='mute-toggle-wrapper'> 397 - <button id="mute-toggle" aria-label="Toggle mute"> 398 - <span id="mute-text"> 399 - LOADING... 400 - </span> 401 - </button> 402 - </div> 405 + <div id="post-overlay" class="post-overlay"> 403 406 404 - <div id="post-caption-mobile-wrap" class="post-caption-mobile-wrap mobile-overlay"> 405 - <div id="post-caption-mobile-inner" class="post-caption-mobile-inner" data-collapsed="true"> 406 - <div id="post-caption-mobile" class="post-content post-caption-mobile-text" set:html={parseRichText(caption)}></div> 407 - <button 408 - type="button" 409 - id="post-caption-mobile-toggle" 410 - class="post-caption-mobile-toggle" 411 - hidden 412 - aria-expanded="false" 413 - aria-controls="post-caption-mobile" 414 - > 415 - More 407 + <div class='mute-toggle-wrapper'> 408 + <button id="mute-toggle" aria-label="Toggle mute"> 409 + <span id="mute-text"> 410 + LOADING... 411 + </span> 416 412 </button> 417 413 </div> 418 - </div> 419 414 420 - <div class="post-author mobile-overlay"> 421 - 422 - <span class='avatar-wrapper'> 423 - <img id="author-avatar-mobile" class='avatar' src={authorAvatar} alt={`${authorDisplayName}'s avatar`}> 424 - <div class="username"> 425 - <a id="author-link-mobile" href={`/@${authorHandle}`}>{authorDisplayName}</a> 415 + <div id="post-caption-mobile-wrap" class="post-caption-mobile-wrap mobile-overlay"> 416 + <div id="post-caption-mobile-inner" class="post-caption-mobile-inner" data-collapsed="true"> 417 + <div id="post-caption-mobile" class="post-content post-caption-mobile-text" set:html={parseRichText(caption)}></div> 418 + <button 419 + type="button" 420 + id="post-caption-mobile-toggle" 421 + class="post-caption-mobile-toggle" 422 + hidden 423 + aria-expanded="false" 424 + aria-controls="post-caption-mobile" 425 + > 426 + More 427 + </button> 426 428 </div> 427 - <div id="post-time-mobile" style="display: none;"></div> 428 - </span> 429 + </div> 429 430 430 - <span class='likes'> 431 - <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="display: block;"> 432 - <g fill="none"> 433 - <path fill="#FE4359" d="M18.494 3.801c2.095 1.221 3.569 3.7 3.504 6.592C21.86 16.5 13.5 21 12 21s-9.861-4.5-9.998-10.607c-.065-2.892 1.409-5.37 3.504-6.592C7.466 2.66 9.928 2.653 12 4.338c2.072-1.685 4.534-1.679 6.494-.537"/> 434 - </g> 435 - </svg> 436 - <span id="likes-count-mobile">0</span> 437 - </span> 431 + <div class="post-author mobile-overlay"> 438 432 439 - </div> 433 + <span class='avatar-wrapper'> 434 + <img id="author-avatar-mobile" class='avatar' src={authorAvatar} alt={`${authorDisplayName}'s avatar`}> 435 + <div class="username"> 436 + <a id="author-link-mobile" href={`/@${authorHandle}`}>{authorDisplayName}</a> 437 + </div> 438 + <div id="post-time-mobile" style="display: none;"></div> 439 + </span> 440 440 441 - </div> 441 + <span class='likes'> 442 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" style="display: block;"> 443 + <g fill="none"> 444 + <path fill="#FE4359" d="M18.494 3.801c2.095 1.221 3.569 3.7 3.504 6.592C21.86 16.5 13.5 21 12 21s-9.861-4.5-9.998-10.607c-.065-2.892 1.409-5.37 3.504-6.592C7.466 2.66 9.928 2.653 12 4.338c2.072-1.685 4.534-1.679 6.494-.537"/> 445 + </g> 446 + </svg> 447 + <span id="likes-count-mobile">0</span> 448 + </span> 449 + 450 + </div> 442 451 443 - <div class="video-container"> 444 - <video id="vinit" muted playsinline loop autoplay preload="auto" oncontextmenu="return false;"> 445 - </video> 446 - <img id="video-thumbnail" class="video-thumbnail" src={thumbnail} alt="" style={`view-transition-name: video-${postId}`} /> 452 + </div> 447 453 </div> 448 454 449 455 </div>
+74
src/utils/video-ambient-backdrop.ts
··· 1 + // Hash-seeded gradient + scrim values for video backdrops. 2 + const VIDEO_AMBIENT_BACKDROP_GRADIENT_PAIRS = [ 3 + ['#130436', '#06090d'], 4 + ['#091f47', '#110430'], 5 + ['#002c20', '#06090d'], 6 + ['#390420', '#110430'], 7 + ['#440915', '#110430'], 8 + ['#10131a', '#020305'], 9 + ['#392500', '#110430'], 10 + ['#481805', '#110430'], 11 + ['#002a32', '#081c3f'], 12 + ['#002a32', '#06090d'], 13 + ['#392500', '#06090d'], 14 + ['#091f47', '#00271c'], 15 + ['#130436', '#00271c'], 16 + ['#390420', '#3d0813'], 17 + ['#481805', '#020305'], 18 + ['#440915', '#06090d'], 19 + ['#002c20', '#110430'], 20 + ['#392500', '#401504'], 21 + ] as const; 22 + 23 + const VIDEO_AMBIENT_BACKDROP_ANGLES = [155, 170, 185, 200, 215, 230] as const; 24 + const VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_X = [14, 28, 42, 58, 72, 86] as const; 25 + const VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_Y = [12, 20, 28, 36] as const; 26 + const VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_OPACITY = [0.05, 0.07, 0.09, 0.11] as const; 27 + const VIDEO_AMBIENT_BACKDROP_SCRIM_OPACITY = [0.52, 0.56, 0.6, 0.64] as const; 28 + 29 + export function hashVideoAmbientBackdropSeed(value: string): number { 30 + let hash = 0; 31 + for (let i = 0; i < value.length; i += 1) { 32 + hash = (hash << 5) - hash + value.charCodeAt(i); 33 + hash |= 0; 34 + } 35 + return Math.abs(hash); 36 + } 37 + 38 + export function getVideoAmbientBackdropGradientColors( 39 + seedUrl: string | null 40 + ): readonly [string, string] { 41 + const s = seedUrl ?? 'fallback'; 42 + const index = hashVideoAmbientBackdropSeed(s) % VIDEO_AMBIENT_BACKDROP_GRADIENT_PAIRS.length; 43 + return VIDEO_AMBIENT_BACKDROP_GRADIENT_PAIRS[index]; 44 + } 45 + 46 + export function getVideoAmbientBackdropInlineStyle(seedUrl: string | null): string { 47 + const seed = seedUrl ?? 'fallback'; 48 + const [start, end] = getVideoAmbientBackdropGradientColors(seed); 49 + const angle = VIDEO_AMBIENT_BACKDROP_ANGLES[ 50 + hashVideoAmbientBackdropSeed(`${seed}:angle`) % VIDEO_AMBIENT_BACKDROP_ANGLES.length 51 + ]; 52 + const highlightX = VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_X[ 53 + hashVideoAmbientBackdropSeed(`${seed}:hx`) % VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_X.length 54 + ]; 55 + const highlightY = VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_Y[ 56 + hashVideoAmbientBackdropSeed(`${seed}:hy`) % VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_Y.length 57 + ]; 58 + const highlightOpacity = VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_OPACITY[ 59 + hashVideoAmbientBackdropSeed(`${seed}:ho`) % VIDEO_AMBIENT_BACKDROP_HIGHLIGHT_OPACITY.length 60 + ]; 61 + const scrimOpacity = VIDEO_AMBIENT_BACKDROP_SCRIM_OPACITY[ 62 + hashVideoAmbientBackdropSeed(`${seed}:so`) % VIDEO_AMBIENT_BACKDROP_SCRIM_OPACITY.length 63 + ]; 64 + 65 + return [ 66 + `--video-ambient-backdrop-start: ${start}`, 67 + `--video-ambient-backdrop-end: ${end}`, 68 + `--video-ambient-backdrop-angle: ${angle}deg`, 69 + `--video-ambient-backdrop-highlight-x: ${highlightX}%`, 70 + `--video-ambient-backdrop-highlight-y: ${highlightY}%`, 71 + `--video-ambient-backdrop-highlight-opacity: ${highlightOpacity}`, 72 + `--video-ambient-backdrop-scrim-opacity: ${scrimOpacity}`, 73 + ].join('; '); 74 + }