feat: add Globe icon for public shares, update NoteCard and NoteEditor

- Create SharingIndicator component with Globe/Users icon priority
- NoteCard shows Globe icon (gold) for public shares, Users for collabs
- NoteEditor share button reflects public link state
- Share token propagated through store on create/revoke

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Frederic Cabassut
2026-03-20 22:06:41 +01:00
committed by Fred Cabassut
co-authored by Claude Opus 4.6
parent 0661196eca
commit 9dde77af96
3 changed files with 122 additions and 6 deletions
+6 -4
View File
@@ -4,6 +4,7 @@
import { togglePin, trashNote, archiveNote, unarchiveNote, restoreNote, deleteNote, leaveNote, currentFilter } from '$lib/stores/notes.js';
import ImageLightbox from './ImageLightbox.svelte';
import CollaboratorPopover from './CollaboratorPopover.svelte';
import SharingIndicator from './SharingIndicator.svelte';
import type { Note } from '$lib/types/index.js';
import Undo2 from 'lucide-svelte/icons/undo-2';
import Trash2 from 'lucide-svelte/icons/trash-2';
@@ -101,10 +102,11 @@
<!-- Status indicators (top-right) -->
<div class="absolute top-1.5 right-1.5 flex items-center gap-0.5">
{#if note.isShared && note.collaborators}
<CollaboratorPopover
collaborators={note.collaborators}
ownerName={note.isOwner ? 'You' : undefined}
{#if note.shareToken || (note.isShared && note.collaborators)}
<SharingIndicator
shareToken={note.shareToken}
collaborators={note.collaborators ?? []}
isOwner={note.isOwner ?? true}
/>
{/if}
{#if note.pinned}
+22 -2
View File
@@ -21,6 +21,7 @@
import FileText from 'lucide-svelte/icons/file-text';
import UserPlus from 'lucide-svelte/icons/user-plus';
import Users from 'lucide-svelte/icons/users';
import Globe from 'lucide-svelte/icons/globe';
import History from 'lucide-svelte/icons/history';
interface Props {
@@ -70,14 +71,16 @@
let showHistory = $state(false);
// svelte-ignore state_referenced_locally
let collaboratorsList = $state<Collaborator[]>(note?.collaborators ?? []);
// svelte-ignore state_referenced_locally
let currentShareToken = $state<string | undefined>(note?.shareToken);
const isOwner = $derived(note?.isOwner !== false);
const isShared = $derived(collaboratorsList.length > 0);
const hasPublicLink = $derived(!!currentShareToken);
// Use displayName from first user or 'You' for owner
const ownerName = $derived(isOwner ? 'You' : 'Owner');
function handleCollaboratorsUpdate(updated: Collaborator[]) {
collaboratorsList = updated;
// Update the note in the store
if (noteId) {
notes.update((list) =>
list.map((n) =>
@@ -89,6 +92,19 @@
}
}
function handleShareUpdate(token: string | null) {
currentShareToken = token ?? undefined;
if (noteId) {
notes.update((list) =>
list.map((n) =>
n.id === noteId
? { ...n, shareToken: token ?? undefined }
: n
)
);
}
}
async function toggleShareDialog() {
if (currentlyNew) {
const id = await autoSave();
@@ -401,7 +417,9 @@
title="Share note"
data-testid="share-toggle"
>
{#if isShared}
{#if hasPublicLink}
<Globe class="h-5 w-5 text-[var(--primary)]" />
{:else if isShared}
<Users class="h-5 w-5 text-[var(--primary)]" />
{:else}
<UserPlus class="h-5 w-5" />
@@ -442,8 +460,10 @@
noteId={noteId}
collaborators={collaboratorsList}
{ownerName}
shareToken={currentShareToken}
onClose={() => (showShareDialog = false)}
onUpdate={handleCollaboratorsUpdate}
onShareUpdate={handleShareUpdate}
/>
{/if}
@@ -0,0 +1,94 @@
<script lang="ts">
import type { Collaborator } from '$lib/types/index.js';
import Globe from 'lucide-svelte/icons/globe';
import Users from 'lucide-svelte/icons/users';
interface Props {
shareToken?: string;
collaborators: Collaborator[];
isOwner: boolean;
}
const { shareToken, collaborators, isOwner }: Props = $props();
let showPopover = $state(false);
let hoverTimeout: ReturnType<typeof setTimeout> | undefined;
const hasPublicLink = $derived(!!shareToken);
const hasCollaborators = $derived(collaborators.length > 0);
const tooltipLines = $derived.by(() => {
const lines: string[] = [];
if (hasPublicLink) lines.push('Shared publicly via link');
if (hasCollaborators) {
lines.push(`Shared with ${collaborators.length} collaborator${collaborators.length > 1 ? 's' : ''}`);
}
return lines;
});
function handleMouseEnter() {
hoverTimeout = setTimeout(() => {
showPopover = true;
}, 150);
}
function handleMouseLeave() {
clearTimeout(hoverTimeout);
showPopover = false;
}
function handleClick(e: Event) {
e.stopPropagation();
showPopover = !showPopover;
}
</script>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="relative inline-flex"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
>
<button
type="button"
class="rounded-sm p-1 {hasPublicLink ? 'text-[var(--primary)]' : 'text-[var(--text-muted)]'} hover:text-[var(--primary)]"
title={tooltipLines.join(', ')}
onclick={handleClick}
data-testid="sharing-indicator"
>
{#if hasPublicLink}
<Globe class="h-4 w-4" />
{:else}
<Users class="h-4 w-4" />
{/if}
</button>
{#if showPopover}
<div
class="absolute right-0 bottom-full z-20 mb-1 min-w-[180px] rounded-sm border border-[var(--border-subtle)] bg-[var(--bg-surface)] p-2 shadow-[var(--card-shadow)] animate-[fade-in_150ms_ease-out]"
data-testid="sharing-popover"
>
{#if hasPublicLink}
<div class="mb-1 flex items-center gap-1.5">
<Globe class="h-3 w-3 text-[var(--primary)]" />
<span class="text-xs font-semibold text-[var(--primary)]">Public link active</span>
</div>
{/if}
{#if hasCollaborators}
<p class="mb-1 text-xs font-semibold text-[var(--text-muted)]">Shared with</p>
<ul class="space-y-0.5">
{#if isOwner}
<li class="text-xs text-[var(--text)]">
You <span class="text-[var(--text-muted)]">(Owner)</span>
</li>
{/if}
{#each collaborators as collab}
<li class="text-xs text-[var(--text)]">
{collab.displayName || collab.email}
</li>
{/each}
</ul>
{/if}
</div>
{/if}
</div>