Browse Source

moved inline styles into the styles folder

master
silberengel 7 months ago
parent
commit
fcdf6dfe62
  1. 45
      src/lib/components/Notifications.svelte
  2. 214
      src/styles/notifications.css

45
src/lib/components/Notifications.svelte

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import "../../styles/notifications.css";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { Heading, P } from "flowbite-svelte"; import { Heading, P } from "flowbite-svelte";
import type { NDKEvent } from "$lib/utils/nostrUtils"; import type { NDKEvent } from "$lib/utils/nostrUtils";
@ -147,9 +148,9 @@
} }
// ALWAYS highlight the message in blue // ALWAYS highlight the message in blue
element.classList.add('ring-2', 'ring-blue-500'); element.classList.add('message-highlight', 'ring-2', 'ring-blue-500');
setTimeout(() => { setTimeout(() => {
element.classList.remove('ring-2', 'ring-blue-500'); element.classList.remove('message-highlight', 'ring-2', 'ring-blue-500');
}, 2000); }, 2000);
} }
} }
@ -712,7 +713,7 @@
{#each ["to-me", "from-me", "public-messages"] as mode} {#each ["to-me", "from-me", "public-messages"] as mode}
{@const modeLabel = mode === "to-me" ? "To Me" : mode === "from-me" ? "From Me" : "Public Messages"} {@const modeLabel = mode === "to-me" ? "To Me" : mode === "from-me" ? "From Me" : "Public Messages"}
<button <button
class="px-3 py-1 text-sm font-medium rounded-md transition-colors {notificationMode === mode ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-700 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100'}" class="mode-toggle-button px-3 py-1 text-sm font-medium rounded-md {notificationMode === mode ? 'active' : 'inactive'}"
onclick={() => notificationMode = mode as "to-me" | "from-me" | "public-messages"} onclick={() => notificationMode = mode as "to-me" | "from-me" | "public-messages"}
> >
{modeLabel} {modeLabel}
@ -724,7 +725,7 @@
{#if loading} {#if loading}
<div class="flex items-center justify-center py-8 min-h-96"> <div class="flex items-center justify-center py-8 min-h-96">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div> <div class="notifications-loading-spinner rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
<span class="ml-2 text-gray-600 dark:text-gray-400"> <span class="ml-2 text-gray-600 dark:text-gray-400">
Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}... Loading {notificationMode === "public-messages" ? "public messages" : "notifications"}...
</span> </span>
@ -741,7 +742,7 @@
{:else} {:else}
<div class="max-h-[72rem] overflow-y-auto"> <div class="max-h-[72rem] overflow-y-auto">
{#if filteredByUser} {#if filteredByUser}
<div class="mb-4 p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-lg"> <div class="filter-indicator mb-4 p-3 rounded-lg">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="text-sm text-blue-700 dark:text-blue-300"> <span class="text-sm text-blue-700 dark:text-blue-300">
Filtered by user: {@render userBadge(filteredByUser, authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name)} Filtered by user: {@render userBadge(filteredByUser, authorProfiles.get(filteredByUser)?.displayName || authorProfiles.get(filteredByUser)?.name)}
@ -759,7 +760,7 @@
{#each filteredMessages.slice(0, 100) as message} {#each filteredMessages.slice(0, 100) as message}
{@const authorProfile = authorProfiles.get(message.pubkey)} {@const authorProfile = authorProfiles.get(message.pubkey)}
{@const isFromUser = message.pubkey === $userStore.pubkey} {@const isFromUser = message.pubkey === $userStore.pubkey}
<div class="p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md transition-all" data-event-id="{message.id}"> <div class="message-container p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm" data-event-id="{message.id}">
<div class="flex items-start gap-3 {isFromUser ? 'flex-row-reverse' : ''}"> <div class="flex items-start gap-3 {isFromUser ? 'flex-row-reverse' : ''}">
<!-- Author Profile Picture and Name --> <!-- Author Profile Picture and Name -->
<div class="flex-shrink-0 relative"> <div class="flex-shrink-0 relative">
@ -772,7 +773,7 @@
onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'} onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'}
/> />
{:else} {:else}
<div class="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"> <div class="profile-picture-fallback w-10 h-10 rounded-full flex items-center justify-center border border-gray-200 dark:border-gray-600">
<span class="text-sm font-medium text-gray-600 dark:text-gray-300"> <span class="text-sm font-medium text-gray-600 dark:text-gray-300">
{(authorProfile?.displayName || authorProfile?.name || message.pubkey.slice(0, 1)).toUpperCase()} {(authorProfile?.displayName || authorProfile?.name || message.pubkey.slice(0, 1)).toUpperCase()}
</span> </span>
@ -788,7 +789,7 @@
<div class="mt-2 flex flex-col gap-1"> <div class="mt-2 flex flex-col gap-1">
<!-- Reply button --> <!-- Reply button -->
<button <button
class="w-6 h-6 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-full flex items-center justify-center text-xs shadow-sm transition-colors" class="reply-button w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs shadow-sm transition-colors"
onclick={(e) => { onclick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -801,7 +802,7 @@
</button> </button>
<!-- Filter button --> <!-- Filter button -->
<button <button
class="w-6 h-6 bg-gray-400 hover:bg-gray-500 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-300 rounded-full flex items-center justify-center text-xs shadow-sm transition-colors {filteredByUser === message.pubkey ? 'ring-2 ring-gray-300 dark:ring-gray-400 bg-gray-500 dark:bg-gray-500' : ''}" class="filter-button w-6 h-6 bg-gray-400 hover:bg-gray-500 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-300 rounded-full flex items-center justify-center text-xs shadow-sm {filteredByUser === message.pubkey ? 'filter-button-active ring-2 ring-gray-300 dark:ring-gray-400' : ''}"
onclick={() => filterByUser(message.pubkey)} onclick={() => filterByUser(message.pubkey)}
title="Filter by this user" title="Filter by this user"
aria-label="Filter by this user" aria-label="Filter by this user"
@ -815,7 +816,7 @@
</div> </div>
<!-- Message Content --> <!-- Message Content -->
<div class="flex-1 min-w-0 {isFromUser ? 'text-right' : ''}"> <div class="message-content flex-1 {isFromUser ? 'text-right' : ''}">
<div class="flex items-center gap-2 mb-2 {isFromUser ? 'justify-end' : ''}"> <div class="flex items-center gap-2 mb-2 {isFromUser ? 'justify-end' : ''}">
<span class="text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-900 px-2 py-1 rounded"> <span class="text-xs font-medium text-primary-600 dark:text-primary-400 bg-primary-100 dark:bg-primary-900 px-2 py-1 rounded">
{isFromUser ? 'Your Message' : 'Public Message'} {isFromUser ? 'Your Message' : 'Public Message'}
@ -877,7 +878,7 @@
<div class="max-h-[72rem] overflow-y-auto space-y-4"> <div class="max-h-[72rem] overflow-y-auto space-y-4">
{#each notifications.slice(0, 100) as notification} {#each notifications.slice(0, 100) as notification}
{@const authorProfile = authorProfiles.get(notification.pubkey)} {@const authorProfile = authorProfiles.get(notification.pubkey)}
<div class="p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm hover:shadow-md transition-all"> <div class="message-container p-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-sm">
<div class="flex items-start gap-3"> <div class="flex items-start gap-3">
<!-- Author Profile Picture and Name --> <!-- Author Profile Picture and Name -->
<div class="flex-shrink-0"> <div class="flex-shrink-0">
@ -890,7 +891,7 @@
onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'} onerror={(e) => (e.target as HTMLImageElement).style.display = 'none'}
/> />
{:else} {:else}
<div class="w-10 h-10 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center border border-gray-200 dark:border-gray-600"> <div class="profile-picture-fallback w-10 h-10 rounded-full flex items-center justify-center border border-gray-200 dark:border-gray-600">
<span class="text-sm font-medium text-gray-600 dark:text-gray-300"> <span class="text-sm font-medium text-gray-600 dark:text-gray-300">
{(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()} {(authorProfile?.displayName || authorProfile?.name || notification.pubkey.slice(0, 1)).toUpperCase()}
</span> </span>
@ -950,7 +951,7 @@
<!-- New Message Modal --> <!-- New Message Modal -->
<Modal bind:open={showNewMessageModal} size="lg" class="w-full"> <Modal bind:open={showNewMessageModal} size="lg" class="w-full">
<div class="p-6"> <div class="modal-content p-6">
<div class="mb-4"> <div class="mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100"> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
{replyToMessage ? 'Reply to Message' : 'New Public Message'} {replyToMessage ? 'Reply to Message' : 'New Public Message'}
@ -959,7 +960,7 @@
<!-- Quoted Content Display --> <!-- Quoted Content Display -->
{#if quotedContent} {#if quotedContent}
<div class="mb-4 p-3 bg-gray-100 dark:bg-gray-800 border-l-4 border-gray-400 dark:border-gray-500 rounded-r-lg"> <div class="quoted-content mb-4 p-3 rounded-r-lg">
<div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Replying to:</div> <div class="text-sm text-gray-600 dark:text-gray-400 mb-1">Replying to:</div>
<div class="text-sm text-gray-800 dark:text-gray-200"> <div class="text-sm text-gray-800 dark:text-gray-200">
{#await parseContent(quotedContent) then parsedContent} {#await parseContent(quotedContent) then parsedContent}
@ -990,7 +991,7 @@
</div> </div>
{#if selectedRecipients.length === 0} {#if selectedRecipients.length === 0}
<div class="p-3 bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700 rounded-lg"> <div class="recipient-selection p-3 rounded-lg">
<p class="text-sm text-yellow-700 dark:text-yellow-300"> <p class="text-sm text-yellow-700 dark:text-yellow-300">
No recipients selected. Click "Edit Recipients" to add recipients. No recipients selected. Click "Edit Recipients" to add recipients.
</p> </p>
@ -1041,7 +1042,7 @@
id="new-message-content" id="new-message-content"
bind:value={newMessageContent} bind:value={newMessageContent}
placeholder="Type your message here..." placeholder="Type your message here..."
class="w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" class="message-textarea w-full p-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 resize-none"
rows="6" rows="6"
onkeydown={(e) => { onkeydown={(e) => {
if (e.key === 'Enter' && !e.shiftKey && !isComposingMessage && selectedRecipients.length > 0 && newMessageContent.trim()) { if (e.key === 'Enter' && !e.shiftKey && !isComposingMessage && selectedRecipients.length > 0 && newMessageContent.trim()) {
@ -1065,7 +1066,7 @@
color="primary" color="primary"
onclick={sendNewMessage} onclick={sendNewMessage}
disabled={isComposingMessage || selectedRecipients.length === 0 || !newMessageContent.trim()} disabled={isComposingMessage || selectedRecipients.length === 0 || !newMessageContent.trim()}
class="flex items-center gap-2" class="flex items-center gap-2 {isComposingMessage || selectedRecipients.length === 0 || !newMessageContent.trim() ? 'button-disabled' : ''}"
> >
{#if isComposingMessage} {#if isComposingMessage}
<div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
@ -1078,7 +1079,7 @@
<!-- Recipient Selection Modal --> <!-- Recipient Selection Modal -->
<Modal bind:open={showRecipientModal} size="lg" class="w-full"> <Modal bind:open={showRecipientModal} size="lg" class="w-full">
<div class="p-6"> <div class="modal-content p-6">
<div class="mb-4"> <div class="mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Select Recipients</h3> <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Select Recipients</h3>
</div> </div>
@ -1090,7 +1091,7 @@
placeholder="Search display name, name, NIP-05, or npub..." placeholder="Search display name, name, NIP-05, or npub..."
bind:value={recipientSearch} bind:value={recipientSearch}
bind:this={recipientSearchInput} bind:this={recipientSearchInput}
class="w-full rounded-lg border border-gray-300 bg-gray-50 text-gray-900 text-sm focus:border-primary-500 focus:ring-primary-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-primary-500 dark:focus:ring-primary-500 p-2.5 {recipientLoading ? 'pr-10' : ''}" class="search-input w-full rounded-lg border border-gray-300 bg-gray-50 text-gray-900 text-sm dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 p-2.5 {recipientLoading ? 'pr-10' : ''}"
/> />
{#if recipientLoading} {#if recipientLoading}
<div class="absolute inset-y-0 right-0 flex items-center pr-3"> <div class="absolute inset-y-0 right-0 flex items-center pr-3">
@ -1100,14 +1101,14 @@
</div> </div>
{#if recipientResults.length > 0} {#if recipientResults.length > 0}
<div class="max-h-64 overflow-y-auto"> <div class="recipient-results">
<ul class="space-y-2"> <ul class="space-y-2">
{#each recipientResults as profile} {#each recipientResults as profile}
{@const isAlreadySelected = selectedRecipients.some(r => r.pubkey === profile.pubkey)} {@const isAlreadySelected = selectedRecipients.some(r => r.pubkey === profile.pubkey)}
<button <button
onclick={() => selectRecipient(profile)} onclick={() => selectRecipient(profile)}
disabled={isAlreadySelected} disabled={isAlreadySelected}
class="w-full flex items-center gap-3 p-3 text-left bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors {isAlreadySelected ? 'opacity-50 cursor-not-allowed' : ''}" class="recipient-selection-button w-full flex items-center gap-3 p-3 text-left bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 {isAlreadySelected ? 'opacity-50 cursor-not-allowed' : ''}"
> >
{#if profile.picture} {#if profile.picture}
<img <img
@ -1150,7 +1151,7 @@
</div> </div>
{#if recipientCommunityStatus[profile.pubkey || ""]} {#if recipientCommunityStatus[profile.pubkey || ""]}
<div <div
class="flex-shrink-0 w-4 h-4 bg-yellow-100 dark:bg-yellow-900 rounded-full flex items-center justify-center" class="community-status-indicator flex-shrink-0 w-4 h-4 rounded-full flex items-center justify-center"
title="Has posted to the community" title="Has posted to the community"
> >
<svg <svg

214
src/styles/notifications.css

@ -0,0 +1,214 @@
/* Notifications Component Styles */
/* Loading spinner animation */
.notifications-loading-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Message highlighting for jump-to functionality */
.message-highlight {
transition: all 0.2s ease-in-out;
}
.message-highlight.ring-2 {
box-shadow: 0 0 0 2px rgb(59 130 246);
}
/* Modal content styling */
.modal-content {
max-height: 80vh;
overflow-y: auto;
}
/* Recipient search results */
.recipient-results {
max-height: 16rem;
overflow-y: auto;
}
/* Message content area */
.message-content {
min-width: 0;
word-wrap: break-word;
}
/* Profile picture fallback */
.profile-picture-fallback {
background: linear-gradient(135deg, #e5e7eb 0%, #d1d5db 100%);
}
.dark .profile-picture-fallback {
background: linear-gradient(135deg, #4b5563 0%, #374151 100%);
}
/* Filter button states */
.filter-button-active {
background-color: rgb(107 114 128);
color: rgb(243 244 246);
}
.dark .filter-button-active {
background-color: rgb(107 114 128);
color: rgb(243 244 246);
}
/* Reply button hover states */
.reply-button:hover {
background-color: rgb(37 99 235);
}
.dark .reply-button:hover {
background-color: rgb(29 78 216);
}
/* Community status indicator */
.community-status-indicator {
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
}
.dark .community-status-indicator {
background: linear-gradient(135deg, #78350f 0%, #92400e 100%);
}
/* Quoted content styling */
.quoted-content {
border-left: 4px solid rgb(156 163 175);
background-color: rgb(249 250 251);
}
.dark .quoted-content {
border-left-color: rgb(107 114 128);
background-color: rgb(31 41 55);
}
/* Recipient selection styling */
.recipient-selection {
background-color: rgb(243 244 246);
border: 1px solid rgb(229 231 235);
}
.dark .recipient-selection {
background-color: rgb(55 65 81);
border-color: rgb(75 85 99);
}
/* Message container hover effects */
.message-container {
transition: all 0.2s ease-in-out;
}
.message-container:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
.dark .message-container:hover {
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.2);
}
/* Filter indicator styling */
.filter-indicator {
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
border: 1px solid rgb(147 197 253);
}
.dark .filter-indicator {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%);
border-color: rgb(59 130 246);
}
/* Textarea focus states */
.message-textarea:focus {
outline: none;
border-color: transparent;
box-shadow: 0 0 0 2px rgb(59 130 246);
}
/* Button disabled states */
.button-disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Search input focus states */
.search-input:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.2);
}
.dark .search-input:focus {
border-color: rgb(59 130 246);
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.3);
}
/* Transition utilities */
.transition-colors {
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, text-decoration-color 0.15s ease-in-out, fill 0.15s ease-in-out, stroke 0.15s ease-in-out;
}
.transition-all {
transition: all 0.15s ease-in-out;
}
/* Mode toggle button states */
.mode-toggle-button {
transition: all 0.15s ease-in-out;
}
.mode-toggle-button.active {
background-color: rgb(255 255 255);
color: rgb(17 24 39);
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.dark .mode-toggle-button.active {
background-color: rgb(31 41 55);
color: rgb(243 244 246);
}
.mode-toggle-button.inactive {
color: rgb(55 65 81);
}
.dark .mode-toggle-button.inactive {
color: rgb(156 163 175);
}
.mode-toggle-button.inactive:hover {
color: rgb(17 24 39);
}
.dark .mode-toggle-button.inactive:hover {
color: rgb(243 244 246);
}
/* Filter button transitions */
.filter-button {
transition: all 0.15s ease-in-out;
}
/* Recipient selection button transitions */
.recipient-selection-button {
transition: all 0.15s ease-in-out;
}
.recipient-selection-button:hover {
background-color: rgb(249 250 251);
}
.dark .recipient-selection-button:hover {
background-color: rgb(55 65 81);
}
.recipient-selection-button:focus {
outline: none;
box-shadow: 0 0 0 2px rgb(59 130 246);
}
Loading…
Cancel
Save