grain.social is a photo sharing platform built on atproto. grain.social
atproto photography appview
57
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: horizontal overflow menu on all galleries, move report to menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+33 -13
+3 -3
app/lib/components/atoms/OverflowMenu.svelte
··· 1 1 <script lang="ts"> 2 2 import type { Snippet } from 'svelte' 3 - import { EllipsisVertical } from 'lucide-svelte' 3 + import { EllipsisVertical, Ellipsis } from 'lucide-svelte' 4 4 5 - let { children }: { children: Snippet } = $props() 5 + let { children, horizontal = false }: { children: Snippet; horizontal?: boolean } = $props() 6 6 7 7 let open = $state(false) 8 8 </script> 9 9 10 10 <div class="overflow-menu"> 11 11 <button class="overflow-btn" type="button" onclick={(e) => { e.stopPropagation(); open = !open }} aria-label="More options"> 12 - <EllipsisVertical size={18} /> 12 + {#if horizontal}<Ellipsis size={18} />{:else}<EllipsisVertical size={18} />{/if} 13 13 </button> 14 14 {#if open} 15 15 <div class="overflow-backdrop" role="button" tabindex="-1" onclick={() => (open = false)} onkeydown={(e) => e.key === 'Escape' && (open = false)}></div>
+17 -10
app/lib/components/molecules/GalleryCard.svelte
··· 10 10 import ReportButton from './ReportButton.svelte' 11 11 import ProfilePopover from './ProfilePopover.svelte' 12 12 import { relativeTime } from '$lib/utils' 13 - import { MessageCircle, Send, ChevronLeft, ChevronRight, Trash2, Heart } from 'lucide-svelte' 13 + import { MessageCircle, Send, ChevronLeft, ChevronRight, Trash2, Heart, Flag } from 'lucide-svelte' 14 14 import OverflowMenu from '../atoms/OverflowMenu.svelte' 15 15 import { share } from '$lib/utils/share' 16 16 import { browser } from '$app/environment' ··· 24 24 const queryClient = useQueryClient() 25 25 const isOwner = $derived($viewer?.did === gallery.creator?.did) 26 26 let deleting = $state(false) 27 + let reportOpen = $state(false) 27 28 let doFavorite: (() => void) | undefined = $state(undefined) 28 29 let showHeartAnim = $state(false) 29 30 ··· 170 171 </div> 171 172 </a> 172 173 </ProfilePopover> 173 - {#if isOwner} 174 - <OverflowMenu> 174 + <OverflowMenu horizontal> 175 + {#if $isAuthenticated} 176 + <button class="menu-item" type="button" onclick={() => (reportOpen = true)}> 177 + <Flag size={15} /> 178 + Report 179 + </button> 180 + {/if} 181 + {#if isOwner} 182 + <div class="menu-divider"></div> 175 183 <button class="menu-item delete" type="button" onclick={deleteGallery} disabled={deleting}> 176 184 <Trash2 size={15} /> 177 185 Delete gallery 178 186 </button> 179 - </OverflowMenu> 180 - {/if} 187 + {/if} 188 + </OverflowMenu> 181 189 </header> 182 190 183 191 {#if photos.length > 0} ··· 259 267 <button class="stat" type="button" onclick={handleShare} aria-label="Share"> 260 268 <Send size={20} /> 261 269 </button> 262 - <span class="spacer"></span> 263 - {#if $isAuthenticated} 264 - <ReportButton subjectUri={gallery.uri} subjectCid={gallery.cid} /> 265 - {/if} 266 270 </div> 271 + {#if $isAuthenticated} 272 + <ReportButton subjectUri={gallery.uri} subjectCid={gallery.cid} showButton={false} bind:open={reportOpen} /> 273 + {/if} 267 274 268 275 <Toast message="Link copied" bind:visible={showToast} /> 269 276 ··· 365 372 .menu-item.delete { 366 373 color: #f87171; 367 374 } 375 + .menu-divider { height: 1px; background: var(--border); margin: 4px 0; } 368 376 .menu-item:disabled { 369 377 opacity: 0.5; 370 378 cursor: not-allowed; ··· 543 551 transition: opacity 0.15s; 544 552 } 545 553 .stat:hover { opacity: 0.7; } 546 - .spacer { flex: 1; } 547 554 .stat-count { color: var(--text-secondary); } 548 555 549 556 /* Content */
+13
app/lib/components/molecules/ReportButton.svelte
··· 10 10 subjectUri, 11 11 subjectCid, 12 12 variant = 'default', 13 + showButton = true, 14 + open: openProp = $bindable(false), 13 15 onopen, 14 16 onclose, 15 17 }: { 16 18 subjectUri: string 17 19 subjectCid: string 18 20 variant?: 'default' | 'overlay' 21 + showButton?: boolean 22 + open?: boolean 19 23 onopen?: () => void 20 24 onclose?: () => void 21 25 } = $props() 22 26 23 27 let open = $state(false) 28 + 29 + $effect(() => { 30 + if (openProp && !open) { 31 + openModal() 32 + openProp = false 33 + } 34 + }) 24 35 let label = $state('') 25 36 let reason = $state('') 26 37 let submitting = $state(false) ··· 73 84 } 74 85 </script> 75 86 87 + {#if showButton} 76 88 <button 77 89 class={variant === 'overlay' ? 'overlay-btn' : 'stat'} 78 90 type="button" ··· 82 94 > 83 95 <Flag size={variant === 'overlay' ? 20 : 18} /> 84 96 </button> 97 + {/if} 85 98 86 99 <Modal bind:open title={submitted ? 'Report Submitted' : 'Report Content'}> 87 100 {#if submitted}