[READ-ONLY] Mirror of https://github.com/flo-bit/ui-kit. 🦊 fox ui, svelte 5 and tailwind 4 flo-bit.dev/ui-kit/
svelte tailwindcss ui-components
0

Configure Feed

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

improve bsky post

Florian (Mar 5, 2026, 4:23 AM +0100) cb600eb5 8195172f

+230 -174
+5
.changeset/green-steaks-hide.md
··· 1 + --- 2 + '@foxui/social': minor 3 + --- 4 + 5 + improve bluesky post (changes api)
+4 -1
apps/docs/eslint.config.js
··· 18 18 ...svelte.configs.prettier, 19 19 { 20 20 languageOptions: { 21 - globals: { ...globals.browser, ...globals.node } 21 + globals: { ...globals.browser, ...globals.node }, 22 + parserOptions: { 23 + tsconfigRootDir: fileURLToPath(new URL('.', import.meta.url)) 24 + } 22 25 }, 23 26 rules: { 'no-undef': 'off' } 24 27 },
+4 -1
packages/social/eslint.config.js
··· 18 18 ...svelte.configs.prettier, 19 19 { 20 20 languageOptions: { 21 - globals: { ...globals.browser, ...globals.node } 21 + globals: { ...globals.browser, ...globals.node }, 22 + parserOptions: { 23 + tsconfigRootDir: fileURLToPath(new URL('.', import.meta.url)) 24 + } 22 25 }, 23 26 rules: { 'no-undef': 'off' } 24 27 },
+15 -18
packages/social/src/lib/components/bluesky-post/BlueskyPost.svelte
··· 1 1 <script lang="ts"> 2 2 import { Post } from '../post'; 3 + import type { PostProps } from '../post/types'; 3 4 import { blueskyPostToPostData } from '.'; 4 - import type { Snippet } from 'svelte'; 5 5 import type { PostView } from '@atcute/bluesky/types/app/feed/defs'; 6 6 7 + type BlueskyPostProps = Omit<PostProps, 'data'> & { 8 + data?: PostView; 9 + // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 + reason?: any; 11 + showLogo?: boolean; 12 + }; 13 + 7 14 let { 8 - feedViewPost, 15 + data, 9 16 reason, 10 17 children, 11 18 showLogo = false, 19 + logo, 12 20 showAvatar = true, 13 21 compact = false, 14 22 showImages = true, ··· 16 24 showVideo = true, 17 25 showQuotes = true, 18 26 ...restProps 19 - }: { 20 - feedViewPost?: PostView; 21 - // eslint-disable-next-line @typescript-eslint/no-explicit-any 22 - reason?: any; 23 - children?: Snippet; 24 - showLogo?: boolean; 25 - showAvatar?: boolean; 26 - compact?: boolean; 27 - showImages?: boolean; 28 - showExternal?: boolean; 29 - showVideo?: boolean; 30 - showQuotes?: boolean; 31 - } = $props(); 27 + }: BlueskyPostProps = $props(); 32 28 33 29 const postData = $derived( 34 - feedViewPost ? blueskyPostToPostData(feedViewPost, undefined, reason) : undefined 30 + data ? blueskyPostToPostData(data, undefined, reason) : undefined 35 31 ); 36 32 </script> 37 33 38 - {#snippet logo()} 34 + {#snippet defaultLogo()} 39 35 <a 40 36 class="text-accent-700 dark:text-accent-400 hover:text-accent-600 dark:hover:text-accent-500 accent:text-accent-900 accent:hover:text-accent-800" 41 37 href={postData?.href} ··· 57 53 replyHref={postData?.href} 58 54 repostHref={postData?.href} 59 55 likeHref={postData?.href} 60 - logo={showLogo ? logo : undefined} 56 + timestampHref={postData?.href} 57 + logo={logo ?? (showLogo ? defaultLogo : undefined)} 61 58 {showAvatar} 62 59 {compact} 63 60 {showImages}
+37 -48
packages/social/src/lib/components/post/Post.svelte
··· 1 1 <script lang="ts"> 2 2 import { cn, sanitize } from '@foxui/core'; 3 - import type { WithChildren, WithElementRef } from 'bits-ui'; 3 + import type { WithElementRef } from 'bits-ui'; 4 4 import type { HTMLAttributes } from 'svelte/elements'; 5 - import type { PostData, PostImageData, QuotedPostData } from '.'; 5 + import type { PostProps } from './types'; 6 6 import PostAction from './PostAction.svelte'; 7 - import type { Snippet } from 'svelte'; 8 7 import { numberToHumanReadable } from '..'; 9 8 import { RelativeTime } from '@foxui/time'; 10 9 import PostEmbed from './PostEmbed.svelte'; ··· 34 33 onclickpost, 35 34 onclickhandle, 36 35 36 + timestampHref, 37 + ontimestampclick, 38 + 37 39 customActions, 38 40 39 41 children, ··· 47 49 showExternal = true, 48 50 showVideo = true, 49 51 showQuotes = true 50 - }: WithElementRef<WithChildren<HTMLAttributes<HTMLDivElement>>> & { 51 - data: PostData; 52 - class?: string; 53 - 54 - bookmarked?: boolean; 55 - liked?: boolean; 56 - 57 - showReply?: boolean; 58 - showRepost?: boolean; 59 - showLike?: boolean; 60 - showBookmark?: boolean; 61 - 62 - onReplyClick?: () => void; 63 - onRepostClick?: () => void; 64 - onLikeClick?: () => void; 65 - onBookmarkClick?: () => void; 66 - 67 - replyHref?: string; 68 - repostHref?: string; 69 - likeHref?: string; 70 - 71 - onclickimage?: (image: PostImageData) => void; 72 - onclickpost?: (data: PostData | QuotedPostData, href?: string) => void; 73 - onclickhandle?: (handle: string, href?: string) => void; 74 - 75 - customActions?: Snippet; 76 - 77 - logo?: Snippet; 78 - 79 - showAvatar?: boolean; 80 - compact?: boolean; 81 - 82 - showImages?: boolean; 83 - showExternal?: boolean; 84 - showVideo?: boolean; 85 - showQuotes?: boolean; 86 - } = $props(); 52 + }: WithElementRef<HTMLAttributes<HTMLDivElement>> & PostProps = $props(); 87 53 88 54 function handleContentClick(e: MouseEvent) { 89 55 if (!onclickhandle) return; ··· 267 233 </div> 268 234 {/if} 269 235 270 - <div 271 - class={cn( 272 - 'text-base-600 dark:text-base-400 accent:text-accent-950 block no-underline', 273 - compact ? 'text-xs' : 'text-sm' 274 - )} 275 - > 276 - <RelativeTime date={new Date(data.createdAt)} locale="en" /> 277 - </div> 236 + {#if timestampHref} 237 + <a 238 + href={timestampHref} 239 + target="_blank" 240 + class={cn( 241 + 'text-base-600 dark:text-base-400 accent:text-accent-950 hover:text-accent-600 dark:hover:text-accent-400 block no-underline', 242 + compact ? 'text-xs' : 'text-sm' 243 + )} 244 + > 245 + <RelativeTime date={new Date(data.createdAt)} locale="en" /> 246 + </a> 247 + {:else if ontimestampclick} 248 + <button 249 + onclick={ontimestampclick} 250 + class={cn( 251 + 'text-base-600 dark:text-base-400 accent:text-accent-950 hover:text-accent-600 dark:hover:text-accent-400 block cursor-pointer', 252 + compact ? 'text-xs' : 'text-sm' 253 + )} 254 + > 255 + <RelativeTime date={new Date(data.createdAt)} locale="en" /> 256 + </button> 257 + {:else} 258 + <div 259 + class={cn( 260 + 'text-base-600 dark:text-base-400 accent:text-accent-950 block no-underline', 261 + compact ? 'text-xs' : 'text-sm' 262 + )} 263 + > 264 + <RelativeTime date={new Date(data.createdAt)} locale="en" /> 265 + </div> 266 + {/if} 278 267 </div> 279 268 280 269 {#if logo}
+14 -105
packages/social/src/lib/components/post/index.ts
··· 1 - export type PostImageData = { 2 - alt: string; 3 - thumb: string; 4 - fullsize: string; 5 - aspectRatio?: { 6 - width: number; 7 - height: number; 8 - }; 9 - }; 1 + export type { 2 + PostImageData, 3 + PostEmbedImage, 4 + PostEmbedExternal, 5 + PostEmbedVideo, 6 + QuotedPostData, 7 + PostEmbedRecord, 8 + PostEmbedRecordWithMedia, 9 + UnknownEmbed, 10 + PostEmbed, 11 + PostData, 12 + PostProps 13 + } from './types'; 10 14 11 - export type PostEmbedImage = { 12 - type: 'images'; 13 - images: PostImageData[]; 14 - }; 15 - 16 - export type PostEmbedExternal = { 17 - type: 'external'; 18 - 19 - external: { 20 - href: string; 21 - thumb: string; 22 - title: string; 23 - description: string; 24 - }; 25 - }; 26 - 27 - export type PostEmbedVideo = { 28 - type: 'video'; 29 - 30 - video: { 31 - playlist: string; 32 - 33 - thumb: string; 34 - alt: string; 35 - 36 - aspectRatio?: { 37 - width: number; 38 - height: number; 39 - }; 40 - }; 41 - }; 42 - 43 - export type QuotedPostData = { 44 - author: { 45 - displayName: string; 46 - handle: string; 47 - avatar?: string; 48 - href?: string; 49 - }; 50 - href?: string; 51 - htmlContent?: string; 52 - createdAt?: string; 53 - embed?: PostEmbed; 54 - }; 55 - 56 - export type PostEmbedRecord = { 57 - type: 'record'; 58 - record: QuotedPostData; 59 - }; 60 - 61 - export type PostEmbedRecordWithMedia = { 62 - type: 'recordWithMedia'; 63 - record: QuotedPostData; 64 - media: PostEmbed; 65 - }; 66 - 67 - export type UnknownEmbed = { 68 - type: 'unknown'; 69 - } & Record<string, unknown>; 70 - 71 - export type PostEmbed = 72 - | PostEmbedImage 73 - | PostEmbedExternal 74 - | PostEmbedVideo 75 - | PostEmbedRecord 76 - | PostEmbedRecordWithMedia 77 - | UnknownEmbed; 78 - 79 - export type PostData = { 80 - href?: string; 81 - id?: string; 82 - 83 - reposted?: { handle: string; href: string }; 84 - replyTo?: { handle: string; href: string }; 85 - 86 - author: { 87 - displayName: string; 88 - handle: string; 89 - avatar?: string; 90 - href?: string; 91 - }; 92 - 93 - replyCount: number; 94 - repostCount: number; 95 - likeCount: number; 96 - 97 - createdAt: string; 98 - 99 - embed?: PostEmbed; 100 - 101 - htmlContent?: string; 102 - 103 - replies?: PostData[]; 104 - 105 - labels?: string[]; 106 - }; 15 + import type { PostData } from './types'; 107 16 108 17 export const nsfwLabels = ['porn', 'sexual', 'graphic-media', 'nudity']; 109 18
+150
packages/social/src/lib/components/post/types.ts
··· 1 + import type { Snippet } from 'svelte'; 2 + 3 + export type PostImageData = { 4 + alt: string; 5 + thumb: string; 6 + fullsize: string; 7 + aspectRatio?: { 8 + width: number; 9 + height: number; 10 + }; 11 + }; 12 + 13 + export type PostEmbedImage = { 14 + type: 'images'; 15 + images: PostImageData[]; 16 + }; 17 + 18 + export type PostEmbedExternal = { 19 + type: 'external'; 20 + 21 + external: { 22 + href: string; 23 + thumb: string; 24 + title: string; 25 + description: string; 26 + }; 27 + }; 28 + 29 + export type PostEmbedVideo = { 30 + type: 'video'; 31 + 32 + video: { 33 + playlist: string; 34 + 35 + thumb: string; 36 + alt: string; 37 + 38 + aspectRatio?: { 39 + width: number; 40 + height: number; 41 + }; 42 + }; 43 + }; 44 + 45 + export type QuotedPostData = { 46 + author: { 47 + displayName: string; 48 + handle: string; 49 + avatar?: string; 50 + href?: string; 51 + }; 52 + href?: string; 53 + htmlContent?: string; 54 + createdAt?: string; 55 + embed?: PostEmbed; 56 + }; 57 + 58 + export type PostEmbedRecord = { 59 + type: 'record'; 60 + record: QuotedPostData; 61 + }; 62 + 63 + export type PostEmbedRecordWithMedia = { 64 + type: 'recordWithMedia'; 65 + record: QuotedPostData; 66 + media: PostEmbed; 67 + }; 68 + 69 + export type UnknownEmbed = { 70 + type: 'unknown'; 71 + } & Record<string, unknown>; 72 + 73 + export type PostEmbed = 74 + | PostEmbedImage 75 + | PostEmbedExternal 76 + | PostEmbedVideo 77 + | PostEmbedRecord 78 + | PostEmbedRecordWithMedia 79 + | UnknownEmbed; 80 + 81 + export type PostData = { 82 + href?: string; 83 + id?: string; 84 + 85 + reposted?: { handle: string; href: string }; 86 + replyTo?: { handle: string; href: string }; 87 + 88 + author: { 89 + displayName: string; 90 + handle: string; 91 + avatar?: string; 92 + href?: string; 93 + }; 94 + 95 + replyCount: number; 96 + repostCount: number; 97 + likeCount: number; 98 + 99 + createdAt: string; 100 + 101 + embed?: PostEmbed; 102 + 103 + htmlContent?: string; 104 + 105 + replies?: PostData[]; 106 + 107 + labels?: string[]; 108 + }; 109 + 110 + export type PostProps = { 111 + data: PostData; 112 + class?: string; 113 + children?: Snippet; 114 + 115 + bookmarked?: boolean; 116 + liked?: boolean; 117 + 118 + showReply?: boolean; 119 + showRepost?: boolean; 120 + showLike?: boolean; 121 + showBookmark?: boolean; 122 + 123 + onReplyClick?: () => void; 124 + onRepostClick?: () => void; 125 + onLikeClick?: () => void; 126 + onBookmarkClick?: () => void; 127 + 128 + replyHref?: string; 129 + repostHref?: string; 130 + likeHref?: string; 131 + 132 + onclickimage?: (image: PostImageData) => void; 133 + onclickpost?: (data: PostData | QuotedPostData, href?: string) => void; 134 + onclickhandle?: (handle: string, href?: string) => void; 135 + 136 + timestampHref?: string; 137 + ontimestampclick?: () => void; 138 + 139 + customActions?: Snippet; 140 + 141 + logo?: Snippet; 142 + 143 + showAvatar?: boolean; 144 + compact?: boolean; 145 + 146 + showImages?: boolean; 147 + showExternal?: boolean; 148 + showVideo?: boolean; 149 + showQuotes?: boolean; 150 + };
+1 -1
apps/docs/src/routes/(main)/components/social/post/Example-bluesky.svelte
··· 27 27 28 28 <Box class="not-prose relative flex min-h-24 w-full flex-col gap-8 p-8"> 29 29 {#each posts as { post, reason } (post.uri)} 30 - <BlueskyPost feedViewPost={post} {reason} showLogo showAvatar={false}></BlueskyPost> 30 + <BlueskyPost data={post} {reason} showLogo showAvatar={true}></BlueskyPost> 31 31 {/each} 32 32 </Box>