|
|
<script lang="ts"> |
|
|
import { sessionManager } from '../../services/auth/session-manager.js'; |
|
|
import { signAndPublish } from '../../services/nostr/auth-handler.js'; |
|
|
import { nostrClient } from '../../services/nostr/nostr-client.js'; |
|
|
import { onMount } from 'svelte'; |
|
|
import type { NostrEvent } from '../../types/nostr.js'; |
|
|
import emojiData from 'unicode-emoji-json/data-ordered-emoji.json'; |
|
|
import { resolveCustomEmojis } from '../../services/nostr/nip30-emoji.js'; |
|
|
|
|
|
interface Props { |
|
|
event: NostrEvent; // Feed event |
|
|
} |
|
|
|
|
|
let { event }: Props = $props(); |
|
|
|
|
|
let reactions = $state<Map<string, { content: string; pubkeys: Set<string> }>>(new Map()); |
|
|
let userReaction = $state<string | null>(null); |
|
|
let loading = $state(true); |
|
|
let showMenu = $state(false); |
|
|
let menuButton: HTMLButtonElement | null = $state(null); |
|
|
let menuPosition = $state<'above' | 'below'>('above'); // Track menu position |
|
|
let customEmojiUrls = $state<Map<string, string>>(new Map()); // Map of :shortcode: -> image URL |
|
|
let emojiSearchQuery = $state(''); |
|
|
let isMobile = $state(false); |
|
|
|
|
|
// Derived value for heart count |
|
|
let heartCount = $derived(getReactionCount('+')); |
|
|
|
|
|
// Get emoji list from unicode-emoji-json library |
|
|
// The library provides an array of emoji strings in order |
|
|
const reactionMenu = $derived.by(() => { |
|
|
const emojis: string[] = ['+']; // Heart (default) always first |
|
|
|
|
|
// Add ALL emojis from the library - the menu will scroll |
|
|
// The data-ordered-emoji.json is already an array of emoji strings |
|
|
for (let i = 0; i < emojiData.length; i++) { |
|
|
const emoji = emojiData[i]; |
|
|
if (typeof emoji === 'string' && emoji.trim()) { |
|
|
emojis.push(emoji); |
|
|
} |
|
|
} |
|
|
|
|
|
return emojis; |
|
|
}); |
|
|
|
|
|
// Filter emojis based on search query |
|
|
const filteredReactionMenu = $derived.by(() => { |
|
|
if (!emojiSearchQuery.trim()) { |
|
|
return reactionMenu; |
|
|
} |
|
|
|
|
|
const query = emojiSearchQuery.toLowerCase().trim(); |
|
|
return reactionMenu.filter(emoji => { |
|
|
// Search by emoji character itself |
|
|
if (emoji.toLowerCase().includes(query)) { |
|
|
return true; |
|
|
} |
|
|
// For custom emojis, search by shortcode |
|
|
if (emoji.startsWith(':') && emoji.endsWith(':')) { |
|
|
return emoji.toLowerCase().includes(query); |
|
|
} |
|
|
// Try to match emoji unicode name (basic approach) |
|
|
// This is a simple search - could be enhanced with proper emoji name data |
|
|
return false; |
|
|
}); |
|
|
}); |
|
|
|
|
|
// Custom emoji reactions (like :turtlehappy_sm:) |
|
|
// These will be added dynamically from actual reactions received |
|
|
|
|
|
onMount(() => { |
|
|
nostrClient.initialize().then(() => { |
|
|
loadReactions(); |
|
|
}); |
|
|
|
|
|
// Check if mobile on mount and resize |
|
|
checkMobile(); |
|
|
window.addEventListener('resize', checkMobile); |
|
|
|
|
|
return () => { |
|
|
window.removeEventListener('resize', checkMobile); |
|
|
}; |
|
|
}); |
|
|
|
|
|
function checkMobile() { |
|
|
isMobile = window.innerWidth < 768; // Match Tailwind's md breakpoint |
|
|
} |
|
|
|
|
|
async function loadReactions() { |
|
|
loading = true; |
|
|
try { |
|
|
const config = nostrClient.getConfig(); |
|
|
|
|
|
const filters = [ |
|
|
{ |
|
|
kinds: [7], |
|
|
'#e': [event.id] |
|
|
} |
|
|
]; |
|
|
|
|
|
const reactionEvents = await nostrClient.fetchEvents( |
|
|
filters, |
|
|
[...config.defaultRelays], |
|
|
{ useCache: true, cacheResults: true, onUpdate: (updated) => { |
|
|
processReactions(updated); |
|
|
}} |
|
|
); |
|
|
|
|
|
processReactions(reactionEvents); |
|
|
} catch (error) { |
|
|
console.error('Error loading reactions:', error); |
|
|
} finally { |
|
|
loading = false; |
|
|
} |
|
|
} |
|
|
|
|
|
async function processReactions(reactionEvents: NostrEvent[]) { |
|
|
const reactionMap = new Map<string, { content: string; pubkeys: Set<string> }>(); |
|
|
const currentUser = sessionManager.getCurrentPubkey(); |
|
|
|
|
|
for (const reactionEvent of reactionEvents) { |
|
|
const content = reactionEvent.content.trim(); |
|
|
|
|
|
if (!reactionMap.has(content)) { |
|
|
reactionMap.set(content, { content, pubkeys: new Set() }); |
|
|
} |
|
|
reactionMap.get(content)!.pubkeys.add(reactionEvent.pubkey); |
|
|
|
|
|
if (currentUser && reactionEvent.pubkey === currentUser) { |
|
|
userReaction = content; |
|
|
} |
|
|
} |
|
|
|
|
|
reactions = reactionMap; |
|
|
|
|
|
// Resolve custom emojis (NIP-30) to image URLs |
|
|
const emojiUrls = await resolveCustomEmojis(reactionMap); |
|
|
customEmojiUrls = emojiUrls; |
|
|
} |
|
|
|
|
|
async function toggleReaction(content: string) { |
|
|
if (!sessionManager.isLoggedIn()) { |
|
|
alert('Please log in to react'); |
|
|
return; |
|
|
} |
|
|
|
|
|
if (userReaction === content) { |
|
|
// Remove reaction |
|
|
userReaction = null; |
|
|
const reaction = reactions.get(content); |
|
|
if (reaction) { |
|
|
const currentUser = sessionManager.getCurrentPubkey(); |
|
|
if (currentUser) { |
|
|
reaction.pubkeys.delete(currentUser); |
|
|
if (reaction.pubkeys.size === 0) { |
|
|
reactions.delete(content); |
|
|
} |
|
|
} |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const tags: string[][] = [ |
|
|
['e', event.id], |
|
|
['p', event.pubkey], |
|
|
['k', '1'] |
|
|
]; |
|
|
|
|
|
if (sessionManager.getCurrentPubkey() && includeClientTag) { |
|
|
tags.push(['client', 'Aitherboard']); |
|
|
} |
|
|
|
|
|
const reactionEvent: Omit<NostrEvent, 'id' | 'sig'> = { |
|
|
kind: 7, |
|
|
pubkey: sessionManager.getCurrentPubkey()!, |
|
|
created_at: Math.floor(Date.now() / 1000), |
|
|
tags, |
|
|
content |
|
|
}; |
|
|
|
|
|
const config = nostrClient.getConfig(); |
|
|
await signAndPublish(reactionEvent, [...config.defaultRelays]); |
|
|
|
|
|
userReaction = content; |
|
|
const currentPubkey = sessionManager.getCurrentPubkey()!; |
|
|
if (!reactions.has(content)) { |
|
|
reactions.set(content, { content, pubkeys: new Set([currentPubkey]) }); |
|
|
} else { |
|
|
reactions.get(content)!.pubkeys.add(currentPubkey); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Error publishing reaction:', error); |
|
|
alert('Error publishing reaction'); |
|
|
} |
|
|
} |
|
|
|
|
|
function getReactionDisplay(content: string): string { |
|
|
if (content === '+') return '❤️'; |
|
|
|
|
|
// Check if this is a custom emoji with a resolved URL |
|
|
if (content.startsWith(':') && content.endsWith(':')) { |
|
|
const url = customEmojiUrls.get(content); |
|
|
if (url) { |
|
|
// Return a placeholder that will be replaced with img tag in template |
|
|
return content; // We'll render as img in template |
|
|
} |
|
|
} |
|
|
|
|
|
return content; |
|
|
} |
|
|
|
|
|
function isCustomEmoji(content: string): boolean { |
|
|
return content.startsWith(':') && content.endsWith(':') && customEmojiUrls.has(content); |
|
|
} |
|
|
|
|
|
function getCustomEmojiUrl(content: string): string | null { |
|
|
return customEmojiUrls.get(content) || null; |
|
|
} |
|
|
|
|
|
function getReactionCount(content: string): number { |
|
|
return reactions.get(content)?.pubkeys.size || 0; |
|
|
} |
|
|
|
|
|
function getAllReactions(): Array<{ content: string; count: number }> { |
|
|
// Get all reactions that have counts > 0, sorted by count (descending) |
|
|
const allReactions: Array<{ content: string; count: number }> = []; |
|
|
for (const [content, data] of reactions.entries()) { |
|
|
if (data.pubkeys.size > 0) { |
|
|
allReactions.push({ content, count: data.pubkeys.size }); |
|
|
} |
|
|
} |
|
|
// Sort by count descending, then by content |
|
|
return allReactions.sort((a, b) => { |
|
|
if (b.count !== a.count) return b.count - a.count; |
|
|
return a.content.localeCompare(b.content); |
|
|
}); |
|
|
} |
|
|
|
|
|
function getCustomEmojis(): string[] { |
|
|
// Extract custom emoji reactions (format: :name:) |
|
|
const customEmojis: string[] = []; |
|
|
for (const content of reactions.keys()) { |
|
|
if (content.startsWith(':') && content.endsWith(':') && !reactionMenu.includes(content)) { |
|
|
customEmojis.push(content); |
|
|
} |
|
|
} |
|
|
return customEmojis.sort(); |
|
|
} |
|
|
|
|
|
// Filter custom emojis based on search query |
|
|
const filteredCustomEmojis = $derived.by(() => { |
|
|
const customEmojis = getCustomEmojis(); |
|
|
if (!emojiSearchQuery.trim()) { |
|
|
return customEmojis; |
|
|
} |
|
|
|
|
|
const query = emojiSearchQuery.toLowerCase().trim(); |
|
|
return customEmojis.filter(emoji => { |
|
|
// Search by shortcode (e.g., :turtlehappy_sm:) |
|
|
return emoji.toLowerCase().includes(query); |
|
|
}); |
|
|
}); |
|
|
|
|
|
function closeMenuOnOutsideClick(e: MouseEvent) { |
|
|
const target = e.target as HTMLElement; |
|
|
if (menuButton && |
|
|
!menuButton.contains(target) && |
|
|
!target.closest('.reaction-menu')) { |
|
|
showMenu = false; |
|
|
emojiSearchQuery = ''; // Clear search when closing |
|
|
} |
|
|
} |
|
|
|
|
|
function handleHeartClick() { |
|
|
if (showMenu) { |
|
|
// If menu is open, clicking heart again should just like/unlike |
|
|
toggleReaction('+'); |
|
|
showMenu = false; |
|
|
emojiSearchQuery = ''; // Clear search when closing |
|
|
} else { |
|
|
// On mobile, always use bottom drawer |
|
|
if (isMobile) { |
|
|
menuPosition = 'below'; |
|
|
} else { |
|
|
// Check if there's enough space above the button |
|
|
if (menuButton) { |
|
|
const rect = menuButton.getBoundingClientRect(); |
|
|
const spaceAbove = rect.top; |
|
|
const spaceBelow = window.innerHeight - rect.bottom; |
|
|
// Position below if there's more space below or if space above is less than 300px |
|
|
menuPosition = spaceBelow > spaceAbove || spaceAbove < 300 ? 'below' : 'above'; |
|
|
} |
|
|
} |
|
|
// If menu is closed, open it |
|
|
showMenu = true; |
|
|
emojiSearchQuery = ''; // Clear search when opening |
|
|
|
|
|
// Focus search input after menu opens (using setTimeout to ensure DOM is ready) |
|
|
if (!isMobile) { |
|
|
setTimeout(() => { |
|
|
const searchInput = document.querySelector('.emoji-search-input') as HTMLInputElement; |
|
|
if (searchInput) { |
|
|
searchInput.focus(); |
|
|
} |
|
|
}, 0); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
function handleSearchInput(e: Event) { |
|
|
const target = e.target as HTMLInputElement; |
|
|
emojiSearchQuery = target.value; |
|
|
} |
|
|
|
|
|
// Action to focus input when menu opens |
|
|
function focusOnMount(node: HTMLInputElement, shouldFocus: boolean) { |
|
|
if (shouldFocus) { |
|
|
setTimeout(() => node.focus(), 0); |
|
|
} |
|
|
return { |
|
|
update(newShouldFocus: boolean) { |
|
|
if (newShouldFocus) { |
|
|
setTimeout(() => node.focus(), 0); |
|
|
} |
|
|
} |
|
|
}; |
|
|
} |
|
|
|
|
|
$effect(() => { |
|
|
if (showMenu) { |
|
|
document.addEventListener('click', closeMenuOnOutsideClick); |
|
|
return () => document.removeEventListener('click', closeMenuOnOutsideClick); |
|
|
} |
|
|
}); |
|
|
|
|
|
let includeClientTag = $state(true); |
|
|
</script> |
|
|
|
|
|
<div class="Feed-reaction-buttons flex gap-2 items-center flex-wrap"> |
|
|
<!-- Heart button - always visible, opens menu --> |
|
|
<div class="reaction-wrapper"> |
|
|
<button |
|
|
bind:this={menuButton} |
|
|
onclick={handleHeartClick} |
|
|
class="reaction-btn heart-btn {userReaction === '+' ? 'active' : ''}" |
|
|
title="Like or choose reaction" |
|
|
aria-label="Like or choose reaction" |
|
|
> |
|
|
❤️ {heartCount > 0 ? heartCount : ''} |
|
|
</button> |
|
|
|
|
|
<!-- Reaction menu dropdown --> |
|
|
{#if showMenu} |
|
|
{#if isMobile} |
|
|
<!-- Mobile backdrop --> |
|
|
<div |
|
|
class="mobile-drawer-backdrop" |
|
|
onclick={() => { showMenu = false; emojiSearchQuery = ''; }} |
|
|
onkeydown={(e) => { |
|
|
if (e.key === 'Enter' || e.key === ' ') { |
|
|
e.preventDefault(); |
|
|
showMenu = false; |
|
|
emojiSearchQuery = ''; |
|
|
} |
|
|
}} |
|
|
role="button" |
|
|
tabindex="0" |
|
|
aria-label="Close emoji menu" |
|
|
></div> |
|
|
{/if} |
|
|
<div class="reaction-menu" class:menu-below={menuPosition === 'below'} class:mobile-drawer={isMobile}> |
|
|
<!-- Search box - always visible at top --> |
|
|
<div class="emoji-search-container"> |
|
|
<input |
|
|
type="text" |
|
|
placeholder="Search emojis..." |
|
|
value={emojiSearchQuery} |
|
|
oninput={handleSearchInput} |
|
|
class="emoji-search-input" |
|
|
aria-label="Search emojis" |
|
|
use:focusOnMount={!isMobile} |
|
|
/> |
|
|
</div> |
|
|
|
|
|
<!-- Scrollable content area --> |
|
|
<div class="reaction-menu-content"> |
|
|
<div class="reaction-menu-grid"> |
|
|
{#each filteredReactionMenu as reaction} |
|
|
{@const count = getReactionCount(reaction)} |
|
|
<button |
|
|
onclick={() => { |
|
|
toggleReaction(reaction); |
|
|
showMenu = false; |
|
|
}} |
|
|
class="reaction-menu-item {userReaction === reaction ? 'active' : ''}" |
|
|
title={reaction === '+' ? 'Like' : `React with ${getReactionDisplay(reaction)}`} |
|
|
> |
|
|
{#if isCustomEmoji(reaction)} |
|
|
{@const url = getCustomEmojiUrl(reaction)} |
|
|
{#if url} |
|
|
<img src={url} alt={reaction} class="custom-emoji-img" /> |
|
|
{:else} |
|
|
{reaction} |
|
|
{/if} |
|
|
{:else} |
|
|
{getReactionDisplay(reaction)} |
|
|
{/if} |
|
|
{#if count > 0} |
|
|
<span class="reaction-count">{count}</span> |
|
|
{/if} |
|
|
</button> |
|
|
{/each} |
|
|
</div> |
|
|
|
|
|
<!-- Custom emojis section --> |
|
|
{#if filteredCustomEmojis.length > 0} |
|
|
<div class="custom-emojis-section"> |
|
|
<div class="custom-emojis-label">Custom</div> |
|
|
<div class="reaction-menu-grid"> |
|
|
{#each filteredCustomEmojis as emoji} |
|
|
{@const count = getReactionCount(emoji)} |
|
|
<button |
|
|
onclick={() => { |
|
|
toggleReaction(emoji); |
|
|
showMenu = false; |
|
|
}} |
|
|
class="reaction-menu-item {userReaction === emoji ? 'active' : ''}" |
|
|
title={`React with ${emoji}`} |
|
|
> |
|
|
{#if isCustomEmoji(emoji)} |
|
|
{@const url = getCustomEmojiUrl(emoji)} |
|
|
{#if url} |
|
|
<img src={url} alt={emoji} class="custom-emoji-img" /> |
|
|
{:else} |
|
|
{emoji} |
|
|
{/if} |
|
|
{:else} |
|
|
{emoji} |
|
|
{/if} |
|
|
{#if count > 0} |
|
|
<span class="reaction-count">{count}</span> |
|
|
{/if} |
|
|
</button> |
|
|
{/each} |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
</div> |
|
|
</div> |
|
|
{/if} |
|
|
</div> |
|
|
|
|
|
<!-- Display all other reactions that have counts > 0 (except + which is shown as heart) --> |
|
|
{#each getAllReactions() as { content, count }} |
|
|
{#if content !== '+'} |
|
|
<button |
|
|
onclick={() => toggleReaction(content)} |
|
|
class="reaction-btn {userReaction === content ? 'active' : ''}" |
|
|
title={`React with ${content}`} |
|
|
aria-label={`React with ${content}`} |
|
|
> |
|
|
{#if isCustomEmoji(content)} |
|
|
{@const url = getCustomEmojiUrl(content)} |
|
|
{#if url} |
|
|
<img src={url} alt={content} class="custom-emoji-img" /> |
|
|
{:else} |
|
|
{content} |
|
|
{/if} |
|
|
{:else} |
|
|
{content} |
|
|
{/if} |
|
|
{count} |
|
|
</button> |
|
|
{/if} |
|
|
{/each} |
|
|
</div> |
|
|
|
|
|
<style> |
|
|
.Feed-reaction-buttons { |
|
|
margin-top: 0.5rem; |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.Feed-reaction-buttons { |
|
|
gap: 0.375rem; /* Smaller gap on mobile */ |
|
|
} |
|
|
|
|
|
.reaction-btn { |
|
|
padding: 0.25rem 0.5rem; /* Smaller padding on mobile */ |
|
|
font-size: 0.8125rem; /* Slightly smaller text */ |
|
|
} |
|
|
} |
|
|
|
|
|
.reaction-btn { |
|
|
padding: 0.25rem 0.75rem; |
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
border-radius: 0.25rem; |
|
|
background: var(--fog-post, #ffffff); |
|
|
color: var(--fog-text, #1f2937); |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
font-size: 0.875rem; |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-btn { |
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
} |
|
|
|
|
|
.reaction-btn:hover { |
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
border-color: var(--fog-accent, #64748b); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-btn:hover { |
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
} |
|
|
|
|
|
.reaction-btn.active { |
|
|
background: var(--fog-accent, #64748b); |
|
|
color: var(--fog-text, #475569); |
|
|
border-color: var(--fog-accent, #64748b); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-btn.active { |
|
|
background: var(--fog-dark-accent, #64748b); |
|
|
color: var(--fog-dark-text, #cbd5e1); |
|
|
border-color: var(--fog-dark-accent, #64748b); |
|
|
} |
|
|
|
|
|
.reaction-wrapper { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.heart-btn { |
|
|
/* Heart button styling */ |
|
|
} |
|
|
|
|
|
.reaction-menu { |
|
|
position: absolute; |
|
|
bottom: 100%; |
|
|
left: 0; |
|
|
margin-bottom: 0.5rem; |
|
|
background: var(--fog-post, #ffffff); |
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
border-radius: 0.5rem; |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
padding: 0.5rem; |
|
|
z-index: 1000; |
|
|
min-width: 200px; |
|
|
max-width: 300px; |
|
|
max-height: min(60vh, 400px); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
overflow: hidden; |
|
|
/* Ensure scrollbar is always visible */ |
|
|
scrollbar-width: thin; |
|
|
scrollbar-color: var(--fog-border, #e5e7eb) var(--fog-post, #ffffff); |
|
|
} |
|
|
|
|
|
.reaction-menu-content { |
|
|
overflow-y: auto; |
|
|
overflow-x: hidden; |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
/* Mobile drawer styles */ |
|
|
.reaction-menu.mobile-drawer { |
|
|
position: fixed; |
|
|
bottom: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
top: auto; |
|
|
margin: 0; |
|
|
border-radius: 1rem 1rem 0 0; |
|
|
max-width: 100%; |
|
|
max-height: 70vh; |
|
|
min-width: auto; |
|
|
width: 100%; |
|
|
box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1), 0 -2px 4px -1px rgba(0, 0, 0, 0.06); |
|
|
animation: slideUp 0.3s ease-out; |
|
|
} |
|
|
|
|
|
@keyframes slideUp { |
|
|
from { |
|
|
transform: translateY(100%); |
|
|
} |
|
|
to { |
|
|
transform: translateY(0); |
|
|
} |
|
|
} |
|
|
|
|
|
/* Backdrop for mobile drawer */ |
|
|
.mobile-drawer-backdrop { |
|
|
position: fixed; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
bottom: 0; |
|
|
background: rgba(0, 0, 0, 0.5); |
|
|
z-index: 999; |
|
|
animation: fadeIn 0.3s ease-out; |
|
|
} |
|
|
|
|
|
@keyframes fadeIn { |
|
|
from { |
|
|
opacity: 0; |
|
|
} |
|
|
to { |
|
|
opacity: 1; |
|
|
} |
|
|
} |
|
|
|
|
|
.reaction-menu.mobile-drawer { |
|
|
z-index: 1000; |
|
|
} |
|
|
|
|
|
.reaction-menu.menu-below { |
|
|
bottom: auto; |
|
|
top: 100%; |
|
|
margin-bottom: 0; |
|
|
margin-top: 0.5rem; |
|
|
} |
|
|
|
|
|
.reaction-menu::-webkit-scrollbar { |
|
|
width: 8px; |
|
|
} |
|
|
|
|
|
.reaction-menu::-webkit-scrollbar-track { |
|
|
background: var(--fog-post, #ffffff); |
|
|
border-radius: 0.5rem; |
|
|
} |
|
|
|
|
|
.reaction-menu::-webkit-scrollbar-thumb { |
|
|
background: var(--fog-border, #e5e7eb); |
|
|
border-radius: 4px; |
|
|
} |
|
|
|
|
|
.reaction-menu::-webkit-scrollbar-thumb:hover { |
|
|
background: var(--fog-accent, #64748b); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-menu::-webkit-scrollbar-track { |
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-menu::-webkit-scrollbar-thumb { |
|
|
background: var(--fog-dark-border, #374151); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-menu::-webkit-scrollbar-thumb:hover { |
|
|
background: var(--fog-dark-accent, #64748b); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-menu { |
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2); |
|
|
} |
|
|
|
|
|
.reaction-menu-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(6, 1fr); |
|
|
gap: 0.25rem; |
|
|
} |
|
|
|
|
|
.reaction-menu-item { |
|
|
position: relative; |
|
|
padding: 0.5rem; |
|
|
border: 1px solid transparent; |
|
|
border-radius: 0.25rem; |
|
|
background: transparent; |
|
|
cursor: pointer; |
|
|
transition: all 0.2s; |
|
|
font-size: 1.25rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
min-height: 2.5rem; |
|
|
} |
|
|
|
|
|
.reaction-menu-item:hover { |
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
border-color: var(--fog-border, #e5e7eb); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-menu-item:hover { |
|
|
background: var(--fog-dark-highlight, #374151); |
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
} |
|
|
|
|
|
.reaction-menu-item.active { |
|
|
background: var(--fog-accent, #64748b); |
|
|
border-color: var(--fog-accent, #64748b); |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-menu-item.active { |
|
|
background: var(--fog-dark-accent, #64748b); |
|
|
border-color: var(--fog-dark-accent, #64748b); |
|
|
} |
|
|
|
|
|
.reaction-count { |
|
|
position: absolute; |
|
|
bottom: 0.125rem; |
|
|
right: 0.125rem; |
|
|
font-size: 0.625rem; |
|
|
font-weight: 600; |
|
|
background: var(--fog-accent, #64748b); |
|
|
color: white; |
|
|
border-radius: 50%; |
|
|
width: 1rem; |
|
|
height: 1rem; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
line-height: 1; |
|
|
} |
|
|
|
|
|
:global(.dark) .reaction-count { |
|
|
background: var(--fog-dark-accent, #64748b); |
|
|
} |
|
|
|
|
|
.custom-emojis-section { |
|
|
margin-top: 0.75rem; |
|
|
padding-top: 0.75rem; |
|
|
border-top: 1px solid var(--fog-border, #e5e7eb); |
|
|
} |
|
|
|
|
|
:global(.dark) .custom-emojis-section { |
|
|
border-top-color: var(--fog-dark-border, #374151); |
|
|
} |
|
|
|
|
|
.custom-emojis-label { |
|
|
font-size: 0.75rem; |
|
|
font-weight: 600; |
|
|
color: var(--fog-text-light, #6b7280); |
|
|
margin-bottom: 0.5rem; |
|
|
text-transform: uppercase; |
|
|
letter-spacing: 0.05em; |
|
|
} |
|
|
|
|
|
:global(.dark) .custom-emojis-label { |
|
|
color: var(--fog-dark-text-light, #9ca3af); |
|
|
} |
|
|
|
|
|
.custom-emoji-img { |
|
|
width: 1.25rem; |
|
|
height: 1.25rem; |
|
|
object-fit: contain; |
|
|
display: inline-block; |
|
|
vertical-align: middle; |
|
|
} |
|
|
|
|
|
.emoji-search-container { |
|
|
margin-bottom: 0.5rem; |
|
|
padding-bottom: 0.5rem; |
|
|
padding-top: 0; |
|
|
border-bottom: 1px solid var(--fog-border, #e5e7eb); |
|
|
display: block; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
:global(.dark) .emoji-search-container { |
|
|
border-bottom-color: var(--fog-dark-border, #374151); |
|
|
} |
|
|
|
|
|
.emoji-search-input { |
|
|
width: 100%; |
|
|
padding: 0.5rem; |
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
border-radius: 0.25rem; |
|
|
background: var(--fog-post, #ffffff); |
|
|
color: var(--fog-text, #1f2937); |
|
|
font-size: 0.875rem; |
|
|
display: block; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
.emoji-search-input:focus { |
|
|
outline: none; |
|
|
border-color: var(--fog-accent, #64748b); |
|
|
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1); |
|
|
} |
|
|
|
|
|
:global(.dark) .emoji-search-input { |
|
|
background: var(--fog-dark-post, #1f2937); |
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
color: var(--fog-dark-text, #f9fafb); |
|
|
} |
|
|
|
|
|
:global(.dark) .emoji-search-input:focus { |
|
|
border-color: var(--fog-dark-accent, #64748b); |
|
|
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.2); |
|
|
} |
|
|
|
|
|
/* Adjust grid for mobile */ |
|
|
@media (max-width: 768px) { |
|
|
.reaction-menu-grid { |
|
|
grid-template-columns: repeat(8, 1fr); |
|
|
} |
|
|
|
|
|
.reaction-menu-item { |
|
|
min-height: 2.25rem; |
|
|
font-size: 1.125rem; |
|
|
} |
|
|
} |
|
|
</style>
|
|
|
|