Browse Source

change to svg icons

master
Silberengel 1 month ago
parent
commit
650044c1bd
  1. 10
      package-lock.json
  2. 1
      package.json
  3. 4
      public/healthz.json
  4. 73
      scripts/extract-icons.js
  5. 56
      src/lib/components/EventMenu.svelte
  6. 20
      src/lib/components/content/VoteCount.svelte
  7. 16
      src/lib/components/layout/Header.svelte
  8. 16
      src/lib/components/layout/SearchBox.svelte
  9. 21
      src/lib/components/modals/EventJsonModal.svelte
  10. 15
      src/lib/components/preferences/ThemeToggle.svelte
  11. 9
      src/lib/components/profile/ProfileMenu.svelte
  12. 67
      src/lib/components/ui/Icon.svelte
  13. 90
      src/lib/components/ui/IconButton.svelte
  14. 54
      src/lib/components/write/AdvancedEditor.svelte
  15. 41
      src/lib/components/write/CreateEventForm.svelte
  16. 11
      src/lib/modules/discussions/DiscussionCard.svelte
  17. 8
      src/lib/modules/feed/ZapReceiptReply.svelte
  18. 6
      src/lib/modules/profiles/PaymentAddresses.svelte
  19. 14
      src/lib/modules/reactions/FeedReactionButtons.svelte
  20. 6
      src/lib/modules/zaps/ZapButton.svelte
  21. 4
      src/lib/modules/zaps/ZapReceipt.svelte
  22. 46
      src/routes/login/+page.svelte
  23. 16
      src/routes/settings/+page.svelte
  24. 1
      static/icons/arrow-left.svg
  25. 1
      static/icons/check.svg
  26. 1
      static/icons/chevron-down.svg
  27. 1
      static/icons/chevron-up.svg
  28. 1
      static/icons/code.svg
  29. 1
      static/icons/copy.svg
  30. 1
      static/icons/edit.svg
  31. 1
      static/icons/eye.svg
  32. 1
      static/icons/file-text.svg
  33. 1
      static/icons/heart.svg
  34. 1
      static/icons/image.svg
  35. 1
      static/icons/key.svg
  36. 1
      static/icons/link.svg
  37. 1
      static/icons/log-in.svg
  38. 1
      static/icons/log-out.svg
  39. 1
      static/icons/message-square.svg
  40. 1
      static/icons/moon.svg
  41. 1
      static/icons/plus.svg
  42. 1
      static/icons/radio.svg
  43. 1
      static/icons/search.svg
  44. 1
      static/icons/send.svg
  45. 1
      static/icons/settings.svg
  46. 1
      static/icons/share.svg
  47. 1
      static/icons/smile.svg
  48. 1
      static/icons/sun.svg
  49. 1
      static/icons/trash.svg
  50. 1
      static/icons/upload.svg
  51. 1
      static/icons/user.svg
  52. 1
      static/icons/video.svg
  53. 1
      static/icons/x.svg
  54. 1
      static/icons/zap.svg

10
package-lock.json generated

@ -28,6 +28,7 @@ @@ -28,6 +28,7 @@
"emoji-picker-element": "^1.28.1",
"highlight.js": "^11.11.1",
"idb": "^8.0.0",
"lucide-svelte": "^0.563.0",
"marked": "^11.1.1",
"nostr-tools": "^2.22.1",
"svelte": "^5.0.0",
@ -3867,6 +3868,15 @@ @@ -3867,6 +3868,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/lucide-svelte": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.563.0.tgz",
"integrity": "sha512-pjZKw7TpQcamfQrx7YdbOHgmrcNeKiGGMD0tKZQaVktwSsbqw28CsKc2Q97ttwjytiCWkJyOa8ij2Q+Og0nPfQ==",
"license": "ISC",
"peerDependencies": {
"svelte": "^3 || ^4 || ^5.0.0-next.42"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",

1
package.json

@ -42,6 +42,7 @@ @@ -42,6 +42,7 @@
"emoji-picker-element": "^1.28.1",
"highlight.js": "^11.11.1",
"idb": "^8.0.0",
"lucide-svelte": "^0.563.0",
"marked": "^11.1.1",
"nostr-tools": "^2.22.1",
"svelte": "^5.0.0",

4
public/healthz.json

@ -2,7 +2,7 @@ @@ -2,7 +2,7 @@
"status": "ok",
"service": "aitherboard",
"version": "0.2.0",
"buildTime": "2026-02-06T17:29:48.880Z",
"buildTime": "2026-02-06T19:28:59.767Z",
"gitCommit": "unknown",
"timestamp": 1770398988880
"timestamp": 1770406139768
}

