You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

339 lines
7.6 KiB

<script lang="ts">
interface Props {
open: boolean;
title: string;
items: string[];
onSelect: (item: string) => void;
onClose: () => void;
searchPlaceholder?: string;
emptyMessage?: string;
searchQuery?: string;
onSearchChange?: (query: string) => void;
children?: import('svelte').Snippet;
}
let {
open,
title,
items,
onSelect,
onClose,
searchPlaceholder = "Search emojis...",
emptyMessage = "No emojis found",
searchQuery: externalSearchQuery,
onSearchChange,
children
}: Props = $props();
let searchInput: HTMLInputElement | null = $state(null);
let internalSearchQuery = $state('');
// Use external search query if provided, otherwise use internal
const currentSearchQuery = $derived(externalSearchQuery ?? internalSearchQuery);
function handleSearchInput(e: Event) {
const target = e.target as HTMLInputElement;
const value = target.value;
if (onSearchChange) {
onSearchChange(value);
} else if (externalSearchQuery !== undefined) {
// If external search query is provided, parent should handle updates
// For now, just use internal state
internalSearchQuery = value;
} else {
internalSearchQuery = value;
}
}
function handleItemSelect(item: string) {
onSelect(item);
onClose();
}
// Handle keyboard navigation
function handleKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape') {
onClose();
}
}
// Focus search input when drawer opens
$effect(() => {
if (open) {
setTimeout(() => {
if (searchInput) {
searchInput.focus();
}
}, 100);
} else {
// Reset search when closed
if (onSearchChange) {
onSearchChange('');
} else {
internalSearchQuery = '';
}
}
});
</script>
{#if open}
<div
class="drawer-backdrop"
onclick={onClose}
onkeydown={handleKeyDown}
role="button"
tabindex="0"
aria-label="Close {title.toLowerCase()}"
></div>
<div
class="emoji-drawer drawer-left"
onkeydown={handleKeyDown}
role="dialog"
aria-modal="true"
aria-label={title}
tabindex="-1"
>
<div class="drawer-header">
<h3 class="drawer-title">{title}</h3>
<button
onclick={onClose}
class="drawer-close"
aria-label="Close {title.toLowerCase()}"
title="Close"
>
×
</button>
</div>
<div class="emoji-search-container">
<input
bind:this={searchInput}
type="text"
placeholder={searchPlaceholder}
value={currentSearchQuery}
oninput={handleSearchInput}
class="emoji-search-input"
aria-label="Search emojis"
/>
</div>
<div class="emoji-drawer-content">
{#if children}
{@render children()}
{:else if items.length === 0}
<div class="emoji-empty">{emptyMessage}</div>
{:else}
<div class="emoji-grid">
{#each items as item}
<button
onclick={() => handleItemSelect(item)}
class="emoji-item"
title="Click to select"
>
{item}
</button>
{/each}
</div>
{/if}
</div>
</div>
{/if}
<style>
.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;
}
}
.emoji-drawer {
position: fixed;
top: 0;
left: 0;
bottom: 0;
width: min(400px, 80vw);
max-width: 400px;
background: var(--fog-post, #ffffff);
border-right: 2px solid var(--fog-border, #cbd5e1);
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2);
padding: 0;
z-index: 1000;
display: flex;
flex-direction: column;
overflow: hidden;
animation: slideInLeft 0.3s ease-out;
transform: translateX(0);
/* Override global grayscale filter for entire drawer */
filter: none !important;
}
/* Override grayscale filters for all children in the drawer */
.emoji-drawer * {
filter: none !important;
}
:global(.dark) .emoji-drawer {
background: var(--fog-dark-post, #1f2937);
border-right-color: var(--fog-dark-border, #475569);
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.5);
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .drawer-header {
border-bottom-color: var(--fog-dark-border, #374151);
}
.drawer-title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--fog-text, #1f2937);
}
:global(.dark) .drawer-title {
color: var(--fog-dark-text, #f9fafb);
}
.drawer-close {
background: transparent;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: var(--fog-text-light, #9ca3af);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s;
}
.drawer-close:hover {
background: var(--fog-highlight, #f3f4f6);
color: var(--fog-text, #1f2937);
}
:global(.dark) .drawer-close {
color: var(--fog-dark-text-light, #6b7280);
}
:global(.dark) .drawer-close:hover {
background: var(--fog-dark-highlight, #374151);
color: var(--fog-dark-text, #f9fafb);
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
.emoji-search-container {
padding: 1rem;
border-bottom: 1px solid var(--fog-border, #e5e7eb);
}
:global(.dark) .emoji-search-container {
border-bottom-color: var(--fog-dark-border, #374151);
}
.emoji-search-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-surface, #f8fafc);
color: var(--fog-text, #1f2937);
font-size: 0.875rem;
transition: all 0.2s;
}
.emoji-search-input:focus {
outline: none;
border-color: var(--fog-accent, #64748b);
background: var(--fog-post, #ffffff);
}
:global(.dark) .emoji-search-input {
background: var(--fog-dark-surface, #1e293b);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
:global(.dark) .emoji-search-input:focus {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-accent, #64748b);
}
.emoji-drawer-content {
overflow-y: auto;
overflow-x: hidden;
flex: 1;
padding: 1rem;
}
.emoji-empty {
text-align: center;
padding: 2rem;
color: var(--fog-text-light, #9ca3af);
font-size: 0.875rem;
}
:global(.dark) .emoji-empty {
color: var(--fog-dark-text-light, #6b7280);
}
.emoji-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr));
gap: 0.25rem;
}
.emoji-item {
padding: 0.5rem;
border: 1px solid transparent;
border-radius: 0.25rem;
background: transparent;
cursor: pointer;
transition: all 0.2s;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
min-height: 2.5rem;
/* Override global grayscale filter for emojis in drawer */
filter: none !important;
}
.emoji-item:hover {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-border, #e5e7eb);
}
:global(.dark) .emoji-item:hover {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-border, #374151);
}
</style>