[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.

text: fix link stuff, social: improve post creator, core: add default bg and text color

Florian (Mar 25, 2026, 7:29 PM +0100) caec940b be3fc8d9

+326 -126
+7
.changeset/breezy-dogs-dance.md
··· 1 + --- 2 + '@foxui/social': patch 3 + '@foxui/core': patch 4 + '@foxui/text': patch 5 + --- 6 + 7 + text: fix link stuff, social: improve post creator, core: add default bg and text color
+1 -1
apps/docs/src/app.html
··· 14 14 </head> 15 15 <body 16 16 data-sveltekit-preload-data="hover" 17 - class="bg-base-100 dark:bg-base-900 overflow-x-hidden motion-safe:transition-colors" 17 + class="overflow-x-hidden motion-safe:transition-colors" 18 18 > 19 19 <div style="display: contents">%sveltekit.body%</div> 20 20 </body>
+4
packages/core/src/lib/theme.css
··· 33 33 } 34 34 35 35 @layer base { 36 + body { 37 + @apply bg-base-100 text-base-950 dark:bg-base-900 dark:text-base-50; 38 + } 39 + 36 40 :root { 37 41 --accent-50: var(--color-pink-50); 38 42 --accent-100: var(--color-pink-100);
+2 -2
apps/docs/src/lib/site-components/Logo.svelte
··· 11 11 12 12 <div class={cn('flex items-center gap-2', className)}> 13 13 <a 14 - class="text-base-950 dark:text-base-50 focus-visible:outline-base-900 dark:focus-visible:outline-base-50 rounded-xl font-semibold focus-visible:outline-2 focus-visible:outline-offset-4" 15 - href={resolve('/')}>🦊 fox ui</a 14 + class="text-base-950 dark:text-base-50 focus-visible:outline-base-900 dark:focus-visible:outline-base-50 rounded-xl font-semibold focus-visible:outline-2 focus-visible:outline-offset-4 flex gap-2" 15 + href={resolve('/')}>🦊 <span class="hidden sm:block">fox ui</span></a 16 16 > 17 17 <Badge>alpha</Badge> 18 18 </div>
+9 -1
packages/social/src/lib/components/bluesky-post/BlueskyPost.svelte
··· 14 14 baseUrl?: string; 15 15 hrefs?: BlueskyHrefs; 16 16 showLogo?: boolean; 17 + 18 + liked?: boolean; 19 + bookmarked?: boolean; 17 20 }; 18 21 19 22 let { ··· 27 30 showAvatar = true, 28 31 compact = false, 29 32 target = '_blank', 33 + 34 + liked, 35 + bookmarked, 36 + 30 37 ...restProps 31 38 }: BlueskyPostProps = $props(); 32 39 ··· 58 65 actions={{ 59 66 reply: { count: result.postData.replyCount, href: result.postData.href }, 60 67 repost: { count: result.postData.repostCount, href: result.postData.href }, 61 - like: { count: result.postData.likeCount, href: result.postData.href } 68 + like: { count: result.postData.likeCount, href: result.postData.href, active: liked }, 69 + bookmark: bookmarked ? { active: true } : undefined 62 70 }} 63 71 timestamp={{ href: result.postData.href }} 64 72 logo={logo ?? (showLogo ? defaultLogo : undefined)}
+2 -2
packages/social/src/lib/components/embed/External.svelte
··· 9 9 10 10 <article 11 11 class={[ 12 - 'group dark:bg-base-900 bg-base-200 border-base-300 dark:border-base-600/30 relative isolate flex max-w-md flex-col justify-end overflow-hidden rounded-2xl border p-4', 13 - data.external.thumb ? 'aspect-video' : '' 12 + 'group dark:bg-base-900 bg-base-200 border-base-300 dark:border-base-600/30 relative isolate flex w-full flex-col justify-end overflow-hidden rounded-2xl border p-4', 13 + data.external.thumb ? 'aspect-1200/630' : '' 14 14 ]} 15 15 > 16 16 {#if data.external.thumb}
+47
packages/social/src/lib/components/embed/Image.svelte
··· 1 + <script lang="ts"> 2 + import type { ImageData } from './types'; 3 + 4 + const { 5 + image, 6 + onclick, 7 + class: className 8 + }: { 9 + image: ImageData; 10 + onclick?: (image: ImageData) => void; 11 + class?: string; 12 + } = $props(); 13 + </script> 14 + 15 + {#if onclick} 16 + <button class="cursor-pointer" onclick={() => onclick(image)}> 17 + <img 18 + loading="lazy" 19 + src={image.thumb} 20 + alt={image.alt} 21 + width={image.aspectRatio?.width} 22 + height={image.aspectRatio?.height} 23 + style={image.aspectRatio 24 + ? `aspect-ratio: ${image.aspectRatio.width} / ${image.aspectRatio.height}` 25 + : 'aspect-ratio: 1 / 1'} 26 + class={[ 27 + 'border-base-500/20 dark:border-base-400/20 accent:border-accent-900 max-h-160 w-full max-w-full rounded-2xl border object-cover', 28 + className 29 + ]} 30 + /> 31 + </button> 32 + {:else} 33 + <img 34 + loading="lazy" 35 + src={image.thumb} 36 + alt={image.alt} 37 + width={image.aspectRatio?.width} 38 + height={image.aspectRatio?.height} 39 + style={image.aspectRatio 40 + ? `aspect-ratio: ${image.aspectRatio.width} / ${image.aspectRatio.height}` 41 + : 'aspect-ratio: 1 / 1'} 42 + class={[ 43 + 'border-base-500/20 dark:border-base-400/20 accent:border-accent-900 max-h-160 w-full max-w-full rounded-2xl border object-cover', 44 + className 45 + ]} 46 + /> 47 + {/if}
+4 -39
packages/social/src/lib/components/embed/Images.svelte
··· 1 1 <script lang="ts"> 2 - import type { EmbedImageData, ImageData } from './types'; 2 + import type { EmbedImageData } from './types'; 3 + import Image from './Image.svelte'; 3 4 4 5 const { 5 6 data, ··· 11 12 12 13 let revealed = $state(false); 13 14 </script> 14 - 15 - {#snippet imageSnippet(image: ImageData, className?: string)} 16 - {#if data.onclick} 17 - <button class="cursor-pointer" onclick={() => data.onclick!(image)}> 18 - <img 19 - loading="lazy" 20 - src={image.thumb} 21 - alt={image.alt} 22 - width={image.aspectRatio?.width} 23 - height={image.aspectRatio?.height} 24 - style={image.aspectRatio 25 - ? `aspect-ratio: ${image.aspectRatio.width} / ${image.aspectRatio.height}` 26 - : 'aspect-ratio: 1 / 1'} 27 - class={[ 28 - 'border-base-500/20 dark:border-base-400/20 accent:border-accent-900 max-h-160 w-full max-w-full rounded-2xl border object-cover', 29 - className 30 - ]} 31 - /> 32 - </button> 33 - {:else} 34 - <img 35 - loading="lazy" 36 - src={image.thumb} 37 - alt={image.alt} 38 - width={image.aspectRatio?.width} 39 - height={image.aspectRatio?.height} 40 - style={image.aspectRatio 41 - ? `aspect-ratio: ${image.aspectRatio.width} / ${image.aspectRatio.height}` 42 - : 'aspect-ratio: 1 / 1'} 43 - class={[ 44 - 'border-base-500/20 dark:border-base-400/20 accent:border-accent-900 max-h-160 w-full max-w-full rounded-2xl border object-cover', 45 - className 46 - ]} 47 - /> 48 - {/if} 49 - {/snippet} 50 15 51 16 {#if data.sensitive && showSensitive && !revealed} 52 17 {@const firstImage = data.images[0]} ··· 60 25 Sensitive content, click to show. 61 26 </button> 62 27 {:else if data.images.length === 1} 63 - {@render imageSnippet(data.images[0])} 28 + <Image image={data.images[0]} onclick={data.onclick} /> 64 29 {:else} 65 30 <div class="columns-2 gap-4"> 66 31 {#each data.images as image (image.thumb)} 67 - {@render imageSnippet(image, 'mb-4')} 32 + <Image {image} onclick={data.onclick} class="mb-4" /> 68 33 {/each} 69 34 </div> 70 35 {/if}
+32 -15
packages/social/src/lib/components/embed/Video.svelte
··· 18 18 initPlayer(); 19 19 }); 20 20 21 + function isHlsSource(url: string) { 22 + return url.endsWith('.m3u8') || url.includes('/playlist/'); 23 + } 24 + 21 25 async function initPlayer() { 22 - const [{ default: Plyr }, { default: Hls }] = await Promise.all([ 23 - import('plyr'), 24 - import('hls.js') 25 - ]); 26 + const src = data.video.playlist; 26 27 27 - if (Hls.isSupported()) { 28 - const hls = new Hls(); 29 - hls.loadSource(data.video.playlist); 30 - hls.attachMedia(element); 28 + if (isHlsSource(src)) { 29 + const [{ default: Plyr }, { default: Hls }] = await Promise.all([ 30 + import('plyr'), 31 + import('hls.js') 32 + ]); 33 + 34 + if (Hls.isSupported()) { 35 + const hls = new Hls(); 36 + hls.loadSource(src); 37 + hls.attachMedia(element); 38 + } 39 + 40 + new Plyr(element, { 41 + controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'fullscreen'], 42 + ratio: data.video.aspectRatio 43 + ? `${data.video.aspectRatio.width}:${data.video.aspectRatio.height}` 44 + : '16:9' 45 + }); 46 + } else { 47 + element.src = src; 48 + const { default: Plyr } = await import('plyr'); 49 + new Plyr(element, { 50 + controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'fullscreen'], 51 + ratio: data.video.aspectRatio 52 + ? `${data.video.aspectRatio.width}:${data.video.aspectRatio.height}` 53 + : '16:9' 54 + }); 31 55 } 32 - 33 - new Plyr(element, { 34 - controls: ['play-large', 'play', 'progress', 'current-time', 'mute', 'volume', 'fullscreen'], 35 - ratio: data.video.aspectRatio 36 - ? `${data.video.aspectRatio.width}:${data.video.aspectRatio.height}` 37 - : '16:9' 38 - }); 39 56 } 40 57 41 58 function reveal() {
+1
packages/social/src/lib/components/embed/index.ts
··· 9 9 Embed 10 10 } from './types'; 11 11 12 + export { default as EmbedImage } from './Image.svelte'; 12 13 export { default as EmbedImages } from './Images.svelte'; 13 14 export { default as EmbedExternal } from './External.svelte'; 14 15 export { default as EmbedVideo } from './Video.svelte';
+85 -23
packages/social/src/lib/components/microblogging-post-creator/MicrobloggingPostCreator.svelte
··· 1 1 <script lang="ts" module> 2 - import type { Snippet } from 'svelte'; 3 2 import type { Readable } from 'svelte/store'; 4 3 import type { MentionItem, JSONContent, SvelteTiptap } from '@foxui/text'; 5 4 import type { LinkAddedEvent } from '@foxui/text'; ··· 11 10 </script> 12 11 13 12 <script lang="ts"> 13 + import type { Snippet } from 'svelte'; 14 14 import { 15 15 PlainTextEditor, 16 16 MentionNode, ··· 47 47 editor = $bindable<Readable<SvelteTiptap.Editor>>(), 48 48 ref = $bindable<HTMLDivElement | null>(null), 49 49 searchMentions, 50 - embed = $bindable<unknown>(undefined), 51 50 onlinkadded, 52 - embedPreview, 53 - mentionItem: mentionItemSnippet 51 + onimageadded, 52 + onvideoadded, 53 + mentionItem: mentionItemSnippet, 54 + textEditorClass = "", 55 + children 54 56 }: { 55 57 class?: string; 56 58 placeholder?: string; ··· 60 62 editor?: Readable<SvelteTiptap.Editor>; 61 63 ref?: HTMLDivElement | null; 62 64 searchMentions?: (query: string) => Promise<MentionItem[]>; 63 - embed?: unknown; 64 65 onlinkadded?: (event: LinkAddedEvent) => void; 65 - embedPreview?: Snippet<[{ embed: unknown; removeEmbed: () => void }]>; 66 + onimageadded?: (files: File[]) => void; 67 + onvideoadded?: (files: File[]) => void; 66 68 mentionItem?: Snippet< 67 69 [{ item: MentionItem; isActive: boolean; select: () => void }] 68 70 >; 71 + textEditorClass?: string; 72 + children?: Snippet; 69 73 } = $props(); 74 + 75 + let acceptsMedia = $derived(!!onimageadded || !!onvideoadded); 76 + let dragCounter = $state(0); 77 + let dragging = $derived(dragCounter > 0); 78 + 79 + function handleFiles(files: File[]) { 80 + const images = files.filter((f) => f.type.startsWith('image/')); 81 + const videos = files.filter((f) => f.type.startsWith('video/')); 82 + if (images.length && onimageadded) onimageadded(images); 83 + if (videos.length && onvideoadded) onvideoadded(videos); 84 + } 85 + 86 + function handlePaste(e: ClipboardEvent) { 87 + if (!acceptsMedia || !e.clipboardData?.files.length) return; 88 + const files = Array.from(e.clipboardData.files).filter( 89 + (f) => 90 + (onimageadded && f.type.startsWith('image/')) || 91 + (onvideoadded && f.type.startsWith('video/')) 92 + ); 93 + if (files.length) { 94 + e.preventDefault(); 95 + handleFiles(files); 96 + } 97 + } 98 + 99 + function handleDrop(e: DragEvent) { 100 + dragCounter = 0; 101 + if (!acceptsMedia || !e.dataTransfer?.files.length) return; 102 + const files = Array.from(e.dataTransfer.files).filter( 103 + (f) => 104 + (onimageadded && f.type.startsWith('image/')) || 105 + (onvideoadded && f.type.startsWith('video/')) 106 + ); 107 + if (files.length) { 108 + e.preventDefault(); 109 + handleFiles(files); 110 + } 111 + } 112 + 113 + function handleDragEnter(e: DragEvent) { 114 + if (!acceptsMedia) return; 115 + e.preventDefault(); 116 + dragCounter++; 117 + } 118 + 119 + function handleDragOver(e: DragEvent) { 120 + if (!acceptsMedia) return; 121 + e.preventDefault(); 122 + } 123 + 124 + function handleDragLeave() { 125 + dragCounter--; 126 + } 70 127 71 128 let charCount = $derived(content.text.length); 72 129 let remaining = $derived(maxLength - charCount); 73 130 74 131 let extraExtensions = $derived.by(() => { 75 132 const exts: any[] = [ 76 - LinkExtension.configure({ onlinkadded: handleLinkAdded }), 133 + LinkExtension.configure({ onlinkadded }), 77 134 HashtagDecoration 78 135 ]; 79 136 if (searchMentions) { ··· 87 144 } 88 145 return exts; 89 146 }); 90 - 91 - function handleLinkAdded(event: LinkAddedEvent) { 92 - if (!embed) { 93 - embed = { type: 'link', url: event.href, text: event.text }; 94 - } 95 - onlinkadded?.(event); 96 - } 97 - 98 - function removeEmbed() { 99 - embed = undefined; 100 - } 101 147 </script> 102 148 103 - <div class={cn('relative', className)}> 149 + <!-- svelte-ignore a11y_no_static_element_interactions --> 150 + <div 151 + class={cn('relative', className)} 152 + onpaste={handlePaste} 153 + ondrop={handleDrop} 154 + ondragenter={handleDragEnter} 155 + ondragover={handleDragOver} 156 + ondragleave={handleDragLeave} 157 + > 158 + {#if dragging} 159 + <div 160 + class="border-accent-500 bg-accent-500/10 absolute inset-0 z-10 flex items-center justify-center rounded-lg border-2 border-dashed" 161 + > 162 + <span class="text-accent-600 dark:text-accent-400 text-sm font-medium"> 163 + Drop {onimageadded && onvideoadded ? 'image or video' : onimageadded ? 'image' : 'video'} here 164 + </span> 165 + </div> 166 + {/if} 104 167 <PlainTextEditor 105 168 bind:editor 106 169 bind:ref ··· 111 174 content = { text: extractText(json), json }; 112 175 onupdate?.(content); 113 176 }} 177 + class={textEditorClass} 114 178 > 115 179 {#if searchMentions && $editor} 116 180 <MentionMenu editor={$editor} items={searchMentions} item={mentionItemSnippet} /> 117 181 {/if} 118 182 </PlainTextEditor> 119 183 120 - {#if embed && embedPreview} 121 - <div class="mt-2"> 122 - {@render embedPreview({ embed, removeEmbed })} 123 - </div> 184 + {#if children} 185 + {@render children()} 124 186 {/if} 125 187 126 188 {#if maxLength}
+8 -1
packages/social/src/lib/components/post/Post.svelte
··· 27 27 28 28 showAvatar = true, 29 29 compact = false, 30 - target = '_blank' 30 + target = '_blank', 31 + extraEmbeds 31 32 }: WithElementRef<HTMLAttributes<HTMLDivElement>> & PostProps = $props(); 32 33 </script> 33 34 ··· 156 157 {#each embeds as embed} 157 158 <Embed {embed} {showSensitive} /> 158 159 {/each} 160 + {/if} 161 + 162 + {#if extraEmbeds} 163 + <div class="flex flex-col gap-2 pt-3 text-sm"> 164 + {@render extraEmbeds()} 165 + </div> 159 166 {/if} 160 167 161 168 {#if actions}
+1
packages/social/src/lib/components/post/types.ts
··· 50 50 51 51 embeds?: Embed[]; 52 52 showSensitive?: boolean; 53 + extraEmbeds?: Snippet; 53 54 54 55 actions?: ActionButtonsProps; 55 56
+23 -9
packages/text/src/lib/components/link/LinkExtension.ts
··· 1 1 import { Extension, InputRule, markInputRule, markPasteRule, PasteRule } from '@tiptap/core'; 2 2 import { Link } from '@tiptap/extension-link'; 3 + import { Plugin, PluginKey } from '@tiptap/pm/state'; 3 4 import { AddMarkStep } from '@tiptap/pm/transform'; 4 5 import type { LinkExtensionOptions } from './helpers'; 5 6 ··· 52 53 }; 53 54 }, 54 55 55 - onTransaction({ transaction }) { 56 - const onlinkadded = this.options.onlinkadded; 57 - if (!onlinkadded || !transaction.docChanged) return; 56 + addProseMirrorPlugins() { 57 + const ext = this; 58 + return [ 59 + new Plugin({ 60 + key: new PluginKey('linkAddedDetector'), 61 + appendTransaction(transactions, _oldState, newState) { 62 + const onlinkadded = ext.options.onlinkadded; 63 + if (!onlinkadded) return; 58 64 59 - for (const step of transaction.steps) { 60 - if (step instanceof AddMarkStep && step.mark.type.name === 'link') { 61 - const text = transaction.doc.textBetween(step.from, step.to); 62 - onlinkadded({ href: step.mark.attrs.href, text, editor: this.editor }); 63 - } 64 - } 65 + for (const transaction of transactions) { 66 + for (const step of transaction.steps) { 67 + if (step instanceof AddMarkStep && step.mark.type.name === 'link') { 68 + const text = newState.doc.textBetween(step.from, step.to); 69 + console.log('[LinkExtension] link detected:', { href: step.mark.attrs.href, text }); 70 + onlinkadded({ href: step.mark.attrs.href, text, editor: ext.editor }); 71 + } 72 + } 73 + } 74 + return null; 75 + } 76 + }) 77 + ]; 65 78 }, 66 79 67 80 addExtensions() { ··· 109 122 }, 110 123 addPasteRules() { 111 124 return [ 125 + ...(this.parent?.() ?? []), 112 126 linkPasteRule({ 113 127 find: pasteRegex, 114 128 type: this.type,
+1 -1
apps/docs/src/lib/docs/social/bluesky-post/Example.svelte
··· 15 15 </script> 16 16 17 17 <Box 18 - class="not-prose divide-base-200/50 dark:divide-base-800/70 relative flex h-96 w-full flex-col divide-y overflow-y-scroll" 18 + class="not-prose divide-base-200/50 dark:divide-base-800/70 relative flex h-96 max-w-lg flex-col divide-y overflow-y-scroll" 19 19 > 20 20 {#each feed as item (item.post.uri)} 21 21 <div class="py-4 first:pt-0 last:pb-0">
+95 -30
apps/docs/src/lib/docs/social/microblogging-post-creator/Example.svelte
··· 2 2 import { 3 3 MicrobloggingPostCreator, 4 4 createBlueskyMentionSearch, 5 - type MicrobloggingPostContent 5 + type MicrobloggingPostContent, 6 + type Embed, 7 + type ImageData, 8 + EmbedRouter, 9 + EmbedImage, 10 + Box 6 11 } from '@foxui/all'; 12 + import Avatar from '../../../../../../../packages/core/src/lib/components/avatar/Avatar.svelte'; 7 13 8 14 let content: MicrobloggingPostContent = $state({ text: '', json: { type: 'doc' } }); 9 - let embed: { type: string; url: string; text: string } | undefined = $state(undefined); 15 + type EmbedItem = 16 + | { type: 'image'; image: ImageData } 17 + | { type: 'video'; embed: Embed } 18 + | { type: 'link'; embed: Embed }; 19 + 20 + let embeds: EmbedItem[] = $state([]); 10 21 11 22 const searchMentions = createBlueskyMentionSearch(); 23 + 24 + function removeEmbed(index: number) { 25 + embeds = embeds.filter((_, i) => i !== index); 26 + } 12 27 </script> 13 28 14 - <div class="not-prose flex w-full flex-col gap-4"> 15 - <MicrobloggingPostCreator 16 - bind:content 17 - {searchMentions} 18 - bind:embed 19 - class="border-base-300 dark:border-base-700 rounded-2xl border p-4" 20 - > 21 - {#snippet embedPreview({ embed: e, removeEmbed })} 22 - <div 23 - class="border-base-300 dark:border-base-700 flex items-center gap-2 rounded-xl border p-3" 24 - > 25 - <span class="text-base-500 min-w-0 flex-1 truncate text-sm">{e.url}</span> 26 - <button 27 - onclick={removeEmbed} 28 - class="text-base-400 hover:text-base-600 dark:hover:text-base-300 text-xs" 29 - >Remove</button 30 - > 31 - </div> 32 - {/snippet} 33 - </MicrobloggingPostCreator> 34 - 35 - {#if content.text} 36 - <details class="text-sm"> 37 - <summary class="cursor-pointer">Output</summary> 38 - <pre class="mt-2 overflow-auto rounded-lg bg-black/5 p-3 dark:bg-white/5">{JSON.stringify(content, null, 2)}</pre> 39 - </details> 40 - {/if} 41 - </div> 29 + <Box class="not-prose gap-4"> 30 + <div class="flex w-full gap-4"> 31 + <Avatar src="https://github.com/flo-bit.png" /> 32 + <MicrobloggingPostCreator 33 + bind:content 34 + {searchMentions} 35 + textEditorClass="max-h-48 overflow-y-scroll min-h-24" 36 + class="grow" 37 + onlinkadded={(e) => { 38 + if (!embeds.some((em) => em.type === 'link' && em.embed.type === 'external' && em.embed.external.href === e.href)) { 39 + embeds = [ 40 + ...embeds, 41 + { 42 + type: 'link', 43 + embed: { 44 + type: 'external', 45 + external: { href: e.href, thumb: '', title: e.text, description: '' } 46 + } 47 + } 48 + ]; 49 + } 50 + }} 51 + onimageadded={(files) => { 52 + const newEmbeds: EmbedItem[] = files.map((f) => { 53 + const url = URL.createObjectURL(f); 54 + return { type: 'image', image: { alt: f.name, thumb: url, fullsize: url } }; 55 + }); 56 + embeds = [...embeds, ...newEmbeds]; 57 + }} 58 + onvideoadded={(files) => { 59 + for (const f of files) { 60 + const url = URL.createObjectURL(f); 61 + embeds = [ 62 + ...embeds, 63 + { 64 + type: 'video', 65 + embed: { type: 'video', video: { playlist: url, thumb: '', alt: f.name } } 66 + } 67 + ]; 68 + } 69 + }} 70 + > 71 + {#if embeds.length > 0} 72 + <div class="mt-2 flex flex-col gap-2"> 73 + {#each embeds as item, i} 74 + <div class="relative"> 75 + {#if item.type === 'image'} 76 + <EmbedImage image={item.image} /> 77 + {:else} 78 + <EmbedRouter embed={item.embed} /> 79 + {/if} 80 + <button 81 + type="button" 82 + class="bg-base-900/60 hover:bg-base-900/80 absolute top-2 right-2 z-10 cursor-pointer rounded-full p-1 text-white" 83 + onclick={() => removeEmbed(i)} 84 + > 85 + <svg 86 + xmlns="http://www.w3.org/2000/svg" 87 + fill="none" 88 + viewBox="0 0 24 24" 89 + stroke-width="2" 90 + stroke="currentColor" 91 + class="size-4" 92 + > 93 + <path 94 + stroke-linecap="round" 95 + stroke-linejoin="round" 96 + d="M6 18 18 6M6 6l12 12" 97 + /> 98 + </svg> 99 + </button> 100 + </div> 101 + {/each} 102 + </div> 103 + {/if} 104 + </MicrobloggingPostCreator> 105 + </div> 106 + </Box>
+2
apps/docs/src/routes/(main)/docs/quick-start/QuickStart.md
··· 28 28 @custom-variant dark (&:is(.dark *)); 29 29 30 30 @import '@foxui/core/theme.css'; 31 + 32 + @source "../node_modules/@foxui"; 31 33 ``` 32 34 33 35 ## 4. Use a component