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

Merge branch 'main' of https://github.com/flo-bit/ui-kit

Florian (Apr 21, 2026, 2:25 PM +0200) 2894dc08 83beb196

+180 -90
+7 -2
apps/docs/src/lib/docs/social/emoji-picker/Example.svelte
··· 13 13 {/each} 14 14 </div> 15 15 16 - <EmojiPicker onpicked={(emoji) => emojis.unshift(emoji.unicode)} /> 16 + <EmojiPicker search favorites onpicked={(emoji) => emojis.unshift(emoji.unicode)} /> 17 17 18 18 <h3>Popover Emoji Picker</h3> 19 19 20 - <PopoverEmojiPicker onpicked={(emoji) => emojis.unshift(emoji.unicode)} triggerText="Emoji Picker"> 20 + <PopoverEmojiPicker 21 + search 22 + favorites 23 + onpicked={(emoji) => emojis.unshift(emoji.unicode)} 24 + triggerText="Emoji Picker" 25 + > 21 26 {#snippet child({ props })} 22 27 <Button size="iconLg" {...props}> 23 28 <Smile size={24} strokeWidth={1.5} />
+24 -2
apps/docs/src/lib/docs/social/emoji-picker/api.ts
··· 17 17 width: { 18 18 type: 'number', 19 19 description: 'The width of the picker in pixels.', 20 - default: '344' 20 + default: '366' 21 21 }, 22 22 columns: { 23 23 type: 'number', 24 24 description: 'The number of emoji columns.', 25 - default: '8' 25 + default: '9' 26 + }, 27 + search: { 28 + type: 'boolean', 29 + description: 'Whether to show a search tab for querying emojis by name.', 30 + default: 'false' 31 + }, 32 + favorites: { 33 + type: 'boolean', 34 + description: 35 + 'Whether to show a favorites tab of the most-picked emojis (persisted in IndexedDB).', 36 + default: 'false' 26 37 }, 27 38 class: { 28 39 type: 'string', ··· 52 63 children: { 53 64 type: 'Snippet', 54 65 description: 'Custom trigger content.' 66 + }, 67 + search: { 68 + type: 'boolean', 69 + description: 'Whether to show a search tab for querying emojis by name.', 70 + default: 'false' 71 + }, 72 + favorites: { 73 + type: 'boolean', 74 + description: 75 + 'Whether to show a favorites tab of the most-picked emojis (persisted in IndexedDB).', 76 + default: 'false' 55 77 }, 56 78 class: { 57 79 type: 'string',
+97 -27
packages/social/src/lib/components/emoji-picker/EmojiPicker.svelte
··· 10 10 }); 11 11 12 12 export function loadEmojis() { 13 - if(emojiDatabase.db) return; 13 + if (emojiDatabase.db) return; 14 14 15 15 import('emoji-picker-element').then(({ Database }) => { 16 16 emojiDatabase.db = new Database(); ··· 22 22 } 23 23 }); 24 24 }); 25 - console.log('emojis loaded'); 26 25 } 27 26 </script> 28 27 29 28 <script lang="ts"> 30 - import { cn, ScrollArea } from '@foxui/core'; 31 - import { allGroups } from './emoji'; 32 - import type { NativeEmoji } from 'emoji-picker-element/shared'; 29 + import { cn, Input, ScrollArea } from '@foxui/core'; 30 + import { 31 + allGroups, 32 + FAVORITES_GROUP_ID, 33 + favoritesGroup, 34 + SEARCH_GROUP_ID, 35 + searchGroup 36 + } from './emoji'; 37 + import type { Emoji, NativeEmoji } from 'emoji-picker-element/shared'; 33 38 import { fade } from 'svelte/transition'; 34 - import { onMount } from 'svelte'; 39 + import { onMount, tick } from 'svelte'; 35 40 36 41 let currentGroup = $state(allGroups[0].id); 42 + let query = $state(''); 43 + let searchInputRef = $state<HTMLInputElement | null>(null); 44 + let favoritesVersion = $state(0); 37 45 38 46 let { 39 47 onpicked, 40 48 height = 300, 41 - width = 344, 42 - columns = 8, 49 + width = 366, 50 + columns = 9, 51 + search = false, 52 + favorites = false, 43 53 class: className 44 54 }: { 45 55 onpicked?: (emoji: NativeEmoji) => void; 46 56 height?: number; 47 57 width?: number; 48 58 columns?: number; 59 + search?: boolean; 60 + favorites?: boolean; 49 61 class?: string; 50 62 } = $props(); 51 63 64 + const SEARCH_INPUT_HEIGHT = 48; 65 + const FAVORITES_LIMIT = 40; 66 + 67 + let tabs = $derived([ 68 + ...(search ? [searchGroup] : []), 69 + ...(favorites ? [favoritesGroup] : []), 70 + ...allGroups 71 + ]); 72 + let isSearching = $derived(currentGroup === SEARCH_GROUP_ID); 73 + let scrollHeight = $derived(isSearching ? height - SEARCH_INPUT_HEIGHT : height); 74 + let trimmedQuery = $derived(query.trim()); 75 + 76 + let emojisPromise = $derived.by(() => { 77 + if (!emojiDatabase.db) return Promise.resolve<Emoji[] | undefined>(undefined); 78 + if (isSearching) { 79 + if (trimmedQuery.length < 2) return Promise.resolve<Emoji[] | undefined>([]); 80 + return emojiDatabase.db.getEmojiBySearchQuery(trimmedQuery); 81 + } 82 + if (currentGroup === FAVORITES_GROUP_ID) { 83 + favoritesVersion; 84 + return emojiDatabase.db.getTopFavoriteEmoji(FAVORITES_LIMIT); 85 + } 86 + return emojiDatabase.db.getEmojiByGroup(currentGroup); 87 + }); 88 + 89 + async function selectTab(id: number) { 90 + if (id !== SEARCH_GROUP_ID) query = ''; 91 + if (id === FAVORITES_GROUP_ID) favoritesVersion++; 92 + currentGroup = id; 93 + if (id === SEARCH_GROUP_ID) { 94 + await tick(); 95 + searchInputRef?.focus(); 96 + } 97 + } 98 + 99 + function isNative(emoji: Emoji): emoji is NativeEmoji { 100 + return 'unicode' in emoji; 101 + } 102 + 103 + function pickEmoji(emoji: NativeEmoji) { 104 + onpicked?.(emoji); 105 + emojiDatabase.db?.incrementFavoriteEmojiCount(emoji.unicode); 106 + } 107 + 52 108 onMount(() => { 53 109 loadEmojis(); 54 - }) 110 + }); 55 111 </script> 56 112 57 113 <div class={cn('flex flex-col', className)} style="height: {height}px; width: {width}px;"> 58 - <ScrollArea 59 - class="grid w-full select-none space-y-0 px-2" 60 - style="height: {height}px; grid-template-columns: repeat({columns}, minmax(0, 1fr));" 61 - > 62 - {#await emojiDatabase.db?.getEmojiByGroup(currentGroup) then emojis} 63 - {#if emojis} 64 - {#each emojis as emoji} 65 - {#if isEmojiSupported(emoji.unicode)} 114 + {#if isSearching} 115 + <div class="px-2 pt-2 pb-1" style="height: {SEARCH_INPUT_HEIGHT}px;"> 116 + <Input 117 + bind:ref={searchInputRef} 118 + bind:value={query} 119 + sizeVariant="sm" 120 + type="search" 121 + placeholder="Search emojiโ€ฆ" 122 + class="w-full" 123 + /> 124 + </div> 125 + {/if} 126 + <ScrollArea class="w-full" style="height: {scrollHeight}px;"> 127 + <div 128 + class="grid w-full select-none auto-rows-min px-2" 129 + style="grid-template-columns: repeat({columns}, minmax(0, 1fr));" 130 + > 131 + {#await emojisPromise then emojis} 132 + {#if emojis} 133 + {@const visible = emojis.filter( 134 + (e): e is NativeEmoji => isNative(e) && isEmojiSupported(e.unicode) 135 + )} 136 + {#each visible as emoji, i (i)} 66 137 <button 67 - onclick={() => { 68 - onpicked?.(emoji); 69 - }} 138 + onclick={() => pickEmoji(emoji)} 70 139 class="hover:bg-accent-300/20 dark:hover:bg-accent-700/20 size-10 cursor-pointer rounded-full text-center text-xl transition-transform duration-150 hover:scale-110" 71 140 >{emoji.unicode}</button 72 141 > 73 - {/if} 74 - {/each} 75 - {/if} 76 - {/await} 142 + {/each} 143 + {/if} 144 + {/await} 145 + </div> 77 146 </ScrollArea> 78 147 <div 79 148 class="border-base-300/50 dark:border-base-700/50 flex justify-between gap-2 border-t px-3" 80 149 style="width: {width}px;" 81 150 > 82 - {#each allGroups as group} 151 + {#each tabs as group (group.id)} 152 + {@const Icon = group.icon} 83 153 <button 84 - onclick={() => (currentGroup = group.id)} 154 + onclick={() => selectTab(group.id)} 85 155 class={cn( 86 156 '[&>svg]:size-4.5 relative cursor-pointer py-2 [&>svg]:transition-all [&>svg]:duration-100 [&>svg]:hover:scale-105', 87 157 group.id === currentGroup ··· 89 159 : 'hover:text-accent-700 dark:hover:text-accent-300' 90 160 )} 91 161 > 92 - {@html group.svg} 162 + <Icon strokeWidth={1.5} /> 93 163 <span class="sr-only">{group.name}</span> 94 164 95 165 {#if group.id === currentGroup}
+5 -1
packages/social/src/lib/components/emoji-picker/PopoverEmojiPicker.svelte
··· 10 10 class: className, 11 11 open = $bindable(false), 12 12 triggerRef = $bindable(null), 13 + search = false, 14 + favorites = false, 13 15 ...props 14 16 }: { 15 17 onpicked?: (emoji: NativeEmoji) => void; 16 18 children?: Snippet; 17 19 class?: string; 20 + search?: boolean; 21 + favorites?: boolean; 18 22 } & PopoverProps = $props(); 19 23 </script> 20 24 21 25 <Popover {...props} bind:triggerRef bind:open class={cn('p-0', className)}> 22 26 {@render children?.()} 23 - <EmojiPicker {onpicked} /> 27 + <EmojiPicker {onpicked} {search} {favorites} /> 24 28 </Popover>
+47 -58
packages/social/src/lib/components/emoji-picker/emoji.ts
··· 1 - export const allGroups = [ 2 - [ 3 - 0, 4 - '๐Ÿ˜€', 5 - 'smileys-emotion', 6 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><line x1="9" x2="9.01" y1="9" y2="9"/><line x1="15" x2="15.01" y1="9" y2="9"/></svg>` 7 - ], 8 - [ 9 - 1, 10 - '๐Ÿ‘‹', 11 - 'people-body', 12 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hand-icon lucide-hand"><path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"/><path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"/><path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"/><path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"/></svg>` 13 - ], 14 - [ 15 - 3, 16 - '๐Ÿฑ', 17 - 'animals-nature', 18 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dog-icon lucide-dog"><path d="M11.25 16.25h1.5L12 17z"/><path d="M16 14v.5"/><path d="M4.42 11.247A13.152 13.152 0 0 0 4 14.556C4 18.728 7.582 21 12 21s8-2.272 8-6.444a11.702 11.702 0 0 0-.493-3.309"/><path d="M8 14v.5"/><path d="M8.5 8.5c-.384 1.05-1.083 2.028-2.344 2.5-1.931.722-3.576-.297-3.656-1-.113-.994 1.177-6.53 4-7 1.923-.321 3.651.845 3.651 2.235A7.497 7.497 0 0 1 14 5.277c0-1.39 1.844-2.598 3.767-2.277 2.823.47 4.113 6.006 4 7-.08.703-1.725 1.722-3.656 1-1.261-.472-1.855-1.45-2.239-2.5"/></svg>` 19 - ], 20 - [ 21 - 4, 22 - '๐ŸŽ', 23 - 'food-drink', 24 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-apple-icon lucide-apple"><path d="M12 20.94c1.5 0 2.75 1.06 4 1.06 3 0 6-8 6-12.22A4.91 4.91 0 0 0 17 5c-2.22 0-4 1.44-5 2-1-.56-2.78-2-5-2a4.9 4.9 0 0 0-5 4.78C2 14 5 22 8 22c1.25 0 2.5-1.06 4-1.06Z"/><path d="M10 2c1 .5 2 2 2 5"/></svg> ` 25 - ], 26 - [ 27 - 5, 28 - '๐Ÿ ๏ธ', 29 - 'travel-places', 30 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-house-icon lucide-house"><path d="M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8"/><path d="M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>` 31 - ], 32 - [ 33 - 6, 34 - 'โšฝ', 35 - 'activities', 36 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-volleyball-icon lucide-volleyball"><path d="M11.1 7.1a16.55 16.55 0 0 1 10.9 4"/><path d="M12 12a12.6 12.6 0 0 1-8.7 5"/><path d="M16.8 13.6a16.55 16.55 0 0 1-9 7.5"/><path d="M20.7 17a12.8 12.8 0 0 0-8.7-5 13.3 13.3 0 0 1 0-10"/><path d="M6.3 3.8a16.55 16.55 0 0 0 1.9 11.5"/><circle cx="12" cy="12" r="10"/></svg>` 37 - ], 38 - [ 39 - 7, 40 - '๐Ÿ“', 41 - 'objects', 42 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-lightbulb-icon lucide-lightbulb"><path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"/><path d="M9 18h6"/><path d="M10 22h4"/></svg>` 43 - ], 44 - [ 45 - 8, 46 - 'โ›”๏ธ', 47 - 'symbols', 48 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-square-pi-icon lucide-square-pi"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 7h10"/><path d="M10 7v10"/><path d="M16 17a2 2 0 0 1-2-2V7"/></svg>` 49 - ], 50 - [ 51 - 9, 52 - '๐Ÿ', 53 - 'flags', 54 - `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-flag-icon lucide-flag"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" x2="4" y1="22" y2="15"/></svg>` 55 - ] 56 - ].map(([id, emoji, name, svg]) => ({ id, emoji, name, svg })) as { 1 + import { 2 + Apple, 3 + Dog, 4 + Flag, 5 + Hand, 6 + House, 7 + Lightbulb, 8 + Search, 9 + Smile, 10 + SquarePi, 11 + Star, 12 + Volleyball 13 + } from '@foxui/core/icons'; 14 + import type { Component } from 'svelte'; 15 + 16 + export type EmojiGroup = { 57 17 id: number; 58 18 emoji: string; 59 19 name: string; 60 - svg: string; 61 - }[]; 20 + icon: Component; 21 + }; 22 + 23 + export const allGroups: EmojiGroup[] = [ 24 + { id: 0, emoji: '๐Ÿ˜€', name: 'smileys-emotion', icon: Smile }, 25 + { id: 1, emoji: '๐Ÿ‘‹', name: 'people-body', icon: Hand }, 26 + { id: 3, emoji: '๐Ÿฑ', name: 'animals-nature', icon: Dog }, 27 + { id: 4, emoji: '๐ŸŽ', name: 'food-drink', icon: Apple }, 28 + { id: 5, emoji: '๐Ÿ ๏ธ', name: 'travel-places', icon: House }, 29 + { id: 6, emoji: 'โšฝ', name: 'activities', icon: Volleyball }, 30 + { id: 7, emoji: '๐Ÿ“', name: 'objects', icon: Lightbulb }, 31 + { id: 8, emoji: 'โ›”๏ธ', name: 'symbols', icon: SquarePi }, 32 + { id: 9, emoji: '๐Ÿ', name: 'flags', icon: Flag } 33 + ]; 34 + 35 + export const SEARCH_GROUP_ID = -1; 36 + export const FAVORITES_GROUP_ID = -2; 37 + 38 + export const searchGroup: EmojiGroup = { 39 + id: SEARCH_GROUP_ID, 40 + emoji: '๐Ÿ”', 41 + name: 'search', 42 + icon: Search 43 + }; 44 + 45 + export const favoritesGroup: EmojiGroup = { 46 + id: FAVORITES_GROUP_ID, 47 + emoji: 'โญ', 48 + name: 'favorites', 49 + icon: Star 50 + }; 62 51 63 52 export type { NativeEmoji } from 'emoji-picker-element/shared';