73
scripts/extract-icons.js

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
import { writeFileSync, mkdirSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Icons we need to extract
const icons = [
'zap', // ⚡
'heart', // ❤
'chevron-up', // ⬆
'chevron-down', // ⬇
'sun', // ☀
'moon', // 🌙
'user', // 👤
'plus' // ➕
];
const staticDir = join(__dirname, '..', 'static', 'icons');
mkdirSync(staticDir, { recursive: true });
// Import lucide-svelte to get the icon paths
try {
const lucide = await import('lucide-svelte');
for (const iconName of icons) {
// Convert kebab-case to PascalCase
const componentName = iconName
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join('');
// Get the icon component
const IconComponent = lucide[componentName];
if (!IconComponent) {
console.warn(`Icon ${iconName} (${componentName}) not found`);
continue;
}
// Extract SVG path from the component
// Lucide icons are SVG components, we need to render them to get the path
// For now, let's use the known SVG paths from lucide
const svgPaths = {
'zap': '<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>',
'heart': '<path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z"/>',
'chevron-up': '<path d="m18 15-6-6-6 6"/>',
'chevron-down': '<path d="m6 9 6 6 6-6"/>',
'sun': '<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>',
'moon': '<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/>',
'user': '<path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
'plus': '<path d="M5 12h14"/><path d="M12 5v14"/>'
};
const path = svgPaths[iconName];
if (!path) {
console.warn(`No SVG path found for ${iconName}`);
continue;
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${path}</svg>`;
const filePath = join(staticDir, `${iconName}.svg`);
writeFileSync(filePath, svg, 'utf-8');
console.log(`Extracted ${iconName}.svg`);
}
console.log('All icons extracted successfully!');
} catch (error) {
console.error('Error extracting icons:', error);
process.exit(1);
}

56
src/lib/components/EventMenu.svelte

@ -19,6 +19,7 @@ @@ -19,6 +19,7 @@
import RelatedEventsModal from './modals/RelatedEventsModal.svelte';
import { KIND } from '../types/kind-lookup.js';
import { goto } from '$app/navigation';
import Icon from './ui/Icon.svelte';
interface Props {
event: NostrEvent;
@ -384,30 +385,36 @@ @@ -384,30 +385,36 @@
style="top: {menuPosition.top}px; right: {menuPosition.right}px;"
>
<button class="menu-item" onclick={copyUserId}>
Copy user ID
<Icon name="copy" size={16} />
<span>Copy user ID</span>
{#if copied === 'npub'}
<span class="copied-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={copyEventId}>
Copy event ID
<Icon name="copy" size={16} />
<span>Copy event ID</span>
{#if copied === 'nevent'}
<span class="copied-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={viewJson}>
View JSON
<Icon name="code" size={16} />
<span>View JSON</span>
</button>
{#if isLoggedIn}
<button class="menu-item" onclick={viewRelatedEvents}>
View your related events
<Icon name="search" size={16} />
<span>View your related events</span>
</button>
{/if}
<button class="menu-item" onclick={broadcastEvent} disabled={broadcasting}>
{broadcasting ? 'Broadcasting...' : 'Broadcast event'}
<Icon name="radio" size={16} />
<span>{broadcasting ? 'Broadcasting...' : 'Broadcast event'}</span>
</button>
<button class="menu-item" onclick={shareWithaitherboard}>
Share with aitherboard
<Icon name="share" size={16} />
<span>Share with aitherboard</span>
{#if copied === 'share'}
<span class="copied-indicator"></span>
{/if}
@ -416,25 +423,29 @@ @@ -416,25 +423,29 @@
{#if isLoggedIn && onReply}
<div class="menu-divider"></div>
<button class="menu-item" onclick={() => { onReply(); closeMenu(); }}>
Reply
<Icon name="message-square" size={16} />
<span>Reply</span>
</button>
{/if}
{#if isLoggedIn && showContentActions}
<div class="menu-divider"></div>
<button class="menu-item" onclick={pinNote} class:active={pinnedState}>
Pin note
<Icon name="plus" size={16} />
<span>Pin note</span>
{#if pinnedState}
<span class="action-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={bookmarkNote} class:active={bookmarkedState}>
Bookmark note
<Icon name="plus" size={16} />
<span>Bookmark note</span>
{#if bookmarkedState}
<span class="action-indicator"></span>
{/if}
</button>
<button class="menu-item" onclick={highlightNote} class:active={highlightedState}>
Highlight note
<Icon name="edit" size={16} />
<span>Highlight note</span>
{#if highlightedState}
<span class="action-indicator"></span>
{/if}
@ -444,7 +455,8 @@ @@ -444,7 +455,8 @@
{#if isLoggedIn && isOwnEvent}
<div class="menu-divider"></div>
<button class="menu-item menu-item-danger" onclick={confirmDelete} disabled={deleting}>
{deleting ? 'Deleting...' : 'Delete event'}
<Icon name="trash" size={16} />
<span>{deleting ? 'Deleting...' : 'Delete event'}</span>
</button>
{/if}
</div>
@ -477,9 +489,13 @@ @@ -477,9 +489,13 @@
<h3 id="delete-dialog-title">Delete Event?</h3>
<p>Are you sure you want to delete this event? This action cannot be undone.</p>
<div class="delete-confirm-buttons">
<button class="btn-cancel" onclick={() => deleteConfirmOpen = false}>Cancel</button>
<button class="btn-delete" onclick={deleteEvent} disabled={deleting}>
{deleting ? 'Deleting...' : 'Delete'}
<button class="btn-cancel flex items-center gap-2" onclick={() => deleteConfirmOpen = false}>
<Icon name="x" size={16} />
<span>Cancel</span>
</button>
<button class="btn-delete flex items-center gap-2" onclick={deleteEvent} disabled={deleting}>
<Icon name="trash" size={16} />
<span>{deleting ? 'Deleting...' : 'Delete'}</span>
</button>
</div>
</div>
@ -564,6 +580,15 @@ @@ -564,6 +580,15 @@
font-size: 0.875rem;
color: var(--fog-text, #1f2937);
transition: background-color 0.2s;
gap: 0.5rem;
}
.menu-item :global(.icon) {
flex-shrink: 0;
}
.menu-item span {
flex: 1;
}
.menu-item:hover:not(:disabled) {
@ -697,6 +722,9 @@ @@ -697,6 +722,9 @@
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-cancel {

20
src/lib/components/content/VoteCount.svelte

@ -1,4 +1,6 @@ @@ -1,4 +1,6 @@
<script lang="ts">
import Icon from '../ui/Icon.svelte';
interface Props {
upvotes: number;
downvotes: number;
@ -31,8 +33,14 @@ @@ -31,8 +33,14 @@
<span class="vote-counts flex items-center gap-2 {sizeClass}" class:calculating={!votesCalculated}>
{#if !votesCalculated}
<span class="flex items-center gap-1 text-fog-text-light dark:text-fog-dark-text-light opacity-50">
<span class="upvotes"> 0</span>
<span class="downvotes"> 0</span>
<span class="upvotes flex items-center gap-1">
<Icon name="chevron-up" size={14} />
<span>0</span>
</span>
<span class="downvotes flex items-center gap-1">
<Icon name="chevron-down" size={14} />
<span>0</span>
</span>
<span class="calculating-label text-xs ml-1">Calculating votes</span>
</span>
{:else}
@ -46,7 +54,8 @@ @@ -46,7 +54,8 @@
title={isLoggedIn && onVote ? "Upvote" : !isLoggedIn ? "Login to vote" : ""}
aria-label="Upvote"
>
{upvotes}
<Icon name="chevron-up" size={14} />
<span>{upvotes}</span>
</button>
<button
type="button"
@ -57,7 +66,8 @@ @@ -57,7 +66,8 @@
title={isLoggedIn && onVote ? "Downvote" : !isLoggedIn ? "Login to vote" : ""}
aria-label="Downvote"
>
{downvotes}
<Icon name="chevron-down" size={14} />
<span>{downvotes}</span>
</button>
</span>
{/if}
@ -85,7 +95,6 @@ @@ -85,7 +95,6 @@
align-items: center;
gap: 0.25rem;
border-radius: 0.25rem;
filter: grayscale(100%);
opacity: 0.7;
}
@ -109,7 +118,6 @@ @@ -109,7 +118,6 @@
}
.vote-emoji.active {
filter: none;
opacity: 1;
background: var(--fog-accent, #64748b);
color: var(--fog-text, #ffffff);

16
src/lib/components/layout/Header.svelte

@ -3,6 +3,7 @@ @@ -3,6 +3,7 @@
import ProfileBadge from '../layout/ProfileBadge.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import Icon from '../ui/Icon.svelte';
let currentSession = $state<UserSession | null>(sessionManager.session.value);
let isLoggedIn = $derived(currentSession !== null);
@ -65,11 +66,20 @@ @@ -65,11 +66,20 @@
<a href="/repos" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Repos</a>
<a href="/bookmarks" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Bookmarks</a>
<a href="/cache" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Cache</a>
<a href="/settings" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Settings</a>
<a href="/settings" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap flex items-center gap-1">
<Icon name="settings" size={14} />
<span>/Settings</span>
</a>
{#if isLoggedIn && currentPubkey}
<a href="/logout" onclick={(e) => { e.preventDefault(); handleLogout(); }} class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Logout</a>
<a href="/logout" onclick={(e) => { e.preventDefault(); handleLogout(); }} class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap flex items-center gap-1">
<Icon name="log-out" size={14} />
<span>/Logout</span>
</a>
{:else}
<a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap">/Login</a>
<a href="/login" class="text-fog-text dark:text-fog-dark-text hover:text-fog-accent dark:hover:text-fog-dark-accent transition-colors whitespace-nowrap flex items-center gap-1">
<Icon name="log-in" size={14} />
<span>/Login</span>
</a>
{/if}
</div>
<div class="flex flex-wrap gap-1.5 sm:gap-2 md:gap-4 items-center min-w-0 flex-shrink-0 nav-links">

16
src/lib/components/layout/SearchBox.svelte

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<script lang="ts">
import { nostrClient } from '../../services/nostr/nostr-client.js';
import Icon from '../ui/Icon.svelte';
import { relayManager } from '../../services/nostr/relay-manager.js';
import { getEvent, getEventsByKind, getEventsByPubkey } from '../../services/cache/event-cache.js';
import { cacheEvent } from '../../services/cache/event-cache.js';
@ -230,6 +231,7 @@ @@ -230,6 +231,7 @@
<div class="search-box-container">
<div class="search-input-wrapper">
<Icon name="search" size={18} class="search-icon" />
<input
bind:this={searchInput}
type="text"
@ -291,9 +293,21 @@ @@ -291,9 +293,21 @@
align-items: center;
}
.search-icon {
position: absolute;
left: 0.75rem;
color: var(--fog-text-light, #9ca3af);
pointer-events: none;
z-index: 1;
}
:global(.dark) .search-icon {
color: var(--fog-dark-text-light, #6b7280);
}
.search-input {
width: 100%;
padding: 0.75rem 1rem;
padding: 0.75rem 1rem 0.75rem 2.5rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);

21
src/lib/components/modals/EventJsonModal.svelte

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<script lang="ts">
import type { NostrEvent } from '../../types/nostr.js';
import Icon from '../ui/Icon.svelte';
interface Props {
open?: boolean;
@ -58,7 +59,9 @@ @@ -58,7 +59,9 @@
<div class="modal-content">
<div class="modal-header">
<h2>Event JSON</h2>
<button onclick={close} class="close-button">×</button>
<button onclick={close} class="close-button" aria-label="Close">
<Icon name="x" size={20} />
</button>
</div>
<div class="modal-body">
@ -71,10 +74,14 @@ @@ -71,10 +74,14 @@
</div>
<div class="modal-footer">
<button onclick={copyJson} class="copy-button">
{copied ? 'Copied!' : 'Copy'}
<button onclick={copyJson} class="copy-button flex items-center gap-2">
<Icon name="copy" size={16} />
<span>{copied ? 'Copied!' : 'Copy'}</span>
</button>
<button onclick={close} class="flex items-center gap-2">
<Icon name="x" size={16} />
<span>Close</span>
</button>
<button onclick={close}>Close</button>
</div>
</div>
</div>
@ -144,6 +151,9 @@ @@ -144,6 +151,9 @@
width: 2rem;
height: 2rem;
color: var(--fog-text, #1f2937);
display: inline-flex;
align-items: center;
justify-content: center;
}
:global(.dark) .close-button {
@ -194,6 +204,9 @@ @@ -194,6 +204,9 @@
cursor: pointer;
transition: background-color 0.2s;
font-size: 0.875rem;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.copy-button {

15
src/lib/components/preferences/ThemeToggle.svelte

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import Icon from '../ui/Icon.svelte';
let isDark = $state(false);
@ -29,19 +30,9 @@ @@ -29,19 +30,9 @@
<button
onclick={toggleTheme}
class="px-3 py-1 rounded border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors"
class="px-3 py-1 rounded border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors flex items-center justify-center"
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
title={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
<span class="emoji emoji-grayscale">{#if isDark}{:else}🌙{/if}</span>
<Icon name={isDark ? 'sun' : 'moon'} size={16} />
</button>
<style>
.emoji-grayscale {
filter: grayscale(100%);
}
button:hover .emoji-grayscale {
filter: grayscale(80%);
}
</style>

9
src/lib/components/profile/ProfileMenu.svelte

@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
import { KIND } from '../../types/kind-lookup.js';
import type { NostrEvent } from '../../types/nostr.js';
import { toggleMute, toggleFollow, isMuted, isFollowed } from '../../services/user-actions.js';
import Icon from '../ui/Icon.svelte';
interface Props {
pubkey: string;
@ -362,7 +363,13 @@ @@ -362,7 +363,13 @@
role="menuitem"
disabled={following}
>
<span class="menu-item-icon">{followed ? '👤' : '➕'}</span>
<span class="menu-item-icon">
{#if followed}
<Icon name="user" size={16} />
{:else}
<Icon name="plus" size={16} />
{/if}
</span>
<span class="menu-item-text">{followed ? 'Unfollow this user' : 'Follow this user'}</span>
{#if followed}
<span class="menu-item-check"></span>

67
src/lib/components/ui/Icon.svelte

@ -0,0 +1,67 @@ @@ -0,0 +1,67 @@
<script lang="ts">
import { onMount } from 'svelte';
interface Props {
name: string;
size?: number | string;
class?: string;
}
let { name, size = 16, class: className = '' }: Props = $props();
let svgContent = $state<string>('');
const iconPath = $derived(`/icons/${name}.svg`);
const sizeValue = $derived(typeof size === 'number' ? `${size}px` : size);
onMount(async () => {
try {
const path = iconPath;
const response = await fetch(path);
if (response.ok) {
const text = await response.text();
svgContent = text;
}
} catch (error) {
console.error(`Failed to load icon: ${iconPath}`, error);
}
});
</script>
{#if svgContent}
<div
class="icon-wrapper {className}"
style="width: {sizeValue}; height: {sizeValue}; color: currentColor;"
>
{@html svgContent}
</div>
{:else}
<div
class="icon-placeholder {className}"
style="width: {sizeValue}; height: {sizeValue};"
aria-label={name}
></div>
{/if}
<style>
.icon-wrapper {
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
}
.icon-wrapper :global(svg) {
width: 100%;
height: 100%;
display: block;
}
.icon-placeholder {
display: inline-block;
vertical-align: middle;
flex-shrink: 0;
background: currentColor;
opacity: 0.2;
border-radius: 2px;
}
</style>

90
src/lib/components/ui/IconButton.svelte

@ -0,0 +1,90 @@ @@ -0,0 +1,90 @@
<script lang="ts">
import Icon from './Icon.svelte';
interface Props {
icon: string;
label: string;
size?: number | string;
class?: string;
active?: boolean;
disabled?: boolean;
onclick?: () => void;
}
let {
icon,
label,
size = 16,
class: className = '',
active = false,
disabled = false,
onclick
}: Props = $props();
</script>
<button
class="icon-button {className}"
class:active={active}
class:disabled={disabled}
{onclick}
{disabled}
aria-label={label}
title={label}
>
<Icon {name} {size} />
</button>
<style>
.icon-button {
display: inline-flex;
align-items: center;
justify-content: center;
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;
line-height: 1.5;
gap: 0.375rem;
}
:global(.dark) .icon-button {
background: var(--fog-dark-post, #1f2937);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f9fafb);
}
.icon-button:hover:not(.disabled) {
background: var(--fog-highlight, #f3f4f6);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .icon-button:hover:not(.disabled) {
background: var(--fog-dark-highlight, #374151);
}
.icon-button.active {
background: var(--fog-accent, #64748b);
color: var(--fog-text, #ffffff);
border-color: var(--fog-accent, #64748b);
}
:global(.dark) .icon-button.active {
background: var(--fog-dark-accent, #64748b);
color: var(--fog-dark-text, #ffffff);
border-color: var(--fog-dark-accent, #64748b);
}
.icon-button.disabled {
cursor: not-allowed;
opacity: 0.5;
}
.icon-button:focus-visible {
outline: 2px solid var(--fog-accent, #64748b);
outline-offset: 2px;
}
</style>

54
src/lib/components/write/AdvancedEditor.svelte

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
import MarkdownRenderer from '../content/MarkdownRenderer.svelte';
import MediaAttachments from '../content/MediaAttachments.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import Icon from '../ui/Icon.svelte';
interface Props {
value: string;
@ -760,7 +761,7 @@ @@ -760,7 +761,7 @@
onclick={handleCancel}
aria-label="Close editor"
>
×
<Icon name="x" size={20} />
</button>
</div>
@ -871,7 +872,7 @@ @@ -871,7 +872,7 @@
title="Link"
aria-label="Link"
>
🔗
<Icon name="link" size={16} />
</button>
<button
type="button"
@ -880,7 +881,7 @@ @@ -880,7 +881,7 @@
title="Image"
aria-label="Image"
>
🖼
<Icon name="image" size={16} />
</button>
<button
type="button"
@ -939,7 +940,7 @@ @@ -939,7 +940,7 @@
title="Insert Nostr identifier (npub, nevent, etc.)"
aria-label="Insert Nostr identifier"
>
🔑
<Icon name="key" size={16} />
</button>
<button
type="button"
@ -951,7 +952,7 @@ @@ -951,7 +952,7 @@
title="Insert GIF"
aria-label="Insert GIF"
>
GIF
<Icon name="video" size={16} />
</button>
<button
type="button"
@ -963,7 +964,7 @@ @@ -963,7 +964,7 @@
title="Insert emoji"
aria-label="Insert emoji"
>
😀
<Icon name="smile" size={16} />
</button>
<input
type="file"
@ -981,7 +982,7 @@ @@ -981,7 +982,7 @@
title="Upload file (image, video, or audio)"
aria-label="Upload file"
>
📤
<Icon name="upload" size={16} />
</label>
</div>
@ -996,7 +997,7 @@ @@ -996,7 +997,7 @@
title="Preview"
aria-label="Preview"
>
👁
<Icon name="eye" size={16} />
</button>
<button
type="button"
@ -1005,7 +1006,7 @@ @@ -1005,7 +1006,7 @@
title="View JSON"
aria-label="View JSON"
>
{'{ }'}
<Icon name="code" size={16} />
</button>
</div>
</div>
@ -1014,10 +1015,12 @@ @@ -1014,10 +1015,12 @@
<div class="editor-footer">
<button class="cancel-button" onclick={handleCancel}>
Cancel
<Icon name="x" size={16} />
<span>Cancel</span>
</button>
<button class="save-button" onclick={handleSave}>
Save & Close
<Icon name="check" size={16} />
<span>Save & Close</span>
</button>
</div>
</div>
@ -1049,7 +1052,9 @@ @@ -1049,7 +1052,9 @@
>
<div class="modal-header">
<h2>Preview</h2>
<button onclick={() => showPreviewModal = false} class="close-button">×</button>
<button onclick={() => showPreviewModal = false} class="close-button" aria-label="Close">
<Icon name="x" size={20} />
</button>
</div>
<div class="modal-body preview-body">
{#if previewEvent && previewContent}
@ -1062,7 +1067,10 @@ @@ -1062,7 +1067,10 @@
{/if}
</div>
<div class="modal-footer">
<button onclick={() => showPreviewModal = false}>Close</button>
<button onclick={() => showPreviewModal = false} class="flex items-center gap-2">
<Icon name="x" size={16} />
<span>Close</span>
</button>
</div>
</div>
</div>
@ -1091,7 +1099,9 @@ @@ -1091,7 +1099,9 @@
>
<div class="modal-header">
<h2>Event JSON</h2>
<button onclick={() => showJsonModal = false} class="close-button">×</button>
<button onclick={() => showJsonModal = false} class="close-button" aria-label="Close">
<Icon name="x" size={20} />
</button>
</div>
<div class="modal-body">
<pre class="json-preview">{eventJson}</pre>
@ -1100,8 +1110,14 @@ @@ -1100,8 +1110,14 @@
<button onclick={() => {
navigator.clipboard.writeText(eventJson);
alert('JSON copied to clipboard');
}}>Copy</button>
<button onclick={() => showJsonModal = false}>Close</button>
}} class="flex items-center gap-2">
<Icon name="copy" size={16} />
<span>Copy</span>
</button>
<button onclick={() => showJsonModal = false} class="flex items-center gap-2">
<Icon name="x" size={16} />
<span>Close</span>
</button>
</div>
</div>
</div>
@ -1175,6 +1191,9 @@ @@ -1175,6 +1191,9 @@
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
@ -1449,6 +1468,9 @@ @@ -1449,6 +1468,9 @@
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--fog-border, #e5e7eb);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.cancel-button {

41
src/lib/components/write/CreateEventForm.svelte

@ -17,6 +17,7 @@ @@ -17,6 +17,7 @@
import type { NostrEvent } from '../../types/nostr.js';
import { autoExtractTags, ensureDTagForParameterizedReplaceable } from '../../services/auto-tagging.js';
import { isParameterizedReplaceableKind } from '../../types/kind-lookup.js';
import Icon from '../ui/Icon.svelte';
const SUPPORTED_KINDS = getWritableKinds();
@ -697,7 +698,9 @@ @@ -697,7 +698,9 @@
>
<div class="modal-header">
<h2>Event JSON</h2>
<button onclick={() => showJsonModal = false} class="close-button">×</button>
<button onclick={() => showJsonModal = false} class="close-button" aria-label="Close">
<Icon name="x" size={20} />
</button>
</div>
<div class="modal-body">
<pre class="json-preview">{eventJson}</pre>
@ -706,8 +709,14 @@ @@ -706,8 +709,14 @@
<button onclick={() => {
navigator.clipboard.writeText(eventJson);
alert('JSON copied to clipboard');
}}>Copy</button>
<button onclick={() => showJsonModal = false}>Close</button>
}} class="flex items-center gap-2">
<Icon name="copy" size={16} />
<span>Copy</span>
</button>
<button onclick={() => showJsonModal = false} class="flex items-center gap-2">
<Icon name="x" size={16} />
<span>Close</span>
</button>
</div>
</div>
</div>
@ -736,7 +745,9 @@ @@ -736,7 +745,9 @@
>
<div class="modal-header">
<h2>Preview</h2>
<button onclick={() => showPreviewModal = false} class="close-button">×</button>
<button onclick={() => showPreviewModal = false} class="close-button" aria-label="Close">
<Icon name="x" size={20} />
</button>
</div>
<div class="modal-body preview-body">
{#if previewEvent && previewContent}
@ -799,7 +810,10 @@ @@ -799,7 +810,10 @@
{/if}
</div>
<div class="modal-footer">
<button onclick={() => showPreviewModal = false}>Close</button>
<button onclick={() => showPreviewModal = false} class="flex items-center gap-2">
<Icon name="x" size={16} />
<span>Close</span>
</button>
</div>
</div>
</div>
@ -828,7 +842,9 @@ @@ -828,7 +842,9 @@
>
<div class="modal-header">
<h2>Example of {effectiveKind} event</h2>
<button onclick={() => showExampleModal = false} class="close-button">×</button>
<button onclick={() => showExampleModal = false} class="close-button" aria-label="Close">
<Icon name="x" size={20} />
</button>
</div>
<div class="modal-body">
<pre class="example-json">{exampleJSON}</pre>
@ -837,8 +853,14 @@ @@ -837,8 +853,14 @@
<button onclick={() => {
navigator.clipboard.writeText(exampleJSON);
alert('Example JSON copied to clipboard');
}}>Copy</button>
<button onclick={() => showExampleModal = false}>Close</button>
}} class="flex items-center gap-2">
<Icon name="copy" size={16} />
<span>Copy</span>
</button>
<button onclick={() => showExampleModal = false} class="flex items-center gap-2">
<Icon name="x" size={16} />
<span>Close</span>
</button>
</div>
</div>
</div>
@ -1591,6 +1613,9 @@ @@ -1591,6 +1613,9 @@
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.modal-footer button:hover {

11
src/lib/modules/discussions/DiscussionCard.svelte

@ -14,6 +14,7 @@ @@ -14,6 +14,7 @@
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo, KIND } from '../../types/kind-lookup.js';
import { stripMarkdown } from '../../services/text-utils.js';
import Icon from '../../components/ui/Icon.svelte';
interface Props {
thread: NostrEvent;
@ -339,7 +340,10 @@ @@ -339,7 +340,10 @@
<span class="text-fog-text-light dark:text-fog-dark-text-light">Last: {getLatestResponseTime()}</span>
{/if}
{#if zapCount > 0}
<span class="font-medium">{zapTotal.toLocaleString()} sats ({zapCount})</span>
<span class="font-medium flex items-center gap-1">
<Icon name="zap" size={14} />
<span>{zapTotal.toLocaleString()} sats ({zapCount})</span>
</span>
{/if}
{/if}
{:else}
@ -350,7 +354,10 @@ @@ -350,7 +354,10 @@
<span class="text-fog-text-light dark:text-fog-dark-text-light">Last: {getLatestResponseTime()}</span>
{/if}
{#if zapCount > 0}
<span class="font-medium">{zapTotal.toLocaleString()} sats ({zapCount})</span>
<span class="font-medium flex items-center gap-1">
<Icon name="zap" size={14} />
<span>{zapTotal.toLocaleString()} sats ({zapCount})</span>
</span>
{/if}
{/if}
{/if}

8
src/lib/modules/feed/ZapReceiptReply.svelte

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import EventMenu from '../../components/EventMenu.svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { getKindInfo } from '../../types/kind-lookup.js';
import Icon from '../../components/ui/Icon.svelte';
interface Props {
zapReceipt: NostrEvent; // Kind 9735 zap receipt
@ -89,14 +90,17 @@ @@ -89,14 +90,17 @@
<div class="card-content" class:expanded bind:this={contentElement}>
{#if parentEvent}
<div class="zap-reply-context">
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">⚡ Zapping</span>
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light flex items-center gap-1">
<Icon name="zap" size={14} />
<span>Zapping</span>
</span>
<ReplyContext {parentEvent} targetId="event-{parentEvent.id}" />
</div>
{/if}
<div class="zap-header flex items-center gap-2 mb-2">
<ProfileBadge pubkey={getZapperPubkey()} />
<span class="text-lg"></span>
<Icon name="zap" size={18} />
<span class="text-sm font-semibold">{getAmount().toLocaleString()} sats</span>
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">{getRelativeTime()}</span>
<div class="ml-auto">

6
src/lib/modules/profiles/PaymentAddresses.svelte

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
import Icon from '../../components/ui/Icon.svelte';
interface Props {
pubkey: string;
@ -114,9 +115,10 @@ @@ -114,9 +115,10 @@
{#if isZappable(type)}
<a
href="lightning:{address}"
class="text-xs text-fog-accent dark:text-fog-dark-accent hover:underline"
class="text-xs text-fog-accent dark:text-fog-dark-accent hover:underline flex items-center gap-1"
>
⚡ Zap
<Icon name="zap" size={14} />
<span>Zap</span>
</a>
{/if}
</div>

14
src/lib/modules/reactions/FeedReactionButtons.svelte

@ -11,6 +11,7 @@ @@ -11,6 +11,7 @@
import EmojiPicker from '../../components/content/EmojiPicker.svelte';
import emojiData from 'unicode-emoji-json/data-ordered-emoji.json';
import { shouldIncludeClientTag } from '../../services/client-tag-preference.js';
import Icon from '../../components/ui/Icon.svelte';
interface Props {
event: NostrEvent;
@ -428,7 +429,7 @@ @@ -428,7 +429,7 @@
}
function getReactionDisplay(content: string): string {
if (content === '+') return '❤';
// For +, we'll render it as heart icon in the template
if (content.startsWith(':') && content.endsWith(':')) {
const url = customEmojiUrls.get(content);
if (url) {
@ -557,7 +558,7 @@ @@ -557,7 +558,7 @@
title="Like or choose reaction"
aria-label="Like or choose reaction"
>
<Icon name="heart" size={16} />
</button>
<EmojiPicker
@ -577,7 +578,7 @@ @@ -577,7 +578,7 @@
title={content === '+' ? 'Liked' : `Reacted with ${content}`}
>
{#if content === '+'}
<Icon name="heart" size={16} />
{:else if content.startsWith(':') && content.endsWith(':')}
<!-- Text emoji - try to display as image if URL available -->
{#if hasEmojiUrl(content)}
@ -612,6 +613,9 @@ @@ -612,6 +613,9 @@
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
display: inline-flex;
align-items: center;
justify-content: center;
}
:global(.dark) .reaction-btn {
@ -664,6 +668,10 @@ @@ -664,6 +668,10 @@
user-select: none;
}
.reaction-display :global(.icon) {
flex-shrink: 0;
}
:global(.dark) .reaction-display {
color: var(--fog-dark-text, #f9fafb);
}

6
src/lib/modules/zaps/ZapButton.svelte

@ -2,7 +2,9 @@ @@ -2,7 +2,9 @@
import { sessionManager } from '../../services/auth/session-manager.js';
import { nostrClient } from '../../services/nostr/nostr-client.js';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
import ZapInvoiceModal from './ZapInvoiceModal.svelte';
import Icon from '../../components/ui/Icon.svelte';
interface Props {
event: NostrEvent;
@ -133,7 +135,8 @@ @@ -133,7 +135,8 @@
title="Zap (z)"
aria-label="Zap"
>
⚡ Zap
<Icon name="zap" size={16} />
<span>Zap</span>
</button>
{#if showInvoiceModal && invoice}
@ -161,6 +164,7 @@ @@ -161,6 +164,7 @@
line-height: 1.5;
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
:global(.dark) .zap-button {

4
src/lib/modules/zaps/ZapReceipt.svelte

@ -2,6 +2,8 @@ @@ -2,6 +2,8 @@
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { onMount } from 'svelte';
import type { NostrEvent } from '../../types/nostr.js';
import { KIND } from '../../types/kind-lookup.js';
import Icon from '../../components/ui/Icon.svelte';
interface Props {
eventId: string; // The event that was zapped
@ -109,7 +111,7 @@ @@ -109,7 +111,7 @@
<span class="text-sm text-fog-text-light dark:text-fog-dark-text-light">Loading zaps...</span>
{:else if zapReceipts.length > 0}
<div class="flex items-center gap-2">
<span class="text-lg"></span>
<Icon name="zap" size={18} />
<span class="text-sm font-semibold">{totalAmount.toLocaleString()} sats</span>
<span class="text-xs text-fog-text-light dark:text-fog-dark-text-light">
({zapReceipts.length} {zapReceipts.length === 1 ? 'zap' : 'zaps'})

46
src/routes/login/+page.svelte

@ -9,6 +9,7 @@ @@ -9,6 +9,7 @@
import { listAnonymousKeys } from '../../lib/services/cache/anonymous-key-store.js';
import { sessionManager } from '../../lib/services/auth/session-manager.js';
import { page } from '$app/stores';
import Icon from '../../lib/components/ui/Icon.svelte';
onMount(async () => {
await nostrClient.initialize();
@ -336,9 +337,10 @@ @@ -336,9 +337,10 @@
<button
onclick={loginWithNIP07}
disabled={loading}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded"
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded flex items-center justify-center gap-2"
>
{loading ? 'Connecting...' : 'Login with NIP-07'}
<Icon name="log-in" size={16} />
<span>{loading ? 'Connecting...' : 'Login with NIP-07'}</span>
</button>
</div>
{:else if activeTab === 'nsec'}
@ -381,18 +383,20 @@ @@ -381,18 +383,20 @@
<button
onclick={loginWithStoredNsec}
disabled={loading || !nsecPassword}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded"
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded flex items-center justify-center gap-2"
>
{loading ? 'Logging in...' : 'Login'}
<Icon name="log-in" size={16} />
<span>{loading ? 'Logging in...' : 'Login'}</span>
</button>
</div>
{/if}
<button
onclick={() => { showNewNsecForm = true; selectedNsecKey = null; }}
class="w-full px-4 py-2 border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors rounded"
class="w-full px-4 py-2 border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors rounded flex items-center justify-center gap-2"
>
Add New Nsec Key
<Icon name="plus" size={16} />
<span>Add New Nsec Key</span>
</button>
</div>
{:else}
@ -401,10 +405,11 @@ @@ -401,10 +405,11 @@
{#if storedNsecKeys.length > 0}
<button
onclick={() => { showNewNsecForm = false; selectedNsecKey = null; }}
class="text-fog-accent dark:text-fog-dark-accent hover:underline"
class="text-fog-accent dark:text-fog-dark-accent hover:underline flex items-center gap-1"
style="font-size: 0.875em;"
>
← Back to stored keys
<Icon name="arrow-left" size={14} />
<span>Back to stored keys</span>
</button>
{/if}
<p class="text-fog-text-light dark:text-fog-dark-text-light" style="font-size: 0.875em;">
@ -455,9 +460,10 @@ @@ -455,9 +460,10 @@
<button
onclick={loginWithNsec}
disabled={loading}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded"
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded flex items-center justify-center gap-2"
>
{loading ? 'Encrypting and storing...' : 'Login with Nsec'}
<Icon name="log-in" size={16} />
<span>{loading ? 'Encrypting and storing...' : 'Login with Nsec'}</span>
</button>
</div>
{/if}
@ -501,18 +507,20 @@ @@ -501,18 +507,20 @@
<button
onclick={loginWithStoredAnonymous}
disabled={loading || !anonymousPassword}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded"
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded flex items-center justify-center gap-2"
>
{loading ? 'Logging in...' : 'Login'}
<Icon name="log-in" size={16} />
<span>{loading ? 'Logging in...' : 'Login'}</span>
</button>
</div>
{/if}
<button
onclick={() => { showNewAnonymousForm = true; selectedAnonymousKey = null; }}
class="w-full px-4 py-2 border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors rounded"
class="w-full px-4 py-2 border border-fog-border dark:border-fog-dark-border bg-fog-post dark:bg-fog-dark-post text-fog-text dark:text-fog-dark-text hover:bg-fog-highlight dark:hover:bg-fog-dark-highlight transition-colors rounded flex items-center justify-center gap-2"
>
Generate New Anonymous Key
<Icon name="plus" size={16} />
<span>Generate New Anonymous Key</span>
</button>
</div>
{:else}
@ -521,10 +529,11 @@ @@ -521,10 +529,11 @@
{#if storedAnonymousKeys.length > 0}
<button
onclick={() => { showNewAnonymousForm = false; selectedAnonymousKey = null; }}
class="text-fog-accent dark:text-fog-dark-accent hover:underline"
class="text-fog-accent dark:text-fog-dark-accent hover:underline flex items-center gap-1"
style="font-size: 0.875em;"
>
← Back to stored keys
<Icon name="arrow-left" size={14} />
<span>Back to stored keys</span>
</button>
{/if}
<p class="text-fog-text-light dark:text-fog-dark-text-light" style="font-size: 0.875em;">
@ -559,9 +568,10 @@ @@ -559,9 +568,10 @@
<button
onclick={loginAsAnonymous}
disabled={loading}
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded"
class="w-full px-4 py-2 bg-fog-accent dark:bg-fog-dark-accent text-white hover:opacity-90 disabled:opacity-50 transition-colors rounded flex items-center justify-center gap-2"
>
{loading ? 'Generating key...' : 'Generate Anonymous Key'}
<Icon name="log-in" size={16} />
<span>{loading ? 'Generating key...' : 'Generate Anonymous Key'}</span>
</button>
</div>
{/if}

16
src/routes/settings/+page.svelte

@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
import { hasExpiringEventsEnabled } from '../../lib/services/event-expiration.js';
import { shouldIncludeClientTag, setIncludeClientTag } from '../../lib/services/client-tag-preference.js';
import { goto } from '$app/navigation';
import Icon from '../../lib/components/ui/Icon.svelte';
type TextSize = 'small' | 'medium' | 'large';
type LineSpacing = 'tight' | 'normal' | 'loose';
@ -114,10 +115,11 @@ @@ -114,10 +115,11 @@
<div class="settings-header">
<button
onclick={handleBack}
class="back-button"
class="back-button flex items-center gap-2"
aria-label="Go back to previous page"
>
← Back
<Icon name="arrow-left" size={16} />
<span>Back</span>
</button>
<h1 class="font-bold text-fog-text dark:text-fog-dark-text font-mono mb-6" style="font-size: 1.5em;">/Settings</h1>
</div>
@ -135,7 +137,7 @@ @@ -135,7 +137,7 @@
class:active={isDark}
aria-label={isDark ? 'Switch to light mode' : 'Switch to dark mode'}
>
<span class="emoji">{isDark ? '☀' : '🌙'}</span>
<Icon name={isDark ? 'sun' : 'moon'} size={16} />
<span>{isDark ? 'Light' : 'Dark'}</span>
</button>
</div>
@ -252,7 +254,9 @@ @@ -252,7 +254,9 @@
class:active={expiringEvents}
aria-label={expiringEvents ? 'Disable expiring events' : 'Enable expiring events'}
>
<span>{expiringEvents ? '✓' : ''}</span>
{#if expiringEvents}
<Icon name="check" size={16} />
{/if}
<span>{expiringEvents ? 'Enabled' : 'Disabled'}</span>
</button>
</div>
@ -273,7 +277,9 @@ @@ -273,7 +277,9 @@
class:active={includeClientTag}
aria-label={includeClientTag ? 'Disable client tag' : 'Enable client tag'}
>
<span>{includeClientTag ? '✓' : ''}</span>
{#if includeClientTag}
<Icon name="check" size={16} />
{/if}
<span>{includeClientTag ? 'Enabled' : 'Disabled'}</span>
</button>
</div>

1
static/icons/arrow-left.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>

After

Width:  |  Height:  |  Size: 234 B

1
static/icons/check.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>

After

Width:  |  Height:  |  Size: 223 B

1
static/icons/chevron-down.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>

After

Width:  |  Height:  |  Size: 212 B

1
static/icons/chevron-up.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>

After

Width:  |  Height:  |  Size: 214 B

1
static/icons/code.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>

After

Width:  |  Height:  |  Size: 259 B

1
static/icons/copy.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2h8c1.1 0 2 .9 2 2"/></svg>

After

Width:  |  Height:  |  Size: 310 B

1
static/icons/edit.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>

After

Width:  |  Height:  |  Size: 325 B

1
static/icons/eye.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>

After

Width:  |  Height:  |  Size: 275 B

1
static/icons/file-text.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>

After

Width:  |  Height:  |  Size: 352 B

1
static/icons/heart.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.29 1.51 4.04 3 5.5l7 7Z"/></svg>

After

Width:  |  Height:  |  Size: 338 B

1
static/icons/image.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>

After

Width:  |  Height:  |  Size: 326 B

1
static/icons/key.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="7.5" cy="15.5" r="5.5"/><path d="m21 2-1.76 1.76a2.5 2.5 0 0 0 0 3.52l1.8 1.81-1.8 1.81-4.24-4.24 1.81-1.81a2.5 2.5 0 0 0 0-3.52L21 2"/></svg>

After

Width:  |  Height:  |  Size: 336 B

1
static/icons/link.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>

After

Width:  |  Height:  |  Size: 331 B

1
static/icons/log-in.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" x2="3" y1="12" y2="12"/></svg>

After

Width:  |  Height:  |  Size: 316 B

1
static/icons/log-out.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>

After

Width:  |  Height:  |  Size: 314 B

1
static/icons/message-square.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>

After

Width:  |  Height:  |  Size: 261 B

1
static/icons/moon.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>

After

Width:  |  Height:  |  Size: 234 B

1
static/icons/plus.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="M12 5v14"/></svg>

After

Width:  |  Height:  |  Size: 228 B

1
static/icons/radio.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2c-2.3-2.3-2.3-6.1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8c2.3 2.3 2.3 6.1 0 8.5"/><path d="M19.1 4.9c3.9 3.9 3.9 10.3 0 14.2"/></svg>

After

Width:  |  Height:  |  Size: 393 B

1
static/icons/search.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>

After

Width:  |  Height:  |  Size: 247 B

1
static/icons/send.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>

After

Width:  |  Height:  |  Size: 242 B

1
static/icons/settings.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v18.84a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2z"/><path d="M18.38 6H17a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h1.38a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2z"/><path d="M5.62 6H7a2 2 0 0 0 2 2v8a2 2 0 0 0-2 2H5.62a2 2 0 0 0-2-2V8a2 2 0 0 0 2-2z"/></svg>

After

Width:  |  Height:  |  Size: 458 B

1
static/icons/share.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/></svg>

After

Width:  |  Height:  |  Size: 378 B

1
static/icons/smile.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" 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>

After

Width:  |  Height:  |  Size: 333 B

1
static/icons/sun.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>

After

Width:  |  Height:  |  Size: 429 B

1
static/icons/trash.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>

After

Width:  |  Height:  |  Size: 302 B

1
static/icons/upload.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>

After

Width:  |  Height:  |  Size: 313 B

1
static/icons/user.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>

After

Width:  |  Height:  |  Size: 271 B

1
static/icons/video.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"/><rect x="2" y="6" width="14" height="12" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 322 B

1
static/icons/x.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>

After

Width:  |  Height:  |  Size: 232 B

1
static/icons/zap.svg

@ -0,0 +1 @@ @@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>

After

Width:  |  Height:  |  Size: 355 B

Loading…
Cancel
Save