feat(admin): update admin components with improved UI and icons
- Add album and media icons for better navigation - Update AdminNavBar with new routes and improved styling - Enhance GalleryUploader with better drag-and-drop support - Improve ImagePicker and ImageUploader components - Remove unused ImageUploadPlaceholder component - Update MediaDetailsModal with album association features - Improve Modal component styling and animations - Add PostDropdown for post management actions Modernizes the admin interface with better usability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
78663151a8
commit
0d4bf6d53f
10 changed files with 311 additions and 488 deletions
3
src/assets/icons/album.svg
Normal file
3
src/assets/icons/album.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3C2 2.44772 2.44772 2 3 2H15C16.6569 2 18 3.34315 18 5V15C18 16.6569 16.6569 18 15 18H3C2.44772 18 2 17.5523 2 17V3ZM7 5C6.44772 5 6 5.44772 6 6V10C6 10.5523 6.44772 11 7 11H13C13.5523 11 14 10.5523 14 10V6C14 5.44772 13.5523 5 13 5H7ZM6 14C6 13.4477 6.44772 13 7 13H13C13.5523 13 14 13.4477 14 14C14 14.5523 13.5523 15 13 15H7C6.44772 15 6 14.5523 6 14Z" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 534 B |
4
src/assets/icons/media.svg
Normal file
4
src/assets/icons/media.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M13 18C15.7614 18 18 15.7614 18 13C18 10.2386 15.7614 8 13 8C10.2386 8 8 10.2386 8 13C8 15.7614 10.2386 18 13 18Z" />
|
||||||
|
<path d="M10.5 2C11.3284 2 12 2.67157 12 3.5V6.07227C8.93446 6.51084 6.51084 8.93446 6.07227 12H3.5C2.67157 12 2 11.3284 2 10.5V3.5C2 2.67157 2.67157 2 3.5 2H10.5Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 398 B |
|
|
@ -4,7 +4,8 @@
|
||||||
import AvatarSimple from '$lib/components/AvatarSimple.svelte'
|
import AvatarSimple from '$lib/components/AvatarSimple.svelte'
|
||||||
import WorkIcon from '$icons/work.svg?component'
|
import WorkIcon from '$icons/work.svg?component'
|
||||||
import UniverseIcon from '$icons/universe.svg?component'
|
import UniverseIcon from '$icons/universe.svg?component'
|
||||||
import PhotosIcon from '$icons/photos.svg?component'
|
import MediaIcon from '$icons/media.svg?component'
|
||||||
|
import AlbumIcon from '$icons/album.svg?component'
|
||||||
|
|
||||||
const currentPath = $derived($page.url.pathname)
|
const currentPath = $derived($page.url.pathname)
|
||||||
let isScrolled = $state(false)
|
let isScrolled = $state(false)
|
||||||
|
|
@ -31,8 +32,8 @@
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ text: 'Projects', href: '/admin/projects', icon: WorkIcon },
|
{ text: 'Projects', href: '/admin/projects', icon: WorkIcon },
|
||||||
{ text: 'Universe', href: '/admin/posts', icon: UniverseIcon },
|
{ text: 'Universe', href: '/admin/posts', icon: UniverseIcon },
|
||||||
{ text: 'Albums', href: '/admin/albums', icon: PhotosIcon },
|
{ text: 'Albums', href: '/admin/albums', icon: AlbumIcon },
|
||||||
{ text: 'Media', href: '/admin/media', icon: PhotosIcon }
|
{ text: 'Media', href: '/admin/media', icon: MediaIcon }
|
||||||
]
|
]
|
||||||
|
|
||||||
// Calculate active index based on current path
|
// Calculate active index based on current path
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import Input from './Input.svelte'
|
import Input from './Input.svelte'
|
||||||
import SmartImage from '../SmartImage.svelte'
|
import SmartImage from '../SmartImage.svelte'
|
||||||
import MediaLibraryModal from './MediaLibraryModal.svelte'
|
import UnifiedMediaModal from './UnifiedMediaModal.svelte'
|
||||||
import MediaDetailsModal from './MediaDetailsModal.svelte'
|
import MediaDetailsModal from './MediaDetailsModal.svelte'
|
||||||
import { authenticatedFetch } from '$lib/admin-auth'
|
import { authenticatedFetch } from '$lib/admin-auth'
|
||||||
|
|
||||||
|
|
@ -369,7 +369,7 @@
|
||||||
thumbnailUrl: media.thumbnailUrl,
|
thumbnailUrl: media.thumbnailUrl,
|
||||||
width: media.width,
|
width: media.width,
|
||||||
height: media.height,
|
height: media.height,
|
||||||
altText: media.altText || '',
|
// altText removed - using description only
|
||||||
description: media.description || '',
|
description: media.description || '',
|
||||||
isPhotography: media.isPhotography || false,
|
isPhotography: media.isPhotography || false,
|
||||||
createdAt: media.createdAt,
|
createdAt: media.createdAt,
|
||||||
|
|
@ -387,7 +387,7 @@
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
value[index] = {
|
value[index] = {
|
||||||
...value[index],
|
...value[index],
|
||||||
altText: updatedMedia.altText,
|
// altText removed - using description only
|
||||||
description: updatedMedia.description,
|
description: updatedMedia.description,
|
||||||
isPhotography: updatedMedia.isPhotography,
|
isPhotography: updatedMedia.isPhotography,
|
||||||
updatedAt: updatedMedia.updatedAt
|
updatedAt: updatedMedia.updatedAt
|
||||||
|
|
@ -587,13 +587,13 @@
|
||||||
thumbnailUrl: media.thumbnailUrl,
|
thumbnailUrl: media.thumbnailUrl,
|
||||||
width: media.width,
|
width: media.width,
|
||||||
height: media.height,
|
height: media.height,
|
||||||
altText: media.altText,
|
// altText removed - using description only
|
||||||
description: media.description,
|
description: media.description,
|
||||||
isPhotography: media.isPhotography || false,
|
isPhotography: media.isPhotography || false,
|
||||||
createdAt: media.createdAt,
|
createdAt: media.createdAt,
|
||||||
updatedAt: media.updatedAt
|
updatedAt: media.updatedAt
|
||||||
}}
|
}}
|
||||||
alt={media.altText || media.filename || 'Gallery image'}
|
alt={media.description || media.filename || 'Gallery image'}
|
||||||
containerWidth={300}
|
containerWidth={300}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
aspectRatio="1:1"
|
aspectRatio="1:1"
|
||||||
|
|
@ -675,7 +675,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Library Modal -->
|
<!-- Media Library Modal -->
|
||||||
<MediaLibraryModal
|
<UnifiedMediaModal
|
||||||
bind:isOpen={isMediaLibraryOpen}
|
bind:isOpen={isMediaLibraryOpen}
|
||||||
mode="multiple"
|
mode="multiple"
|
||||||
fileType="image"
|
fileType="image"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import MediaLibraryModal from './MediaLibraryModal.svelte'
|
import UnifiedMediaModal from './UnifiedMediaModal.svelte'
|
||||||
import type { Media } from '@prisma/client'
|
import type { Media } from '@prisma/client'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -212,14 +212,15 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Media Library Modal -->
|
<!-- Media Library Modal -->
|
||||||
<MediaLibraryModal
|
<UnifiedMediaModal
|
||||||
bind:isOpen={showModal}
|
bind:isOpen={showModal}
|
||||||
mode="single"
|
mode="single"
|
||||||
fileType="image"
|
fileType="image"
|
||||||
{selectedIds}
|
{selectedIds}
|
||||||
title="Select Image"
|
title="Select Image"
|
||||||
confirmText="Select Image"
|
confirmText="Select Image"
|
||||||
onselect={handleImageSelect}
|
onSelect={handleImageSelect}
|
||||||
|
onClose={() => (showModal = false)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import type { NodeViewProps } from '@tiptap/core'
|
|
||||||
import type { Media } from '@prisma/client'
|
|
||||||
import Image from 'lucide-svelte/icons/image'
|
|
||||||
import Upload from 'lucide-svelte/icons/upload'
|
|
||||||
import Link from 'lucide-svelte/icons/link'
|
|
||||||
import Grid from 'lucide-svelte/icons/grid-3x3'
|
|
||||||
import { NodeViewWrapper } from 'svelte-tiptap'
|
|
||||||
import MediaLibraryModal from './MediaLibraryModal.svelte'
|
|
||||||
|
|
||||||
const { editor, deleteNode }: NodeViewProps = $props()
|
|
||||||
|
|
||||||
let fileInput: HTMLInputElement
|
|
||||||
let isDragging = $state(false)
|
|
||||||
let isMediaLibraryOpen = $state(false)
|
|
||||||
let isUploading = $state(false)
|
|
||||||
|
|
||||||
function handleBrowseLibrary(e: MouseEvent) {
|
|
||||||
if (!editor.isEditable) return
|
|
||||||
e.preventDefault()
|
|
||||||
isMediaLibraryOpen = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDirectUpload(e: MouseEvent) {
|
|
||||||
if (!editor.isEditable) return
|
|
||||||
e.preventDefault()
|
|
||||||
fileInput.click()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMediaSelect(media: Media | Media[]) {
|
|
||||||
const selectedMedia = Array.isArray(media) ? media[0] : media
|
|
||||||
if (selectedMedia) {
|
|
||||||
// Set a reasonable default width (max 600px)
|
|
||||||
const displayWidth =
|
|
||||||
selectedMedia.width && selectedMedia.width > 600 ? 600 : selectedMedia.width
|
|
||||||
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.setImage({
|
|
||||||
src: selectedMedia.url,
|
|
||||||
alt: selectedMedia.altText || '',
|
|
||||||
width: displayWidth,
|
|
||||||
height: selectedMedia.height,
|
|
||||||
align: 'center'
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
isMediaLibraryOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMediaLibraryClose() {
|
|
||||||
isMediaLibraryOpen = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFileSelect(e: Event) {
|
|
||||||
const target = e.target as HTMLInputElement
|
|
||||||
const file = target.files?.[0]
|
|
||||||
if (file) {
|
|
||||||
await uploadFile(file)
|
|
||||||
}
|
|
||||||
// Reset input
|
|
||||||
target.value = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadFile(file: File) {
|
|
||||||
// Check file type
|
|
||||||
if (!file.type.startsWith('image/')) {
|
|
||||||
alert('Please select an image file')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file size (2MB max)
|
|
||||||
const filesize = file.size / 1024 / 1024
|
|
||||||
if (filesize > 2) {
|
|
||||||
alert(`Image too large! File size: ${filesize.toFixed(2)} MB (max 2MB)`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
isUploading = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const auth = localStorage.getItem('admin_auth')
|
|
||||||
if (!auth) {
|
|
||||||
throw new Error('Not authenticated')
|
|
||||||
}
|
|
||||||
|
|
||||||
const formData = new FormData()
|
|
||||||
formData.append('file', file)
|
|
||||||
formData.append('type', 'image')
|
|
||||||
|
|
||||||
const response = await fetch('/api/media/upload', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
Authorization: `Basic ${auth}`
|
|
||||||
},
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Upload failed')
|
|
||||||
}
|
|
||||||
|
|
||||||
const media = await response.json()
|
|
||||||
|
|
||||||
// Insert the uploaded image with reasonable default width
|
|
||||||
const displayWidth = media.width && media.width > 600 ? 600 : media.width
|
|
||||||
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.setImage({
|
|
||||||
src: media.url,
|
|
||||||
alt: media.altText || '',
|
|
||||||
width: displayWidth,
|
|
||||||
height: media.height,
|
|
||||||
align: 'center'
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Image upload failed:', error)
|
|
||||||
alert('Failed to upload image. Please try again.')
|
|
||||||
} finally {
|
|
||||||
isUploading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Drag and drop handlers
|
|
||||||
function handleDragOver(e: DragEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
isDragging = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDragLeave(e: DragEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
isDragging = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDrop(e: DragEvent) {
|
|
||||||
e.preventDefault()
|
|
||||||
e.stopPropagation()
|
|
||||||
isDragging = false
|
|
||||||
|
|
||||||
const file = e.dataTransfer?.files[0]
|
|
||||||
if (file && file.type.startsWith('image/')) {
|
|
||||||
await uploadFile(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle keyboard navigation
|
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
handleBrowseLibrary(e as any)
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
deleteNode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NodeViewWrapper class="edra-media-placeholder-wrapper" contenteditable="false">
|
|
||||||
<input
|
|
||||||
bind:this={fileInput}
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onchange={handleFileSelect}
|
|
||||||
style="display: none;"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="edra-image-placeholder-container">
|
|
||||||
{#if isUploading}
|
|
||||||
<div class="edra-image-placeholder-uploading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<span>Uploading image...</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
class="edra-image-placeholder-option"
|
|
||||||
onclick={handleDirectUpload}
|
|
||||||
onkeydown={handleKeyDown}
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Upload Image"
|
|
||||||
title="Upload from device"
|
|
||||||
>
|
|
||||||
<Upload class="edra-image-placeholder-icon" />
|
|
||||||
<span class="edra-image-placeholder-text">Upload Image</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="edra-image-placeholder-option"
|
|
||||||
onclick={handleBrowseLibrary}
|
|
||||||
onkeydown={handleKeyDown}
|
|
||||||
tabindex="0"
|
|
||||||
aria-label="Browse Media Library"
|
|
||||||
title="Choose from library"
|
|
||||||
>
|
|
||||||
<Grid class="edra-image-placeholder-icon" />
|
|
||||||
<span class="edra-image-placeholder-text">Browse Library</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Media Library Modal -->
|
|
||||||
<MediaLibraryModal
|
|
||||||
bind:isOpen={isMediaLibraryOpen}
|
|
||||||
mode="single"
|
|
||||||
fileType="image"
|
|
||||||
onSelect={handleMediaSelect}
|
|
||||||
onClose={handleMediaLibraryClose}
|
|
||||||
/>
|
|
||||||
</NodeViewWrapper>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.edra-image-placeholder-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
border: 2px dashed #e5e7eb;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f9fafb;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edra-image-placeholder-container:hover {
|
|
||||||
border-color: #d1d5db;
|
|
||||||
background: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edra-image-placeholder-option {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: white;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edra-image-placeholder-option:hover {
|
|
||||||
border-color: #d1d5db;
|
|
||||||
background: #f9fafb;
|
|
||||||
transform: translateY(-1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.edra-image-placeholder-option:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.edra-image-placeholder-uploading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 20px;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid #f3f4f6;
|
|
||||||
border-top: 2px solid #3b82f6;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.edra-image-placeholder-icon) {
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.edra-image-placeholder-text {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #6b7280;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import Input from './Input.svelte'
|
import Input from './Input.svelte'
|
||||||
import SmartImage from '../SmartImage.svelte'
|
import SmartImage from '../SmartImage.svelte'
|
||||||
import MediaLibraryModal from './MediaLibraryModal.svelte'
|
import UnifiedMediaModal from './UnifiedMediaModal.svelte'
|
||||||
import { authenticatedFetch } from '$lib/admin-auth'
|
import { authenticatedFetch } from '$lib/admin-auth'
|
||||||
import RefreshIcon from '$icons/refresh.svg?component'
|
import RefreshIcon from '$icons/refresh.svg?component'
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
aspectRatio?: string // e.g., "16:9", "1:1"
|
aspectRatio?: string // e.g., "16:9", "1:1"
|
||||||
required?: boolean
|
required?: boolean
|
||||||
error?: string
|
error?: string
|
||||||
allowAltText?: boolean
|
allowAltText?: boolean // @deprecated - Now using description field for alt text
|
||||||
maxFileSize?: number // MB limit
|
maxFileSize?: number // MB limit
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
helpText?: string
|
helpText?: string
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
let uploadError = $state<string | null>(null)
|
let uploadError = $state<string | null>(null)
|
||||||
let isDragOver = $state(false)
|
let isDragOver = $state(false)
|
||||||
let fileInputElement: HTMLInputElement
|
let fileInputElement: HTMLInputElement
|
||||||
let altTextValue = $state(value?.altText || '')
|
// Removed altText - using only description field
|
||||||
let descriptionValue = $state(value?.description || '')
|
let descriptionValue = $state(value?.description || '')
|
||||||
let isMediaLibraryOpen = $state(false)
|
let isMediaLibraryOpen = $state(false)
|
||||||
|
|
||||||
|
|
@ -79,11 +79,9 @@
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
if (allowAltText && altTextValue.trim()) {
|
// Removed altText upload - description is handled separately
|
||||||
formData.append('altText', altTextValue.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowAltText && descriptionValue.trim()) {
|
if (descriptionValue.trim()) {
|
||||||
formData.append('description', descriptionValue.trim())
|
formData.append('description', descriptionValue.trim())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -132,7 +130,7 @@
|
||||||
// Brief delay to show completion
|
// Brief delay to show completion
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
value = uploadedMedia
|
value = uploadedMedia
|
||||||
altTextValue = uploadedMedia.altText || ''
|
// altText removed - using description only
|
||||||
descriptionValue = uploadedMedia.description || ''
|
descriptionValue = uploadedMedia.description || ''
|
||||||
onUpload(uploadedMedia)
|
onUpload(uploadedMedia)
|
||||||
isUploading = false
|
isUploading = false
|
||||||
|
|
@ -181,35 +179,13 @@
|
||||||
// Remove uploaded image
|
// Remove uploaded image
|
||||||
function handleRemove() {
|
function handleRemove() {
|
||||||
value = null
|
value = null
|
||||||
altTextValue = ''
|
// altText removed
|
||||||
descriptionValue = ''
|
descriptionValue = ''
|
||||||
uploadError = null
|
uploadError = null
|
||||||
onRemove?.()
|
onRemove?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update alt text on server
|
// Removed handleAltTextChange - using only description
|
||||||
async function handleAltTextChange() {
|
|
||||||
if (!value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await authenticatedFetch(`/api/media/${value.id}/metadata`, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
altText: altTextValue.trim() || null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const updatedData = await response.json()
|
|
||||||
value = { ...value, altText: updatedData.altText, updatedAt: updatedData.updatedAt }
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update alt text:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDescriptionChange() {
|
async function handleDescriptionChange() {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
|
|
@ -243,7 +219,7 @@
|
||||||
// Since this is single mode, selectedMedia will be a single Media object
|
// Since this is single mode, selectedMedia will be a single Media object
|
||||||
const media = selectedMedia as Media
|
const media = selectedMedia as Media
|
||||||
value = media
|
value = media
|
||||||
altTextValue = media.altText || ''
|
// altText removed - using description only
|
||||||
descriptionValue = media.description || ''
|
descriptionValue = media.description || ''
|
||||||
onUpload(media)
|
onUpload(media)
|
||||||
}
|
}
|
||||||
|
|
@ -275,7 +251,7 @@
|
||||||
<div class="compact-image">
|
<div class="compact-image">
|
||||||
<SmartImage
|
<SmartImage
|
||||||
media={value}
|
media={value}
|
||||||
alt={value?.altText || value?.filename || 'Uploaded image'}
|
alt={value?.description || value?.filename || 'Uploaded image'}
|
||||||
containerWidth={100}
|
containerWidth={100}
|
||||||
loading="eager"
|
loading="eager"
|
||||||
{aspectRatio}
|
{aspectRatio}
|
||||||
|
|
@ -319,29 +295,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="compact-info">
|
<div class="compact-info">
|
||||||
<!-- Alt Text Input in compact mode -->
|
<!-- Description Input in compact mode -->
|
||||||
{#if allowAltText}
|
<div class="compact-metadata">
|
||||||
<div class="compact-metadata">
|
<Input
|
||||||
<Input
|
type="textarea"
|
||||||
type="text"
|
label="Description"
|
||||||
label="Alt Text"
|
bind:value={descriptionValue}
|
||||||
bind:value={altTextValue}
|
placeholder="Describe this image for accessibility and SEO"
|
||||||
placeholder="Describe this image for screen readers"
|
rows={2}
|
||||||
buttonSize="small"
|
buttonSize="small"
|
||||||
onblur={handleAltTextChange}
|
onblur={handleDescriptionChange}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<Input
|
|
||||||
type="textarea"
|
|
||||||
label="Description (Optional)"
|
|
||||||
bind:value={descriptionValue}
|
|
||||||
placeholder="Additional description or caption"
|
|
||||||
rows={2}
|
|
||||||
buttonSize="small"
|
|
||||||
onblur={handleDescriptionChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -520,24 +485,16 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Alt Text Input (only in standard mode, compact mode has it inline) -->
|
<!-- Description Input (only in standard mode, compact mode has it inline) -->
|
||||||
{#if allowAltText && hasValue && !compact}
|
{#if hasValue && !compact}
|
||||||
<div class="metadata-section">
|
<div class="metadata-section">
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
label="Alt Text"
|
|
||||||
bind:value={altTextValue}
|
|
||||||
placeholder="Describe this image for screen readers"
|
|
||||||
helpText="Help make your content accessible. Describe what's in the image."
|
|
||||||
onblur={handleAltTextChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="textarea"
|
type="textarea"
|
||||||
label="Description (Optional)"
|
label="Description"
|
||||||
bind:value={descriptionValue}
|
bind:value={descriptionValue}
|
||||||
placeholder="Additional description or caption"
|
placeholder="Describe this image for accessibility and SEO"
|
||||||
rows={2}
|
helpText="This description will be used for alt text and can also serve as a caption."
|
||||||
|
rows={3}
|
||||||
onblur={handleDescriptionChange}
|
onblur={handleDescriptionChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -559,7 +516,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Media Library Modal -->
|
<!-- Media Library Modal -->
|
||||||
<MediaLibraryModal
|
<UnifiedMediaModal
|
||||||
bind:isOpen={isMediaLibraryOpen}
|
bind:isOpen={isMediaLibraryOpen}
|
||||||
mode="single"
|
mode="single"
|
||||||
fileType="image"
|
fileType="image"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
import Input from './Input.svelte'
|
import Input from './Input.svelte'
|
||||||
import Textarea from './Textarea.svelte'
|
import Textarea from './Textarea.svelte'
|
||||||
import SmartImage from '../SmartImage.svelte'
|
import SmartImage from '../SmartImage.svelte'
|
||||||
|
import AlbumSelector from './AlbumSelector.svelte'
|
||||||
|
import AlbumIcon from '$icons/album.svg?component'
|
||||||
import { authenticatedFetch } from '$lib/admin-auth'
|
import { authenticatedFetch } from '$lib/admin-auth'
|
||||||
import type { Media } from '@prisma/client'
|
import type { Media } from '@prisma/client'
|
||||||
|
|
||||||
|
|
@ -36,19 +38,27 @@
|
||||||
>([])
|
>([])
|
||||||
let loadingUsage = $state(false)
|
let loadingUsage = $state(false)
|
||||||
|
|
||||||
|
// Album management state
|
||||||
|
let albums = $state<Array<{ id: number; title: string; slug: string }>>([])
|
||||||
|
let loadingAlbums = $state(false)
|
||||||
|
let showAlbumSelector = $state(false)
|
||||||
|
|
||||||
// EXIF toggle state
|
// EXIF toggle state
|
||||||
let showExif = $state(false)
|
let showExif = $state(false)
|
||||||
|
|
||||||
// Initialize form when media changes
|
// Initialize form when media changes
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (media) {
|
if (media) {
|
||||||
// Use description if available, otherwise fall back to altText for backwards compatibility
|
description = media.description || ''
|
||||||
description = media.description || media.altText || ''
|
|
||||||
isPhotography = media.isPhotography || false
|
isPhotography = media.isPhotography || false
|
||||||
error = ''
|
error = ''
|
||||||
successMessage = ''
|
successMessage = ''
|
||||||
showExif = false
|
showExif = false
|
||||||
loadUsage()
|
loadUsage()
|
||||||
|
// Only load albums for images
|
||||||
|
if (media.mimeType?.startsWith('image/')) {
|
||||||
|
loadAlbums()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -75,6 +85,27 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load albums the media belongs to
|
||||||
|
async function loadAlbums() {
|
||||||
|
if (!media) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingAlbums = true
|
||||||
|
|
||||||
|
// Load albums this media belongs to
|
||||||
|
const mediaResponse = await authenticatedFetch(`/api/media/${media.id}/albums`)
|
||||||
|
if (mediaResponse.ok) {
|
||||||
|
const data = await mediaResponse.json()
|
||||||
|
albums = data.albums || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading albums:', error)
|
||||||
|
albums = []
|
||||||
|
} finally {
|
||||||
|
loadingAlbums = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleClose() {
|
function handleClose() {
|
||||||
description = ''
|
description = ''
|
||||||
isPhotography = false
|
isPhotography = false
|
||||||
|
|
@ -97,8 +128,6 @@
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
// Use description for both altText and description fields
|
|
||||||
altText: description.trim() || null,
|
|
||||||
description: description.trim() || null,
|
description: description.trim() || null,
|
||||||
isPhotography: isPhotography
|
isPhotography: isPhotography
|
||||||
})
|
})
|
||||||
|
|
@ -205,11 +234,7 @@
|
||||||
<div class="image-pane">
|
<div class="image-pane">
|
||||||
{#if media.mimeType.startsWith('image/')}
|
{#if media.mimeType.startsWith('image/')}
|
||||||
<div class="image-container">
|
<div class="image-container">
|
||||||
<SmartImage
|
<SmartImage {media} alt={media.description || media.filename} class="preview-image" />
|
||||||
{media}
|
|
||||||
alt={media.description || media.altText || media.filename}
|
|
||||||
class="preview-image"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="file-placeholder">
|
<div class="file-placeholder">
|
||||||
|
|
@ -303,103 +328,109 @@
|
||||||
<span class="label">Size</span>
|
<span class="label">Size</span>
|
||||||
<span class="value">{formatFileSize(media.size)}</span>
|
<span class="value">{formatFileSize(media.size)}</span>
|
||||||
</div>
|
</div>
|
||||||
{#if media.width && media.height}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">Dimensions</span>
|
|
||||||
<span class="value">{media.width} × {media.height}px</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.dominantColor}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">Dominant Color</span>
|
|
||||||
<span class="value color-value">
|
|
||||||
<span
|
|
||||||
class="color-swatch"
|
|
||||||
style="background-color: {media.dominantColor}"
|
|
||||||
title={media.dominantColor}
|
|
||||||
></span>
|
|
||||||
{media.dominantColor}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<!-- Debug: dominantColor = {JSON.stringify(media.dominantColor)} -->
|
|
||||||
{/if}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">Uploaded</span>
|
|
||||||
<span class="value">{new Date(media.createdAt).toLocaleDateString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if media.exifData && Object.keys(media.exifData).length > 0}
|
{#if showExif}
|
||||||
{#if showExif}
|
<div class="details-data">
|
||||||
<div class="exif-data">
|
<!-- Media metadata -->
|
||||||
{#if media.exifData.camera}
|
<div class="media-metadata">
|
||||||
|
{#if media.width && media.height}
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">Camera</span>
|
<span class="label">Dimensions</span>
|
||||||
<span class="value">{media.exifData.camera}</span>
|
<span class="value">{media.width} × {media.height}px</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if media.exifData.lens}
|
{#if media.dominantColor}
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="label">Lens</span>
|
<span class="label">Dominant Color</span>
|
||||||
<span class="value">{media.exifData.lens}</span>
|
<span class="value color-value">
|
||||||
</div>
|
<span
|
||||||
{/if}
|
class="color-swatch"
|
||||||
{#if media.exifData.focalLength}
|
style="background-color: {media.dominantColor}"
|
||||||
<div class="info-item">
|
title={media.dominantColor}
|
||||||
<span class="label">Focal Length</span>
|
></span>
|
||||||
<span class="value">{media.exifData.focalLength}</span>
|
{media.dominantColor}
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.exifData.aperture}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">Aperture</span>
|
|
||||||
<span class="value">{media.exifData.aperture}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.exifData.shutterSpeed}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">Shutter Speed</span>
|
|
||||||
<span class="value">{media.exifData.shutterSpeed}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.exifData.iso}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">ISO</span>
|
|
||||||
<span class="value">{media.exifData.iso}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.exifData.dateTaken}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">Date Taken</span>
|
|
||||||
<span class="value"
|
|
||||||
>{new Date(media.exifData.dateTaken).toLocaleDateString()}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if media.exifData.coordinates}
|
|
||||||
<div class="info-item">
|
|
||||||
<span class="label">GPS</span>
|
|
||||||
<span class="value">
|
|
||||||
{media.exifData.coordinates.latitude.toFixed(6)},
|
|
||||||
{media.exifData.coordinates.longitude.toFixed(6)}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Uploaded</span>
|
||||||
|
<span class="value">{new Date(media.createdAt).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<Button
|
<!-- EXIF metadata -->
|
||||||
variant="ghost"
|
{#if media.exifData && Object.keys(media.exifData).length > 0}
|
||||||
onclick={() => (showExif = !showExif)}
|
<div class="metadata-divider"></div>
|
||||||
buttonSize="small"
|
<div class="exif-metadata">
|
||||||
fullWidth
|
{#if media.exifData.camera}
|
||||||
pill={false}
|
<div class="info-item">
|
||||||
class="exif-toggle"
|
<span class="label">Camera</span>
|
||||||
>
|
<span class="value">{media.exifData.camera}</span>
|
||||||
{showExif ? 'Hide EXIF' : 'Show EXIF'}
|
</div>
|
||||||
</Button>
|
{/if}
|
||||||
|
{#if media.exifData.lens}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Lens</span>
|
||||||
|
<span class="value">{media.exifData.lens}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if media.exifData.focalLength}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Focal Length</span>
|
||||||
|
<span class="value">{media.exifData.focalLength}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if media.exifData.aperture}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Aperture</span>
|
||||||
|
<span class="value">{media.exifData.aperture}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if media.exifData.shutterSpeed}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Shutter Speed</span>
|
||||||
|
<span class="value">{media.exifData.shutterSpeed}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if media.exifData.iso}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">ISO</span>
|
||||||
|
<span class="value">{media.exifData.iso}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if media.exifData.dateTaken}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">Date Taken</span>
|
||||||
|
<span class="value"
|
||||||
|
>{new Date(media.exifData.dateTaken).toLocaleDateString()}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if media.exifData.coordinates}
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="label">GPS</span>
|
||||||
|
<span class="value">
|
||||||
|
{media.exifData.coordinates.latitude.toFixed(6)},
|
||||||
|
{media.exifData.coordinates.longitude.toFixed(6)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onclick={() => (showExif = !showExif)}
|
||||||
|
buttonSize="small"
|
||||||
|
fullWidth
|
||||||
|
pill={false}
|
||||||
|
class="exif-toggle"
|
||||||
|
>
|
||||||
|
{showExif ? 'Hide Details' : 'Show Details'}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pane-body-content">
|
<div class="pane-body-content">
|
||||||
|
|
@ -433,7 +464,19 @@
|
||||||
|
|
||||||
<!-- Usage Tracking -->
|
<!-- Usage Tracking -->
|
||||||
<div class="usage-section">
|
<div class="usage-section">
|
||||||
<h4>Used In</h4>
|
<div class="section-header">
|
||||||
|
<h4>Used In</h4>
|
||||||
|
{#if media.mimeType?.startsWith('image/')}
|
||||||
|
<button
|
||||||
|
class="add-album-button"
|
||||||
|
onclick={() => (showAlbumSelector = true)}
|
||||||
|
title="Manage albums"
|
||||||
|
>
|
||||||
|
<AlbumIcon />
|
||||||
|
<span>Albums</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{#if loadingUsage}
|
{#if loadingUsage}
|
||||||
<div class="usage-loading">
|
<div class="usage-loading">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
|
|
@ -473,6 +516,20 @@
|
||||||
<p class="no-usage">This media file is not currently used in any content.</p>
|
<p class="no-usage">This media file is not currently used in any content.</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Albums list -->
|
||||||
|
{#if albums.length > 0}
|
||||||
|
<div class="albums-inline">
|
||||||
|
<h4>Albums</h4>
|
||||||
|
<div class="album-tags">
|
||||||
|
{#each albums as album}
|
||||||
|
<a href="/admin/albums/{album.id}/edit" class="album-tag">
|
||||||
|
{album.title}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -506,6 +563,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Album Selector Modal -->
|
||||||
|
{#if showAlbumSelector && media}
|
||||||
|
<Modal isOpen={showAlbumSelector} onClose={() => (showAlbumSelector = false)} size="medium">
|
||||||
|
<AlbumSelector
|
||||||
|
mediaId={media.id}
|
||||||
|
currentAlbums={albums}
|
||||||
|
onUpdate={(updatedAlbums) => {
|
||||||
|
albums = updatedAlbums
|
||||||
|
showAlbumSelector = false
|
||||||
|
}}
|
||||||
|
onClose={() => (showAlbumSelector = false)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -670,12 +742,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.exif-data {
|
.media-metadata,
|
||||||
|
.exif-metadata {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: $unit-3x;
|
gap: $unit-3x;
|
||||||
padding-top: $unit-3x;
|
}
|
||||||
border-top: 1px solid $grey-90;
|
|
||||||
|
.metadata-divider {
|
||||||
|
border-radius: 1px;
|
||||||
|
height: 2px;
|
||||||
|
background: $grey-80;
|
||||||
|
margin: $unit-3x 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-form {
|
.edit-form {
|
||||||
|
|
@ -763,6 +841,47 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.usage-section {
|
.usage-section {
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: $unit-2x;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $grey-20;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-album-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
|
padding: $unit-half;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: $grey-40;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $grey-95;
|
||||||
|
color: $grey-20;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg,
|
||||||
|
:global(svg) {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.usage-list {
|
.usage-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -856,6 +975,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Albums inline display
|
||||||
|
.albums-inline {
|
||||||
|
margin-top: $unit-4x;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $grey-20;
|
||||||
|
margin: 0 0 $unit-2x 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: $unit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-tag {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: $unit-half $unit-2x;
|
||||||
|
background: $grey-95;
|
||||||
|
border: 1px solid $grey-90;
|
||||||
|
border-radius: 20px;
|
||||||
|
color: $grey-20;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $grey-90;
|
||||||
|
border-color: $grey-85;
|
||||||
|
color: $grey-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pane-footer {
|
.pane-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,8 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isOpen}
|
{#if isOpen}
|
||||||
<div class="modal-backdrop" on:click={handleBackdropClick} transition:fade={{ duration: 200 }}>
|
<div class="modal-backdrop" on:click={handleBackdropClick} transition:fade={{ duration: 150 }}>
|
||||||
<div
|
<div class="modal {modalClass}" on:click|stopPropagation transition:fade={{ duration: 150 }}>
|
||||||
class="modal {modalClass}"
|
|
||||||
on:click|stopPropagation
|
|
||||||
transition:fade={{ duration: 200, delay: 50 }}
|
|
||||||
>
|
|
||||||
{#if showCloseButton}
|
{#if showCloseButton}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import UniverseComposer from './UniverseComposer.svelte'
|
import InlineComposerModal from './InlineComposerModal.svelte'
|
||||||
import Button from './Button.svelte'
|
import Button from './Button.svelte'
|
||||||
import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<UniverseComposer
|
<InlineComposerModal
|
||||||
bind:isOpen={showComposer}
|
bind:isOpen={showComposer}
|
||||||
initialPostType={selectedType}
|
initialPostType={selectedType}
|
||||||
on:close={handleComposerClose}
|
on:close={handleComposerClose}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue