114
README.md
|
|
@ -22,16 +22,19 @@ Personal portfolio website built with SvelteKit featuring a content management s
|
|||
## Development
|
||||
|
||||
Install dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Start development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Build for production:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
|
@ -39,10 +42,12 @@ npm run build
|
|||
## Environment Variables
|
||||
|
||||
Required environment variables:
|
||||
|
||||
- `LASTFM_API_KEY` - Last.fm API key for music data
|
||||
- `REDIS_URL` - Redis connection URL for caching
|
||||
|
||||
Optional environment variables:
|
||||
|
||||
- `DEBUG` - Enable debug logging for specific categories (e.g., `DEBUG=music` for music-related logs)
|
||||
|
||||
## Commands
|
||||
|
|
@ -52,4 +57,111 @@ Optional environment variables:
|
|||
- `npm run preview` - Preview production build
|
||||
- `npm run check` - Type check with svelte-check
|
||||
- `npm run lint` - Check formatting and linting
|
||||
- `npm run format` - Auto-format code with prettier
|
||||
- `npm run format` - Auto-format code with prettier
|
||||
|
||||
## Database Management
|
||||
|
||||
### Quick Start
|
||||
|
||||
Sync remote production database to local development:
|
||||
|
||||
```bash
|
||||
# This backs up both databases first, then copies remote to local
|
||||
npm run db:backup:sync
|
||||
```
|
||||
|
||||
### Prerequisites
|
||||
|
||||
1. PostgreSQL client tools must be installed (`pg_dump`, `psql`)
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install postgresql
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install postgresql-client
|
||||
```
|
||||
|
||||
2. Set environment variables in `.env` or `.env.local`:
|
||||
|
||||
```bash
|
||||
# Required for local database operations
|
||||
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
|
||||
|
||||
# Required for remote database operations (use one of these)
|
||||
REMOTE_DATABASE_URL="postgresql://user:password@remote-host:5432/dbname"
|
||||
DATABASE_URL_PRODUCTION="postgresql://user:password@remote-host:5432/dbname"
|
||||
```
|
||||
|
||||
### Backup Commands
|
||||
|
||||
```bash
|
||||
# Backup local database
|
||||
npm run db:backup:local
|
||||
|
||||
# Backup remote database
|
||||
npm run db:backup:remote
|
||||
|
||||
# Sync remote to local (recommended for daily development)
|
||||
npm run db:backup:sync
|
||||
|
||||
# List all backups
|
||||
npm run db:backups
|
||||
```
|
||||
|
||||
### Restore Commands
|
||||
|
||||
```bash
|
||||
# Restore a backup to local database (interactive)
|
||||
npm run db:restore
|
||||
|
||||
# Restore specific backup to local
|
||||
npm run db:restore ./backups/backup_file.sql.gz
|
||||
|
||||
# Restore to remote (requires typing "RESTORE REMOTE" for safety)
|
||||
npm run db:restore ./backups/backup_file.sql.gz remote
|
||||
```
|
||||
|
||||
### Common Workflows
|
||||
|
||||
#### Daily Development
|
||||
|
||||
Start your day by syncing the production database to local:
|
||||
|
||||
```bash
|
||||
npm run db:backup:sync
|
||||
```
|
||||
|
||||
#### Before Deploying Schema Changes
|
||||
|
||||
Always backup the remote database:
|
||||
|
||||
```bash
|
||||
npm run db:backup:remote
|
||||
```
|
||||
|
||||
#### Recover from Mistakes
|
||||
|
||||
```bash
|
||||
# See available backups
|
||||
npm run db:backups
|
||||
|
||||
# Restore a specific backup
|
||||
npm run db:restore ./backups/local_20240615_143022.sql.gz
|
||||
```
|
||||
|
||||
### Backup Storage
|
||||
|
||||
All backups are stored in `./backups/` with timestamps:
|
||||
|
||||
- Local: `local_YYYYMMDD_HHMMSS.sql.gz`
|
||||
- Remote: `remote_YYYYMMDD_HHMMSS.sql.gz`
|
||||
|
||||
### Safety Features
|
||||
|
||||
1. **Automatic backups** before sync operations
|
||||
2. **Confirmation prompts** for all destructive operations
|
||||
3. **Extra protection** for remote restore (requires typing full phrase)
|
||||
4. **Compressed storage** with gzip
|
||||
5. **Timestamped filenames** prevent overwrites
|
||||
6. **Automatic migrations** after local restore
|
||||
|
|
|
|||
206
SVG_ANALYSIS_REPORT.md
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# SVG Usage Analysis Report
|
||||
|
||||
## Summary
|
||||
|
||||
This analysis examines SVG usage patterns in the Svelte 5 codebase to identify optimization opportunities, inconsistencies, and unused assets.
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Inline SVGs vs. Imported SVGs
|
||||
|
||||
**Inline SVGs Found:**
|
||||
|
||||
- **Close/X buttons**: Found in 7+ components with identical SVG code
|
||||
- `admin/Modal.svelte`
|
||||
- `admin/UnifiedMediaModal.svelte`
|
||||
- `admin/MediaInput.svelte`
|
||||
- `admin/AlbumSelectorModal.svelte`
|
||||
- `admin/GalleryManager.svelte`
|
||||
- `admin/MediaDetailsModal.svelte`
|
||||
- `Lightbox.svelte`
|
||||
- **Loading spinners**: Found in 2+ components
|
||||
|
||||
- `admin/Button.svelte`
|
||||
- `admin/ImageUploader.svelte`
|
||||
- `admin/GalleryUploader.svelte`
|
||||
|
||||
- **Navigation arrows**: Found in `PhotoLightbox.svelte`
|
||||
- **Lock icon**: Found in `LabCard.svelte`
|
||||
- **External link icon**: Found in `LabCard.svelte`
|
||||
|
||||
### 2. SVG Import Patterns
|
||||
|
||||
**Consistent patterns using aliases:**
|
||||
|
||||
```svelte
|
||||
// Good - using $icons alias import ArrowLeft from '$icons/arrow-left.svg' import ChevronDownIcon
|
||||
from '$icons/chevron-down.svg' // Component imports with ?component import PhotosIcon from
|
||||
'$icons/photos.svg?component' import ViewSingleIcon from '$icons/view-single.svg?component' // Raw
|
||||
imports import ChevronDownIcon from '$icons/chevron-down.svg?raw'
|
||||
```
|
||||
|
||||
### 3. Unused SVG Files
|
||||
|
||||
**Unused icons in `/src/assets/icons/`:**
|
||||
|
||||
- `dashboard.svg`
|
||||
- `metadata.svg`
|
||||
|
||||
**Unused illustrations in `/src/assets/illos/`:**
|
||||
|
||||
- `jedmund-blink.svg`
|
||||
- `jedmund-headphones.svg`
|
||||
- `jedmund-listening-downbeat.svg`
|
||||
- `jedmund-listening.svg`
|
||||
- `jedmund-open.svg`
|
||||
- `jedmund-signing-downbeat.svg`
|
||||
- `jedmund-singing.svg`
|
||||
- `logo-figma.svg`
|
||||
- `logo-maitsu.svg`
|
||||
- `logo-pinterest.svg`
|
||||
- `logo-slack.svg`
|
||||
|
||||
### 4. Duplicate SVG Definitions
|
||||
|
||||
**Close/X Button SVG** (appears 7+ times):
|
||||
|
||||
```svg
|
||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
```
|
||||
|
||||
**Loading Spinner SVG** (appears 3+ times):
|
||||
|
||||
```svg
|
||||
<svg class="spinner" width="24" height="24" viewBox="0 0 24 24">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="25" stroke-dashoffset="25" stroke-linecap="round">
|
||||
<animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="1s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
```
|
||||
|
||||
### 5. SVGs That Could Be Componentized
|
||||
|
||||
1. **Close Button**: Used across multiple modals and components
|
||||
2. **Loading Spinner**: Used in buttons and upload components
|
||||
3. **Navigation Arrows**: Used in lightbox and potentially other navigation
|
||||
4. **Status Icons**: Lock, external link, eye icons in LabCard
|
||||
|
||||
## Recommendations
|
||||
|
||||
### 1. Create Reusable Icon Components
|
||||
|
||||
**Option A: Create individual icon components**
|
||||
|
||||
```svelte
|
||||
<!-- $lib/components/icons/CloseIcon.svelte -->
|
||||
<script>
|
||||
let { size = 24, class: className = '' } = $props()
|
||||
</script>
|
||||
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" class={className}>
|
||||
<path d="M6 6L18 18M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
||||
</svg>
|
||||
```
|
||||
|
||||
**Option B: Create an Icon component with name prop**
|
||||
|
||||
```svelte
|
||||
<!-- $lib/components/Icon.svelte -->
|
||||
<script>
|
||||
import CloseIcon from '$icons/close.svg'
|
||||
import LoadingIcon from '$icons/loading.svg'
|
||||
// ... other imports
|
||||
|
||||
let { name, size = 24, class: className = '' } = $props()
|
||||
|
||||
const icons = {
|
||||
close: CloseIcon,
|
||||
loading: LoadingIcon
|
||||
// ... other icons
|
||||
}
|
||||
|
||||
const IconComponent = $derived(icons[name])
|
||||
</script>
|
||||
|
||||
{#if IconComponent}
|
||||
<IconComponent {size} class={className} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
### 2. Extract Inline SVGs to Files
|
||||
|
||||
Create new SVG files for commonly used inline SVGs:
|
||||
|
||||
- `/src/assets/icons/close.svg`
|
||||
- `/src/assets/icons/loading.svg`
|
||||
- `/src/assets/icons/external-link.svg`
|
||||
- `/src/assets/icons/lock.svg`
|
||||
- `/src/assets/icons/eye-off.svg`
|
||||
|
||||
### 3. Clean Up Unused Assets
|
||||
|
||||
Remove the following unused files to reduce bundle size:
|
||||
|
||||
- All unused illustration files (11 files)
|
||||
- Unused icon files (2 files)
|
||||
|
||||
### 4. Standardize Import Methods
|
||||
|
||||
Establish a consistent pattern:
|
||||
|
||||
- Use `?component` for SVGs used as Svelte components
|
||||
- Use direct imports for SVGs used as images
|
||||
- Avoid `?raw` imports unless necessary
|
||||
|
||||
### 5. Create a Loading Component
|
||||
|
||||
```svelte
|
||||
<!-- $lib/components/LoadingSpinner.svelte -->
|
||||
<script>
|
||||
let { size = 24, class: className = '' } = $props()
|
||||
</script>
|
||||
|
||||
<svg class="loading-spinner {className}" width={size} height={size} viewBox="0 0 24 24">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-dasharray="25"
|
||||
stroke-dashoffset="25"
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 12 12"
|
||||
to="360 12 12"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
.loading-spinner {
|
||||
color: currentColor;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Benefits of These Changes
|
||||
|
||||
1. **Reduced code duplication**: Eliminate 20+ duplicate SVG definitions
|
||||
2. **Smaller bundle size**: Remove 13 unused SVG files
|
||||
3. **Better maintainability**: Centralized icon management
|
||||
4. **Consistent styling**: Easier to apply consistent styles to all icons
|
||||
5. **Type safety**: With proper component props
|
||||
6. **Performance**: Less inline SVG parsing, better caching
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
1. **High Priority**: Extract and componentize duplicate inline SVGs (close button, loading spinner)
|
||||
2. **Medium Priority**: Remove unused SVG files
|
||||
3. **Low Priority**: Standardize all import patterns and create comprehensive icon system
|
||||
18443
package-lock.json
generated
|
|
@ -59,6 +59,7 @@
|
|||
"type": "module",
|
||||
"dependencies": {
|
||||
"@aarkue/tiptap-math-extension": "^1.3.6",
|
||||
"@floating-ui/dom": "^1.7.1",
|
||||
"@prisma/client": "^6.8.2",
|
||||
"@sveltejs/adapter-node": "^5.2.0",
|
||||
"@tiptap/core": "^2.12.0",
|
||||
|
|
@ -87,6 +88,7 @@
|
|||
"@tiptap/starter-kit": "^2.12.0",
|
||||
"@tiptap/suggestion": "^2.12.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"@types/leaflet": "^1.9.18",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/redis": "^4.0.10",
|
||||
"@types/steamapi": "^2.2.5",
|
||||
|
|
@ -98,6 +100,7 @@
|
|||
"ioredis": "^5.4.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"katex": "^0.16.22",
|
||||
"leaflet": "^1.9.4",
|
||||
"lowlight": "^3.3.0",
|
||||
"lucide-svelte": "^0.511.0",
|
||||
"marked": "^15.0.12",
|
||||
|
|
@ -108,10 +111,12 @@
|
|||
"redis": "^4.7.0",
|
||||
"sharp": "^0.34.2",
|
||||
"steamapi": "^3.0.11",
|
||||
"svelte-awesome-color-picker": "^4.0.2",
|
||||
"svelte-bricks": "^0.3.2",
|
||||
"svelte-infinite": "^0.5.0",
|
||||
"svelte-medium-image-zoom": "^0.2.6",
|
||||
"svelte-portal": "^2.2.1",
|
||||
"svelte-sonner": "^1.0.5",
|
||||
"svelte-tiptap": "^2.1.0",
|
||||
"svgo": "^3.3.2",
|
||||
"tinyduration": "^3.3.1",
|
||||
|
|
|
|||
210
prd/PRD-album-system-redesign.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
# Product Requirements Document: Album System Redesign
|
||||
|
||||
## Summary of Changes
|
||||
|
||||
This PRD outlines a comprehensive redesign of the album system to transform albums from simple photo containers into rich photographic stories with enhanced content capabilities. The key changes include:
|
||||
|
||||
1. **Many-to-Many Photo-Album Relationships**: Enable a single photo to belong to multiple albums, providing greater flexibility in content organization
|
||||
2. **Enhanced Photo Permalinks**: Display all associated albums on individual photo pages for better context
|
||||
3. **Refined Collection Views**: Remove albums from public collection views while maintaining permalink access
|
||||
4. **Rich Album Composer**: Implement an essay-style composer for albums allowing mixed text and photo content
|
||||
5. **Geo-Location Features**: Add embedded map cards with point-of-interest markers for location-based storytelling
|
||||
|
||||
## Task List by Phase
|
||||
|
||||
### Additional Completed Tasks
|
||||
|
||||
- [x] Add geolocation capability to Edra editor (allows adding maps to any rich text content)
|
||||
|
||||
### Phase 1: Data Model Migration
|
||||
|
||||
- [x] Create database migration to remove direct photo-album relationship
|
||||
- [x] Update schema to ensure AlbumMedia join table supports many-to-many relationships
|
||||
- [x] Add album content field to store rich text/media composition
|
||||
- [x] Create geo-location schema for map embedding (lat/lng, POI data)
|
||||
- [x] Write data migration script to preserve existing album-photo relationships
|
||||
- [x] Update all API endpoints to use new data model
|
||||
|
||||
### Phase 2: Photo Management Updates
|
||||
|
||||
- [x] Update photo permalink page to display associated albums
|
||||
- [x] Create UI component for album badges/links on photo pages
|
||||
- [x] Update photo API to fetch album associations
|
||||
- [x] Modify admin photo editor to manage album associations
|
||||
- [x] Create album selector component for photo editing
|
||||
|
||||
### Phase 3: Album Composer Development
|
||||
|
||||
- [x] Create new AlbumComposer component based on UniverseComposer
|
||||
- [x] Implement rich text editor with photo insertion capabilities
|
||||
- [x] Add photo browser/selector for inserting album photos
|
||||
- [x] Create preview mode for composed album content
|
||||
- [x] Implement auto-save functionality
|
||||
- [ ] Add version history/drafts support
|
||||
|
||||
### Phase 4: Geo-Location Features
|
||||
|
||||
- [x] Design geo-card component with map embed
|
||||
- [x] Integrate mapping library (e.g., Mapbox, Leaflet)
|
||||
- [x] Create POI marker system with customizable popovers
|
||||
- [x] Add geo-location picker in composer
|
||||
- [x] Implement responsive map sizing
|
||||
- [x] Add fallback for non-JS environments
|
||||
|
||||
### Phase 5: Frontend Updates
|
||||
|
||||
- [ ] Update album permalink pages to render composed content
|
||||
- [ ] Remove albums from public collection views
|
||||
- [ ] Update navigation/menus to reflect new album structure
|
||||
- [ ] Implement new album listing page design
|
||||
- [ ] Add SEO metadata for composed albums
|
||||
- [ ] Update Universe feed album cards
|
||||
|
||||
### Phase 6: Admin Interface Updates
|
||||
|
||||
- [ ] Replace current AlbumForm with new composer interface
|
||||
- [ ] Update album list view in admin
|
||||
- [ ] Add bulk operations for album-photo associations
|
||||
- [ ] Create album analytics dashboard
|
||||
- [ ] Implement permission controls for album editing
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Technical Architecture
|
||||
|
||||
1. **Database Structure**:
|
||||
|
||||
```prisma
|
||||
model Album {
|
||||
id String @id
|
||||
slug String @unique
|
||||
title String
|
||||
content Json? // Rich content blocks
|
||||
geoLocations GeoLocation[]
|
||||
media AlbumMedia[]
|
||||
// ... existing fields
|
||||
}
|
||||
|
||||
model Media {
|
||||
id String @id
|
||||
albums AlbumMedia[]
|
||||
// ... existing fields
|
||||
}
|
||||
|
||||
model AlbumMedia {
|
||||
albumId String
|
||||
mediaId String
|
||||
displayOrder Int
|
||||
album Album @relation(...)
|
||||
media Media @relation(...)
|
||||
|
||||
@@id([albumId, mediaId])
|
||||
}
|
||||
|
||||
model GeoLocation {
|
||||
id String @id
|
||||
albumId String
|
||||
latitude Float
|
||||
longitude Float
|
||||
title String
|
||||
description String?
|
||||
album Album @relation(...)
|
||||
}
|
||||
```
|
||||
|
||||
2. **Content Block Structure**:
|
||||
|
||||
```typescript
|
||||
type ContentBlock =
|
||||
| { type: 'text'; content: string }
|
||||
| { type: 'photo'; mediaId: string; caption?: string }
|
||||
| { type: 'photoGrid'; mediaIds: string[]; layout: 'masonry' | 'grid' }
|
||||
| { type: 'geoCard'; locationId: string }
|
||||
```
|
||||
|
||||
3. **API Updates**:
|
||||
- `GET /api/media/[id]/albums` - Get all albums for a photo
|
||||
- `PUT /api/albums/[id]/content` - Update album composed content
|
||||
- `POST /api/albums/[id]/locations` - Add geo-location
|
||||
- `PUT /api/media/[id]/albums` - Update photo's album associations
|
||||
|
||||
### Migration Strategy
|
||||
|
||||
1. **Phase 1**: Deploy database changes with backward compatibility
|
||||
2. **Phase 2**: Update APIs to support both old and new patterns
|
||||
3. **Phase 3**: Migrate frontend components incrementally
|
||||
4. **Phase 4**: Run data migration to new structure
|
||||
5. **Phase 5**: Remove deprecated code and fields
|
||||
|
||||
## Possible Challenges
|
||||
|
||||
### Technical Challenges
|
||||
|
||||
1. **Data Migration Complexity**:
|
||||
|
||||
- Risk of data loss during migration from direct relationships to join table
|
||||
- Need to handle orphaned photos and maintain referential integrity
|
||||
- Performance impact during migration on large datasets
|
||||
|
||||
2. **Performance Considerations**:
|
||||
|
||||
- Many-to-many queries could impact page load times
|
||||
- Rich content rendering may require optimization
|
||||
- Map embeds could slow down initial page loads
|
||||
|
||||
3. **Content Editor Complexity**:
|
||||
|
||||
- Building a robust WYSIWYG editor with photo insertion
|
||||
- Handling drag-and-drop reordering of content blocks
|
||||
- Ensuring responsive preview matches final output
|
||||
|
||||
4. **Geo-Location Integration**:
|
||||
- Map API rate limits and costs
|
||||
- Offline/fallback handling for maps
|
||||
- Privacy concerns with location data
|
||||
|
||||
### User Experience Challenges
|
||||
|
||||
1. **Migration Path for Existing Users**:
|
||||
|
||||
- Users may be confused by the new album structure
|
||||
- Need clear communication about changes
|
||||
- Potential breaking of bookmarked album URLs
|
||||
|
||||
2. **Content Creation Learning Curve**:
|
||||
|
||||
- More complex interface compared to simple photo upload
|
||||
- Need intuitive UI for mixed content creation
|
||||
- Balancing power vs simplicity
|
||||
|
||||
3. **Navigation Changes**:
|
||||
- Albums no longer in collection views may confuse users
|
||||
- Need alternative discovery methods for albums
|
||||
- Maintaining SEO value of existing album pages
|
||||
|
||||
### Operational Challenges
|
||||
|
||||
1. **Storage and Bandwidth**:
|
||||
|
||||
- Rich content will increase storage needs
|
||||
- Map tiles and assets increase bandwidth usage
|
||||
- Need efficient caching strategy
|
||||
|
||||
2. **Content Moderation**:
|
||||
|
||||
- More complex content requires better moderation tools
|
||||
- Geo-location data needs privacy controls
|
||||
- Version control for composed content
|
||||
|
||||
3. **Backward Compatibility**:
|
||||
- API versioning to support existing integrations
|
||||
- Gradual deprecation of old endpoints
|
||||
- Supporting old album URLs with redirects
|
||||
|
||||
### Mitigation Strategies
|
||||
|
||||
1. **Phased Rollout**: Deploy features incrementally with feature flags
|
||||
2. **Comprehensive Testing**: Unit, integration, and end-to-end tests for all changes
|
||||
3. **Performance Monitoring**: Track query performance and optimize hot paths
|
||||
4. **User Documentation**: Create guides and tutorials for new features
|
||||
5. **Rollback Plan**: Maintain ability to revert to previous system if needed
|
||||
270
prd/PRD-codebase-cleanup-refactoring.md
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
# PRD: Codebase Cleanup and Refactoring
|
||||
|
||||
**Date**: December 26, 2025
|
||||
**Author**: Claude Code
|
||||
**Status**: Draft
|
||||
**Priority**: High
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This PRD outlines a comprehensive cleanup and refactoring plan for the jedmund-svelte Svelte 5 codebase. The analysis has identified significant opportunities to reduce code complexity, eliminate duplication, and improve maintainability through systematic refactoring.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Simplify overengineered components** - Break down complex components into smaller, focused units
|
||||
2. **Eliminate dead code** - Remove unused components, functions, and imports
|
||||
3. **Reduce code duplication** - Extract common patterns into reusable components and utilities
|
||||
4. **Standardize styling** - Convert hardcoded values to CSS variables and create consistent patterns
|
||||
5. **Optimize SVG usage** - Remove unused SVGs and create reusable icon components
|
||||
|
||||
## Key Findings
|
||||
|
||||
### 1. Overengineered Components
|
||||
|
||||
- **EnhancedComposer** (1,347 lines) - Handles too many responsibilities
|
||||
- **LastFM Stream Server** (625 lines) - Complex data transformations that could be simplified
|
||||
- **Multiple Media Modals** - Overlapping functionality across 3+ modal components
|
||||
- **Complex State Management** - Components with 10-20 state variables
|
||||
|
||||
### 2. Unused Code
|
||||
|
||||
- 5 unused components (Squiggly, PhotoLightbox, Pill, SVGHoverEffect, MusicPreview)
|
||||
- 13 unused SVG files (2 icons, 11 illustrations)
|
||||
- Minimal commented-out code (good!)
|
||||
- 1 potentially unused API endpoint (/api/health)
|
||||
|
||||
### 3. DRY Violations
|
||||
|
||||
- **Photo Grid Components** - 3 nearly identical components
|
||||
- **Modal Components** - Duplicate backdrop and positioning logic
|
||||
- **Dropdown Components** - Repeated dropdown patterns
|
||||
- **Form Components** - Overlapping FormField and FormFieldWrapper
|
||||
- **Segmented Controllers** - Duplicate animation and positioning logic
|
||||
|
||||
### 4. Hardcoded Values
|
||||
|
||||
- **Colors**: 200+ hardcoded hex/rgba values instead of using existing variables
|
||||
- **Spacing**: 1,000+ hardcoded pixel values instead of using `$unit` system
|
||||
- **Z-indexes**: 60+ hardcoded z-index values without consistent scale
|
||||
- **Animations**: Hardcoded durations instead of using constants
|
||||
- **Border radius**: Not using existing `$corner-radius-*` variables
|
||||
|
||||
### 5. SVG Issues
|
||||
|
||||
- 7+ duplicate inline close button SVGs
|
||||
- 3+ duplicate loading spinner SVGs
|
||||
- Inconsistent import patterns
|
||||
- Inline SVGs that should be componentized
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Phase 1: Quick Wins (Week 1)
|
||||
|
||||
Focus on low-risk, high-impact changes that don't require architectural modifications.
|
||||
|
||||
- [x] **Remove unused components** (5 components)
|
||||
|
||||
- [x] Delete `/src/lib/components/Squiggly.svelte`
|
||||
- [x] Delete `/src/lib/components/PhotoLightbox.svelte`
|
||||
- [x] Delete `/src/lib/components/Pill.svelte`
|
||||
- [x] Delete `/src/lib/components/SVGHoverEffect.svelte`
|
||||
- [x] Delete `/src/lib/components/MusicPreview.svelte`
|
||||
|
||||
- [x] **Remove unused SVG files** (13 files)
|
||||
|
||||
- [x] Delete unused icons: `dashboard.svg`, `metadata.svg`
|
||||
- [x] Delete unused illustrations (11 files - see SVG analysis report)
|
||||
|
||||
- [x] **Clean up dead code**
|
||||
- [x] Remove commented `getWeeklyAlbumChart` line in `/src/routes/api/lastfm/+server.ts`
|
||||
- [x] Address TODO in `/src/lib/server/api-utils.ts` about authentication (noted for future work)
|
||||
|
||||
### Phase 2: CSS Variable Standardization (Week 2)
|
||||
|
||||
Create a consistent design system by extracting hardcoded values.
|
||||
|
||||
- [x] **Create z-index system**
|
||||
|
||||
- [x] Create `src/assets/styles/_z-index.scss` with constants
|
||||
- [x] Replace 60+ hardcoded z-index values
|
||||
|
||||
- [x] **Extract color variables**
|
||||
|
||||
- [x] Add missing color variables for frequently used colors
|
||||
- [x] Replace 200+ hardcoded hex/rgba values (replaced most common colors)
|
||||
- [x] Create shadow/overlay variables for rgba values
|
||||
|
||||
- [x] **Standardize spacing**
|
||||
|
||||
- [x] Add missing unit multipliers (added `$unit-7x` through `$unit-19x` and common pixel values)
|
||||
- [x] Replace 1,000+ hardcoded pixel values with unit variables (replaced in key components)
|
||||
|
||||
- [x] **Define animation constants**
|
||||
- [x] Create transition/animation duration variables
|
||||
- [x] Replace hardcoded duration values (replaced in key components)
|
||||
|
||||
### Phase 3: Component Refactoring (Weeks 3-4) ✅
|
||||
|
||||
Refactor components to reduce duplication and complexity.
|
||||
|
||||
- [x] **Create base components**
|
||||
|
||||
- [x] Extract `BaseModal` component for shared modal logic
|
||||
- [x] Create `BaseDropdown` for dropdown patterns
|
||||
- [x] Merge `FormField` and `FormFieldWrapper`
|
||||
- [x] Create `BaseSegmentedController` for shared logic
|
||||
|
||||
- [x] **Refactor photo grids**
|
||||
|
||||
- [x] Create unified `PhotoGrid` component with `columns` prop
|
||||
- [x] Remove 3 duplicate grid components
|
||||
- [x] Use composition for layout variations
|
||||
|
||||
- [x] **Componentize inline SVGs**
|
||||
|
||||
- [x] Create `CloseButton` icon component
|
||||
- [x] Create `LoadingSpinner` component (already existed)
|
||||
- [x] Create `NavigationArrow` components (using existing arrow SVGs)
|
||||
- [x] Extract other repeated inline SVGs (FileIcon, CopyIcon)
|
||||
|
||||
- [x] **Additional refactoring completed**
|
||||
- [x] Convert slot syntax to Svelte 5 snippets
|
||||
- [x] Fix editor content loading issues
|
||||
- [x] Improve editor design and spacing
|
||||
- [x] Fix drag handle positioning and functionality
|
||||
- [x] Create floating toolbar with glassmorphism
|
||||
- [x] Implement enhanced bubble menu with formatting tools
|
||||
- [x] Add text style dropdown and color pickers
|
||||
- [x] Disable toolbar in favor of bubble menu
|
||||
|
||||
### Phase 4: Complex Refactoring (Weeks 5-6)
|
||||
|
||||
Tackle the most complex components and patterns.
|
||||
|
||||
- [x] **Refactor EnhancedComposer**
|
||||
|
||||
- [x] Split into focused sub-components
|
||||
- [x] Extract toolbar component
|
||||
- [x] Separate media management
|
||||
- [x] Create dedicated link editor
|
||||
- [x] Reduce state variables from 20+ to <10
|
||||
|
||||
- [x] **Simplify LastFM Stream Server** ✅
|
||||
|
||||
- [x] Extract data transformation utilities
|
||||
- [x] Created `lastfmTransformers.ts` for image and data transformations
|
||||
- [x] Simplify "now playing" detection algorithm
|
||||
- [x] Created `nowPlayingDetector.ts` with cleaner detection logic
|
||||
- [x] Reduce state tracking duplication
|
||||
- [x] Created `lastfmStreamManager.ts` to centralize state management
|
||||
- [x] Create separate modules for complex logic
|
||||
- [x] Created `albumEnricher.ts` for album data enrichment
|
||||
- [x] Reduced stream server from 625 lines to 115 lines (81% reduction)
|
||||
|
||||
- [x] **Consolidate media modals** ✅
|
||||
- [x] Extract reusable components from existing modals:
|
||||
- [x] Create MediaGrid component (~150 lines)
|
||||
- [x] Create FileUploadZone component (~120 lines)
|
||||
- [x] Create FilePreviewList component (~100 lines)
|
||||
- [x] Create MediaMetadataPanel component (~150 lines)
|
||||
- [x] Create MediaUsageList component (~80 lines)
|
||||
- [x] Create shared utilities:
|
||||
- [x] mediaHelpers.ts (formatFileSize, getFileType, etc.)
|
||||
- [x] useMediaSelection composable
|
||||
- [x] Update existing modals to use new components
|
||||
- [x] Eliminate ~750-800 lines of duplicate code
|
||||
|
||||
### Phase 5: Architecture & Utilities (Week 7)
|
||||
|
||||
Improve overall architecture and create shared utilities.
|
||||
|
||||
- [ ] **Create shared utilities**
|
||||
|
||||
- [ ] API client with consistent error handling
|
||||
- [ ] CSS mixins for common patterns
|
||||
- [ ] Media handling utilities
|
||||
- [ ] Form validation utilities
|
||||
|
||||
- [ ] **Standardize patterns**
|
||||
- [ ] Create middleware for API routes
|
||||
- [ ] Implement consistent error handling
|
||||
- [ ] Standardize data fetching patterns
|
||||
- [ ] Create shared animation definitions
|
||||
|
||||
### Phase 6: Testing & Documentation (Week 8)
|
||||
|
||||
Ensure changes don't break functionality and document new patterns.
|
||||
|
||||
- [ ] **Testing**
|
||||
|
||||
- [ ] Run full build and type checking
|
||||
- [ ] Test all refactored components
|
||||
- [ ] Verify no regressions in functionality
|
||||
- [ ] Check bundle size improvements
|
||||
|
||||
- [ ] **Documentation**
|
||||
- [ ] Update component documentation
|
||||
- [ ] Document new patterns and utilities
|
||||
- [ ] Update Storybook stories for new components
|
||||
- [ ] Create migration guide for team
|
||||
|
||||
## Success Metrics
|
||||
|
||||
1. **Code Reduction**
|
||||
|
||||
- Target: 20-30% reduction in total lines of code
|
||||
- Eliminate 1,000+ instances of code duplication
|
||||
|
||||
2. **Component Simplification**
|
||||
|
||||
- No component larger than 500 lines
|
||||
- Average component size under 200 lines
|
||||
|
||||
3. **Design System Consistency**
|
||||
|
||||
- Zero hardcoded colors in components
|
||||
- All spacing using design tokens
|
||||
- Consistent z-index scale
|
||||
|
||||
4. **Bundle Size**
|
||||
|
||||
- 10-15% reduction in JavaScript bundle size
|
||||
- Removal of unused assets
|
||||
|
||||
5. **Developer Experience**
|
||||
- Faster build times
|
||||
- Easier component discovery
|
||||
- Reduced cognitive load
|
||||
|
||||
## Risk Mitigation
|
||||
|
||||
1. **Regression Testing**
|
||||
|
||||
- Test each phase thoroughly before moving to next
|
||||
- Keep backups of original components during refactoring
|
||||
- Use feature flags for gradual rollout if needed
|
||||
|
||||
2. **Performance Impact**
|
||||
|
||||
- Monitor bundle size after each phase
|
||||
- Profile component render performance
|
||||
- Ensure no performance regressions
|
||||
|
||||
3. **Team Coordination**
|
||||
- Communicate changes clearly
|
||||
- Update documentation as you go
|
||||
- Create clear migration paths
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
Each phase should be implemented as a separate git branch with the ability to revert if issues arise. Keep the old components available until the new ones are fully tested and stable.
|
||||
|
||||
## Appendix
|
||||
|
||||
- [SVG Analysis Report](/Users/justin/Developer/Personal/jedmund-svelte/SVG_ANALYSIS_REPORT.md) - Detailed SVG usage analysis
|
||||
- [Component Analysis](#) - Detailed breakdown of component complexity
|
||||
- [CSS Variable Audit](#) - Complete list of hardcoded values to replace
|
||||
|
||||
---
|
||||
|
||||
**Next Steps**: Review this PRD and approve the implementation timeline. Each phase can be tracked using the checkboxes above.
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Album" ADD COLUMN "content" JSONB;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "GeoLocation" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"albumId" INTEGER NOT NULL,
|
||||
"latitude" DOUBLE PRECISION NOT NULL,
|
||||
"longitude" DOUBLE PRECISION NOT NULL,
|
||||
"title" VARCHAR(255) NOT NULL,
|
||||
"description" TEXT,
|
||||
"markerColor" VARCHAR(7),
|
||||
"order" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "GeoLocation_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GeoLocation_albumId_idx" ON "GeoLocation"("albumId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "GeoLocation" ADD CONSTRAINT "GeoLocation_albumId_fkey" FOREIGN KEY ("albumId") REFERENCES "Album"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
-- Step 1: Migrate any remaining direct photo-album relationships to AlbumMedia
|
||||
INSERT INTO "AlbumMedia" ("albumId", "mediaId", "displayOrder", "createdAt")
|
||||
SELECT DISTINCT
|
||||
p."albumId",
|
||||
p."mediaId",
|
||||
p."displayOrder",
|
||||
p."createdAt"
|
||||
FROM "Photo" p
|
||||
WHERE p."albumId" IS NOT NULL
|
||||
AND p."mediaId" IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM "AlbumMedia" am
|
||||
WHERE am."albumId" = p."albumId"
|
||||
AND am."mediaId" = p."mediaId"
|
||||
);
|
||||
|
||||
-- Step 2: Drop the foreign key constraint
|
||||
ALTER TABLE "Photo" DROP CONSTRAINT IF EXISTS "Photo_albumId_fkey";
|
||||
|
||||
-- Step 3: Drop the albumId column from Photo table
|
||||
ALTER TABLE "Photo" DROP COLUMN IF EXISTS "albumId";
|
||||
|
||||
-- Step 4: Drop the index on albumId
|
||||
DROP INDEX IF EXISTS "Photo_albumId_idx";
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
|
@ -10,181 +7,172 @@ datasource db {
|
|||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
// Projects table (for /work)
|
||||
model Project {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @db.VarChar(255)
|
||||
title String @db.VarChar(255)
|
||||
subtitle String? @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
year Int
|
||||
client String? @db.VarChar(255)
|
||||
role String? @db.VarChar(255)
|
||||
featuredImage String? @db.VarChar(500)
|
||||
logoUrl String? @db.VarChar(500)
|
||||
gallery Json? // Array of image URLs
|
||||
externalUrl String? @db.VarChar(500)
|
||||
caseStudyContent Json? // BlockNote JSON format
|
||||
backgroundColor String? @db.VarChar(50) // For project card styling
|
||||
highlightColor String? @db.VarChar(50) // For project card accent
|
||||
projectType String @default("work") @db.VarChar(50) // "work" or "labs"
|
||||
displayOrder Int @default(0)
|
||||
status String @default("draft") @db.VarChar(50) // "draft", "published", "list-only", "password-protected"
|
||||
password String? @db.VarChar(255) // Required when status is "password-protected"
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @db.VarChar(255)
|
||||
title String @db.VarChar(255)
|
||||
subtitle String? @db.VarChar(255)
|
||||
description String?
|
||||
year Int
|
||||
client String? @db.VarChar(255)
|
||||
role String? @db.VarChar(255)
|
||||
featuredImage String? @db.VarChar(500)
|
||||
gallery Json?
|
||||
externalUrl String? @db.VarChar(500)
|
||||
caseStudyContent Json?
|
||||
displayOrder Int @default(0)
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
backgroundColor String? @db.VarChar(50)
|
||||
highlightColor String? @db.VarChar(50)
|
||||
logoUrl String? @db.VarChar(500)
|
||||
password String? @db.VarChar(255)
|
||||
projectType String @default("work") @db.VarChar(50)
|
||||
|
||||
@@index([slug])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// Posts table (for /universe)
|
||||
model Post {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @db.VarChar(255)
|
||||
postType String @db.VarChar(50) // post, essay
|
||||
title String? @db.VarChar(255) // Optional for post type
|
||||
content Json? // JSON content for posts and essays
|
||||
|
||||
featuredImage String? @db.VarChar(500)
|
||||
attachments Json? // Array of media IDs for photo attachments
|
||||
tags Json? // Array of tags
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @db.VarChar(255)
|
||||
postType String @db.VarChar(50)
|
||||
title String? @db.VarChar(255)
|
||||
content Json?
|
||||
featuredImage String? @db.VarChar(500)
|
||||
tags Json?
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
publishedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
attachments Json?
|
||||
|
||||
@@index([slug])
|
||||
@@index([status])
|
||||
@@index([postType])
|
||||
}
|
||||
|
||||
// Albums table
|
||||
model Album {
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @db.VarChar(255)
|
||||
title String @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
date DateTime?
|
||||
location String? @db.VarChar(255)
|
||||
coverPhotoId Int?
|
||||
isPhotography Boolean @default(false) // Show in photos experience
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
showInUniverse Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
photos Photo[] // Will be removed after migration
|
||||
media AlbumMedia[]
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
slug String @unique @db.VarChar(255)
|
||||
title String @db.VarChar(255)
|
||||
description String?
|
||||
date DateTime?
|
||||
location String? @db.VarChar(255)
|
||||
coverPhotoId Int?
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
showInUniverse Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
content Json?
|
||||
publishedAt DateTime?
|
||||
media AlbumMedia[]
|
||||
geoLocations GeoLocation[]
|
||||
|
||||
@@index([slug])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
// Photos table
|
||||
model Photo {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int?
|
||||
mediaId Int? // Reference to the Media item
|
||||
filename String @db.VarChar(255)
|
||||
url String @db.VarChar(500)
|
||||
thumbnailUrl String? @db.VarChar(500)
|
||||
width Int?
|
||||
height Int?
|
||||
dominantColor String? @db.VarChar(7) // Hex color like #FFFFFF
|
||||
colors Json? // Full color palette from Cloudinary
|
||||
aspectRatio Float? // Width/height ratio
|
||||
exifData Json?
|
||||
caption String? @db.Text
|
||||
displayOrder Int @default(0)
|
||||
|
||||
// Individual publishing support
|
||||
slug String? @unique @db.VarChar(255)
|
||||
title String? @db.VarChar(255)
|
||||
description String? @db.Text
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
publishedAt DateTime?
|
||||
showInPhotos Boolean @default(true)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
album Album? @relation(fields: [albumId], references: [id], onDelete: Cascade)
|
||||
media Media? @relation(fields: [mediaId], references: [id], onDelete: SetNull)
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
filename String @db.VarChar(255)
|
||||
url String @db.VarChar(500)
|
||||
thumbnailUrl String? @db.VarChar(500)
|
||||
width Int?
|
||||
height Int?
|
||||
exifData Json?
|
||||
caption String?
|
||||
displayOrder Int @default(0)
|
||||
slug String? @unique @db.VarChar(255)
|
||||
title String? @db.VarChar(255)
|
||||
description String?
|
||||
status String @default("draft") @db.VarChar(50)
|
||||
publishedAt DateTime?
|
||||
showInPhotos Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
mediaId Int?
|
||||
dominantColor String? @db.VarChar(7)
|
||||
colors Json?
|
||||
aspectRatio Float?
|
||||
media Media? @relation(fields: [mediaId], references: [id])
|
||||
|
||||
@@index([slug])
|
||||
@@index([status])
|
||||
@@index([mediaId])
|
||||
}
|
||||
|
||||
// Media table (general uploads)
|
||||
model Media {
|
||||
id Int @id @default(autoincrement())
|
||||
filename String @db.VarChar(255)
|
||||
originalName String? @db.VarChar(255) // Original filename from user (optional for backward compatibility)
|
||||
mimeType String @db.VarChar(100)
|
||||
size Int
|
||||
url String @db.Text
|
||||
thumbnailUrl String? @db.Text
|
||||
width Int?
|
||||
height Int?
|
||||
dominantColor String? @db.VarChar(7) // Hex color like #FFFFFF
|
||||
colors Json? // Full color palette from Cloudinary
|
||||
aspectRatio Float? // Width/height ratio
|
||||
exifData Json? // EXIF data for photos
|
||||
description String? @db.Text // Description (used for alt text and captions)
|
||||
isPhotography Boolean @default(false) // Star for photos experience
|
||||
|
||||
// Photo-specific fields (migrated from Photo model)
|
||||
photoCaption String? @db.Text // Caption when used as standalone photo
|
||||
photoTitle String? @db.VarChar(255) // Title when used as standalone photo
|
||||
photoDescription String? @db.Text // Description when used as standalone photo
|
||||
photoSlug String? @unique @db.VarChar(255) // Slug for standalone photo
|
||||
photoPublishedAt DateTime? // Published date for standalone photo
|
||||
|
||||
usedIn Json @default("[]") // Track where media is used (legacy)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
usage MediaUsage[]
|
||||
photos Photo[] // Will be removed after migration
|
||||
albums AlbumMedia[]
|
||||
id Int @id @default(autoincrement())
|
||||
filename String @db.VarChar(255)
|
||||
mimeType String @db.VarChar(100)
|
||||
size Int
|
||||
url String
|
||||
thumbnailUrl String?
|
||||
width Int?
|
||||
height Int?
|
||||
usedIn Json @default("[]")
|
||||
createdAt DateTime @default(now())
|
||||
description String?
|
||||
originalName String? @db.VarChar(255)
|
||||
updatedAt DateTime @updatedAt
|
||||
isPhotography Boolean @default(false)
|
||||
exifData Json?
|
||||
photoCaption String?
|
||||
photoTitle String? @db.VarChar(255)
|
||||
photoDescription String?
|
||||
photoSlug String? @unique @db.VarChar(255)
|
||||
photoPublishedAt DateTime?
|
||||
dominantColor String? @db.VarChar(7)
|
||||
colors Json?
|
||||
aspectRatio Float?
|
||||
albums AlbumMedia[]
|
||||
usage MediaUsage[]
|
||||
photos Photo[]
|
||||
}
|
||||
|
||||
// Media usage tracking table
|
||||
model MediaUsage {
|
||||
id Int @id @default(autoincrement())
|
||||
mediaId Int
|
||||
contentType String @db.VarChar(50) // 'project', 'post', 'album'
|
||||
contentId Int
|
||||
fieldName String @db.VarChar(100) // 'featuredImage', 'logoUrl', 'gallery', 'content'
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
mediaId Int
|
||||
contentType String @db.VarChar(50)
|
||||
contentId Int
|
||||
fieldName String @db.VarChar(100)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([mediaId, contentType, contentId, fieldName])
|
||||
@@index([mediaId])
|
||||
@@index([contentType, contentId])
|
||||
}
|
||||
|
||||
// Album-Media relationship table (many-to-many)
|
||||
model AlbumMedia {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int
|
||||
mediaId Int
|
||||
displayOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
album Album @relation(fields: [albumId], references: [id], onDelete: Cascade)
|
||||
media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
|
||||
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int
|
||||
mediaId Int
|
||||
displayOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
album Album @relation(fields: [albumId], references: [id], onDelete: Cascade)
|
||||
media Media @relation(fields: [mediaId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([albumId, mediaId])
|
||||
@@index([albumId])
|
||||
@@index([mediaId])
|
||||
}
|
||||
}
|
||||
|
||||
model GeoLocation {
|
||||
id Int @id @default(autoincrement())
|
||||
albumId Int
|
||||
latitude Float
|
||||
longitude Float
|
||||
title String @db.VarChar(255)
|
||||
description String?
|
||||
markerColor String? @db.VarChar(7)
|
||||
order Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
album Album @relation(fields: [albumId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([albumId])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ You can also run the scripts directly:
|
|||
## Backup Storage
|
||||
|
||||
All backups are stored in the `./backups/` directory with timestamps:
|
||||
|
||||
- Local backups: `local_YYYYMMDD_HHMMSS.sql.gz`
|
||||
- Remote backups: `remote_YYYYMMDD_HHMMSS.sql.gz`
|
||||
|
||||
|
|
@ -115,6 +116,7 @@ DATABASE_URL_PRODUCTION="postgresql://user:password@remote-host:5432/dbname"
|
|||
### "pg_dump: command not found"
|
||||
|
||||
Install PostgreSQL client tools:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
brew install postgresql
|
||||
|
|
@ -132,4 +134,4 @@ Check that your database URLs are correct and include the password.
|
|||
|
||||
### Backup seems stuck
|
||||
|
||||
Large databases may take time. The scripts show progress. For very large databases, consider using `pg_dump` directly with custom options.
|
||||
Large databases may take time. The scripts show progress. For very large databases, consider using `pg_dump` directly with custom options.
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ async function analyzeImage(filename: string) {
|
|||
|
||||
if (media.colors && Array.isArray(media.colors)) {
|
||||
const colors = media.colors as Array<[string, number]>
|
||||
|
||||
|
||||
console.log('\n=== Color Distribution ===')
|
||||
console.log('Top 15 colors:')
|
||||
colors.slice(0, 15).forEach(([hex, percentage], index) => {
|
||||
|
|
@ -47,7 +47,7 @@ async function analyzeImage(filename: string) {
|
|||
})
|
||||
|
||||
console.log('\n=== Color Analysis Strategies ===')
|
||||
|
||||
|
||||
// Try different strategies
|
||||
const strategies = {
|
||||
'Default (min 2%, prefer vibrant & bright)': selectBestDominantColor(colors, {
|
||||
|
|
@ -56,28 +56,28 @@ async function analyzeImage(filename: string) {
|
|||
excludeGreys: false,
|
||||
preferBrighter: true
|
||||
}),
|
||||
|
||||
|
||||
'Exclude greys, prefer bright': selectBestDominantColor(colors, {
|
||||
minPercentage: 1,
|
||||
preferVibrant: true,
|
||||
excludeGreys: true,
|
||||
preferBrighter: true
|
||||
}),
|
||||
|
||||
|
||||
'Very low threshold (0.5%), bright': selectBestDominantColor(colors, {
|
||||
minPercentage: 0.5,
|
||||
preferVibrant: true,
|
||||
excludeGreys: false,
|
||||
preferBrighter: true
|
||||
}),
|
||||
|
||||
|
||||
'Allow dark colors': selectBestDominantColor(colors, {
|
||||
minPercentage: 1,
|
||||
preferVibrant: true,
|
||||
excludeGreys: false,
|
||||
preferBrighter: false
|
||||
}),
|
||||
|
||||
|
||||
'Focus on prominence (5%)': selectBestDominantColor(colors, {
|
||||
minPercentage: 5,
|
||||
preferVibrant: false,
|
||||
|
|
@ -88,21 +88,25 @@ async function analyzeImage(filename: string) {
|
|||
|
||||
Object.entries(strategies).forEach(([strategy, color]) => {
|
||||
const analysis = analyzeColor(color)
|
||||
console.log(`${strategy}: ${color} | V:${analysis.vibrance.toFixed(2)} B:${analysis.brightness.toFixed(2)}${analysis.isGrey ? ' (grey)' : ''}${analysis.isDark ? ' (dark)' : ''}`)
|
||||
console.log(
|
||||
`${strategy}: ${color} | V:${analysis.vibrance.toFixed(2)} B:${analysis.brightness.toFixed(2)}${analysis.isGrey ? ' (grey)' : ''}${analysis.isDark ? ' (dark)' : ''}`
|
||||
)
|
||||
})
|
||||
|
||||
// Show non-grey colors
|
||||
console.log('\n=== Non-Grey Colors ===')
|
||||
const nonGreyColors = colors.filter(([hex]) => !isGreyColor(hex))
|
||||
console.log(`Found ${nonGreyColors.length} non-grey colors out of ${colors.length} total`)
|
||||
|
||||
|
||||
if (nonGreyColors.length > 0) {
|
||||
console.log('\nTop 10 non-grey colors:')
|
||||
nonGreyColors.slice(0, 10).forEach(([hex, percentage], index) => {
|
||||
const analysis = analyzeColor(hex)
|
||||
console.log(`${index + 1}. ${hex} - ${percentage.toFixed(2)}% | B:${analysis.brightness.toFixed(2)}`)
|
||||
console.log(
|
||||
`${index + 1}. ${hex} - ${percentage.toFixed(2)}% | B:${analysis.brightness.toFixed(2)}`
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
// Look for more vibrant colors deeper in the list
|
||||
console.log('\n=== All Colors with >0.5% ===')
|
||||
const significantColors = colors.filter(([_, pct]) => pct > 0.5)
|
||||
|
|
@ -114,15 +118,16 @@ async function analyzeImage(filename: string) {
|
|||
const b = parseInt(hex.slice(5, 7), 16)
|
||||
const max = Math.max(r, g, b)
|
||||
const min = Math.min(r, g, b)
|
||||
const saturation = max === 0 ? 0 : (max - min) / max * 100
|
||||
|
||||
console.log(`${hex} - ${percentage.toFixed(2)}% | Sat: ${saturation.toFixed(0)}%${isGrey ? ' (grey)' : ''}`)
|
||||
const saturation = max === 0 ? 0 : ((max - min) / max) * 100
|
||||
|
||||
console.log(
|
||||
`${hex} - ${percentage.toFixed(2)}% | Sat: ${saturation.toFixed(0)}%${isGrey ? ' (grey)' : ''}`
|
||||
)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.log('\nNo color data available for this image')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
} finally {
|
||||
|
|
@ -132,4 +137,4 @@ async function analyzeImage(filename: string) {
|
|||
|
||||
// Get filename from command line argument
|
||||
const filename = process.argv[2] || 'B0000295.jpg'
|
||||
analyzeImage(filename)
|
||||
analyzeImage(filename)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ async function checkPhotoColors() {
|
|||
|
||||
// Count photos with dominant color
|
||||
const photosWithColor = await prisma.media.count({
|
||||
where: {
|
||||
where: {
|
||||
isPhotography: true,
|
||||
dominantColor: { not: null }
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ async function checkPhotoColors() {
|
|||
|
||||
// Count photos without dominant color
|
||||
const photosWithoutColor = await prisma.media.count({
|
||||
where: {
|
||||
where: {
|
||||
isPhotography: true,
|
||||
dominantColor: null
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ async function checkPhotoColors() {
|
|||
|
||||
// Get some examples
|
||||
const examples = await prisma.media.findMany({
|
||||
where: {
|
||||
where: {
|
||||
isPhotography: true,
|
||||
dominantColor: { not: null }
|
||||
},
|
||||
|
|
@ -43,16 +43,19 @@ async function checkPhotoColors() {
|
|||
|
||||
console.log('=== Photography Color Analysis ===')
|
||||
console.log(`Total photography items: ${totalPhotos}`)
|
||||
console.log(`With dominant color: ${photosWithColor} (${((photosWithColor/totalPhotos)*100).toFixed(1)}%)`)
|
||||
console.log(`Without dominant color: ${photosWithoutColor} (${((photosWithoutColor/totalPhotos)*100).toFixed(1)}%)`)
|
||||
|
||||
console.log(
|
||||
`With dominant color: ${photosWithColor} (${((photosWithColor / totalPhotos) * 100).toFixed(1)}%)`
|
||||
)
|
||||
console.log(
|
||||
`Without dominant color: ${photosWithoutColor} (${((photosWithoutColor / totalPhotos) * 100).toFixed(1)}%)`
|
||||
)
|
||||
|
||||
if (examples.length > 0) {
|
||||
console.log('\n=== Examples with dominant colors ===')
|
||||
examples.forEach(media => {
|
||||
examples.forEach((media) => {
|
||||
console.log(`${media.filename}: ${media.dominantColor}`)
|
||||
})
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
} finally {
|
||||
|
|
@ -60,4 +63,4 @@ async function checkPhotoColors() {
|
|||
}
|
||||
}
|
||||
|
||||
checkPhotoColors()
|
||||
checkPhotoColors()
|
||||
|
|
|
|||
|
|
@ -77,7 +77,6 @@ async function findImageColors() {
|
|||
if (!photo && !media) {
|
||||
console.log('\nImage B0000295.jpg not found in either Photo or Media tables.')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error searching for image:', error)
|
||||
} finally {
|
||||
|
|
@ -86,4 +85,4 @@ async function findImageColors() {
|
|||
}
|
||||
|
||||
// Run the script
|
||||
findImageColors()
|
||||
findImageColors()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
/**
|
||||
* Script to reanalyze colors for specific images or all images
|
||||
* Usage: tsx scripts/reanalyze-colors.ts [options]
|
||||
*
|
||||
*
|
||||
* Options:
|
||||
* --id <mediaId> Reanalyze specific media ID
|
||||
* --grey-only Only reanalyze images with grey dominant colors
|
||||
|
|
@ -106,7 +106,7 @@ async function reanalyzeColors(options: Options) {
|
|||
console.log(`\n${media.filename}:`)
|
||||
console.log(` Current: ${currentColor || 'none'}`)
|
||||
console.log(` New: ${newColor}`)
|
||||
|
||||
|
||||
// Show color breakdown
|
||||
const topColors = colors.slice(0, 5)
|
||||
console.log(' Top colors:')
|
||||
|
|
@ -141,7 +141,6 @@ async function reanalyzeColors(options: Options) {
|
|||
if (options.dryRun) {
|
||||
console.log(` (Dry run - no changes made)`)
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error)
|
||||
process.exit(1)
|
||||
|
|
@ -164,4 +163,4 @@ if (!options.id && !options.all && !options.greyOnly) {
|
|||
process.exit(1)
|
||||
}
|
||||
|
||||
reanalyzeColors(options)
|
||||
reanalyzeColors(options)
|
||||
|
|
|
|||
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 |
3
src/assets/icons/chevron-right.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 12L10 8L6 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="3" width="6" height="6" stroke="currentColor" stroke-width="1.5" fill="none" rx="1" />
|
||||
<rect x="11" y="3" width="6" height="6" stroke="currentColor" stroke-width="1.5" fill="none" rx="1" />
|
||||
<rect x="3" y="11" width="6" height="6" stroke="currentColor" stroke-width="1.5" fill="none" rx="1" />
|
||||
<rect x="11" y="11" width="6" height="6" stroke="currentColor" stroke-width="1.5" fill="none" rx="1" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 518 B |
8
src/assets/icons/drag-handle.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="5" cy="3" r="1.5" fill="currentColor"/>
|
||||
<circle cx="11" cy="3" r="1.5" fill="currentColor"/>
|
||||
<circle cx="5" cy="8" r="1.5" fill="currentColor"/>
|
||||
<circle cx="11" cy="8" r="1.5" fill="currentColor"/>
|
||||
<circle cx="5" cy="13" r="1.5" fill="currentColor"/>
|
||||
<circle cx="11" cy="13" r="1.5" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 431 B |
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 |
|
|
@ -1,3 +0,0 @@
|
|||
<svg fill="currentColor" viewBox="0 0 56 56" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M 36.4023 19.3164 C 38.8398 19.3164 40.9257 17.7461 41.6992 15.5898 L 49.8085 15.5898 C 50.7695 15.5898 51.6133 14.7461 51.6133 13.6914 C 51.6133 12.6367 50.7695 11.8164 49.8085 11.8164 L 41.7226 11.8164 C 40.9257 9.6367 38.8398 8.0430 36.4023 8.0430 C 33.9648 8.0430 31.8789 9.6367 31.1054 11.8164 L 6.2851 11.8164 C 5.2304 11.8164 4.3867 12.6367 4.3867 13.6914 C 4.3867 14.7461 5.2304 15.5898 6.2851 15.5898 L 31.1054 15.5898 C 31.8789 17.7461 33.9648 19.3164 36.4023 19.3164 Z M 6.1913 26.1133 C 5.2304 26.1133 4.3867 26.9570 4.3867 28.0117 C 4.3867 29.0664 5.2304 29.8867 6.1913 29.8867 L 14.5586 29.8867 C 15.3320 32.0898 17.4179 33.6601 19.8554 33.6601 C 22.3164 33.6601 24.4023 32.0898 25.1757 29.8867 L 49.7149 29.8867 C 50.7695 29.8867 51.6133 29.0664 51.6133 28.0117 C 51.6133 26.9570 50.7695 26.1133 49.7149 26.1133 L 25.1757 26.1133 C 24.3789 23.9570 22.2929 22.3867 19.8554 22.3867 C 17.4413 22.3867 15.3554 23.9570 14.5586 26.1133 Z M 36.4023 47.9570 C 38.8398 47.9570 40.9257 46.3867 41.6992 44.2070 L 49.8085 44.2070 C 50.7695 44.2070 51.6133 43.3867 51.6133 42.3320 C 51.6133 41.2773 50.7695 40.4336 49.8085 40.4336 L 41.6992 40.4336 C 40.9257 38.2539 38.8398 36.7070 36.4023 36.7070 C 33.9648 36.7070 31.8789 38.2539 31.1054 40.4336 L 6.2851 40.4336 C 5.2304 40.4336 4.3867 41.2773 4.3867 42.3320 C 4.3867 43.3867 5.2304 44.2070 6.2851 44.2070 L 31.1054 44.2070 C 31.8789 46.3867 33.9648 47.9570 36.4023 47.9570 Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,47 +0,0 @@
|
|||
<!-- jedmund-blink -->
|
||||
<svg width="497" height="497" viewBox="0 0 497 497" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_1_32)">
|
||||
<path d="M497 0H0V497H497V0Z" fill="#FF2602" style="fill:#FF2602;fill:color(display-p3 1.0000 0.1490 0.0078);fill-opacity:1;"/>
|
||||
<path d="M155.92 497C155.92 497 171.19 448 215.69 431C234.91 423.66 269.19 421.5 291.92 433.5C305.42 440.63 312.92 453.5 316.92 461.73C322.375 472.659 324.956 484.797 324.42 497H155.92Z" fill="#BCBCBC" style="fill:#BCBCBC;fill:color(display-p3 0.7373 0.7373 0.7373);fill-opacity:1;"/>
|
||||
<path d="M264.33 424.17C292.5 424.17 327.21 445.17 331.83 485.17C332.98 495.06 333.12 497.88 333.12 497.88H322.5C322.5 497.88 325.65 436.08 265 426.83C245.92 423.92 179.5 437.83 158.33 497.88H145.83C145.83 497.88 157 442 226.5 419.5C251.06 412.94 264.33 424.17 264.33 424.17Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M296 497.88C296 497.88 295.5 486.62 296.75 481.5C298 476.38 295.88 473.5 293.75 473.12C291.62 472.74 288.88 473.88 287.88 478.25C286.657 484.722 286.028 491.293 286 497.88H296Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M131.81 207.25C131.81 207.25 137.14 236.25 136.81 250.08C136.81 250.08 134.48 274.91 129.81 293.25C125.14 311.59 120.81 348.91 126.14 365.25C131.47 381.59 153.48 411.25 194.48 405.25C235.48 399.25 237.14 397.58 236.81 407.58C236.48 417.58 235.48 427.58 231.48 433.91C231.48 433.91 244.81 436.25 251.48 431.58L258.14 426.91C258.14 426.91 258.54 415.27 261.48 408.25C263.48 403.45 266.14 403.25 266.14 403.25C266.14 403.25 300.81 394.91 320.14 377.58C335.67 363.66 351.48 337.25 354.48 299.91C354.48 299.91 366.81 294.25 368.48 288.25L370.14 282.25C370.14 282.25 370.48 275.91 364.81 276.91C359.14 277.91 353.81 281.91 353.81 281.91C353.81 281.91 360.81 223.25 352.14 193.91C347.14 176.98 338.36 158.64 312.14 141.25C276.48 117.58 189.48 120.25 142.81 157.91C125.16 172.16 131.81 207.25 131.81 207.25Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M135.85 261C135.85 261 147.71 253.65 150.67 246.34C162.35 252.34 181.57 252.34 184.29 246.34C187.01 240.34 184.38 235.16 180.66 232.91C184 234.1 186.25 232.1 186.66 229.54C190.35 232.54 208.22 230.42 212.53 223.16C215.72 226.66 232.41 225.44 237.35 218.85C241.47 225.66 266.28 226.79 274.35 217.16C274.35 217.16 290.69 204.05 277.2 178.33C263.71 152.61 151 155.67 135.85 183.13C120.7 210.59 118.08 236.68 135.85 261Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M245.54 433.73C245.54 433.73 243.72 417 254 405.68C265 393.58 289 392.08 302.7 367.88C302.7 367.88 298.2 354.62 307.2 342.62C307.2 342.62 295.01 321 320.2 310.62L355.07 304.09C355.07 304.09 353.37 331.17 337.2 356.38C331.04 367.44 317.2 387.67 274.83 401.12C274.83 401.12 262.33 404.38 261.04 411.4C260.212 415.793 260.104 420.292 260.72 424.72C260.778 425.106 260.737 425.5 260.6 425.866C260.464 426.232 260.237 426.556 259.94 426.81C255.829 430.338 250.863 432.724 245.54 433.73Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M178.25 359.94C198 360.69 231.63 355.44 231.63 355.44C231.63 355.44 232 344.88 235.5 341C239 337.12 241.13 339.12 241.13 339.12C241.13 339.12 244 340.5 241.13 345.12C238.26 349.74 238.56 356.38 239.31 359.56C240.06 362.74 245 370.5 245 370.5C245.033 370.662 245.02 370.83 244.964 370.986C244.907 371.141 244.809 371.278 244.68 371.381C244.551 371.485 244.396 371.55 244.232 371.571C244.068 371.592 243.901 371.568 243.75 371.5C242.38 371.12 241.13 372.75 241.13 372.75C241.13 372.75 240.38 373.61 238.88 371.88C236.122 368.537 233.945 364.754 232.44 360.69C232.44 360.69 203.86 366.31 177.92 365.31C177.92 365.31 175.81 365.69 175.06 362.56C174.35 359.6 178.25 359.94 178.25 359.94Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M216.13 280C216.13 277.37 216.28 276 218.72 275.87C221.16 275.74 221.06 278.75 221.06 278.75C221.06 278.75 220.67 293.91 215.06 307.75C215.06 307.75 212.19 315.19 210.13 315.19C209.093 315.438 208.001 315.318 207.043 314.849C206.085 314.38 205.32 313.592 204.88 312.62C204.88 312.62 203.56 309.07 202.16 308.97C199.59 308.78 197.96 313.58 197.6 317.23C197.38 319.5 196.44 320.94 194.13 320.94C191.82 320.94 190.94 319.55 191.19 317.12C192.19 310.94 196.47 303.91 199.44 300.94C199.728 300.646 200.072 300.411 200.451 300.25C200.83 300.088 201.238 300.003 201.65 300C204.75 300 205.65 302.67 206.07 304.89C206.81 308.75 208.13 308.47 208.13 308.47C208.13 308.47 208.7 308.67 209.5 307.06C212.94 300.12 216.13 288.44 216.13 280Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M262.09 286.69C262.51 285.52 263.24 284.85 264.22 285.22C265.53 285.72 264.78 288.12 266.09 288.59C269.01 289.64 269.21 286.7 270.2 286.59C271.64 286.4 271.8 288.19 271.51 290.07C271.11 292.66 270.58 293.22 269.69 294.26C268.16 296.04 266.56 296.26 264.44 295.76C263.44 295.53 261.81 294.57 261.63 291.76C261.479 290.056 261.635 288.339 262.09 286.69Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M166.38 281.12C168.81 281.12 168.88 279.12 169.63 279.12C170.38 279.12 170.85 280.01 171.04 281.03C171.518 282.867 171.297 284.816 170.42 286.5C170.42 286.5 167.75 289.67 165.5 289.31C165.067 289.287 164.642 289.178 164.252 288.988C163.862 288.797 163.514 288.531 163.229 288.203C162.945 287.875 162.729 287.494 162.595 287.081C162.461 286.668 162.412 286.232 162.45 285.8C162.52 283.71 163.06 284.46 163.08 282.25C163.08 280.25 163.3 279.5 164.29 279.33C165.56 279.12 165.19 281.12 166.38 281.12Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M253.28 242.12C253.22 248.12 259.28 252.5 269.88 252.75C280.48 253 285.55 251.58 290.88 246.67C296.21 241.76 280.36 237.94 271.04 238.26C261.72 238.58 253.31 239 253.28 242.12Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M255.32 241C255.32 241 256.5 235.67 271.67 236.17C286.84 236.67 288.33 241 289.5 242C290.67 243 290.83 244.67 288 245.5C288 245.5 266.43 247.93 258.33 244.33C254.42 242.59 255.32 241 255.32 241Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M293.13 242.18C292.87 240.61 292.87 238.52 287.81 236.6C282.75 234.68 276.21 234.42 268.76 234.68C264.253 234.858 259.79 235.638 255.49 237C255.49 237 251.69 240.23 254.05 243.89C256.41 247.55 272.54 248.19 272.54 248.19C272.54 248.19 284.93 248.78 289.38 247.38C292.89 246.23 293.39 243.75 293.13 242.18ZM284.23 243.93C284.23 243.93 267.23 245.08 259.23 242.88C255.44 241.88 256.7 239.39 260.8 238.69C264.191 238.337 267.601 238.193 271.01 238.26C271.01 238.26 283.68 237.51 286.28 242.1C287.46 244.1 284.23 243.93 284.23 243.93Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M170 233.33C175 233.33 178.17 233.6 179.5 236.13C180.83 238.66 180.62 241.63 177.67 242.13C174.31 242.73 171.08 242.8 163.81 241.84C160.5 241.39 157.14 241.4 154.94 242.58C152.74 243.76 151.25 241.58 153.75 238.71C156.11 236 165 233.33 170 233.33Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M183.63 236.13C183.63 236.13 182.38 231 171.63 230.88C160.88 230.76 150.23 236.13 150.23 236.13C150.23 236.13 145.73 239.5 147.36 242.75C148.99 246 154.86 245.08 154.86 245.08C157.653 244.474 160.494 244.12 163.35 244.02C166.2 244.1 173.61 244.88 173.61 244.88C176.483 244.962 179.313 244.174 181.73 242.62C182.297 242.336 182.799 241.937 183.206 241.45C183.612 240.963 183.914 240.397 184.092 239.788C184.271 239.18 184.322 238.541 184.242 237.911C184.162 237.282 183.954 236.675 183.63 236.13ZM176.38 240.62C173.585 241.282 170.675 241.282 167.88 240.62C163.5 239.5 157.88 240.5 155.5 241.12C153.12 241.74 152.63 239.75 153.63 239C154.63 238.25 158.63 235.12 169.25 235.12C169.25 235.12 176.88 235.04 178.13 237.38C179.68 240.28 176.38 240.62 176.38 240.62Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M113.14 341.08C113.14 341.08 97.14 334.75 97.47 322.08C97.47 322.08 98.47 310.42 106.81 309.42C106.81 309.42 86.14 309.75 86.14 295.75C86.14 295.75 86.14 283.08 97.47 283.42C97.47 283.42 78.47 275.42 78.47 261.75C78.47 249.08 85.81 240.75 95.81 240.75C95.81 240.75 80.47 232.08 80.47 220.08C80.47 220.08 79.47 209.75 85.14 204.75C85.14 204.75 76.14 184.08 83.47 167.42C83.47 167.42 86.14 156.75 104.14 153.75C104.14 153.75 98.14 137.42 107.47 126.75C107.47 126.75 112.47 118.75 125.47 118.08C125.47 118.08 127.47 102.26 141.47 98.08C141.47 98.08 157.03 97.08 168.69 102.08C168.69 102.08 177.14 79.43 195.14 72.76C195.14 72.76 214.34 65.76 228.34 78.37C228.34 78.37 230.47 57.09 247.97 57.09C265.47 57.09 272.66 72.17 272.66 72.17C272.66 72.17 277.72 59.04 294.47 64.09C312.14 69.43 312.47 93.43 312.47 93.43C312.47 93.43 326.47 78.76 345.81 84.09C353.92 86.33 357.14 97.76 354.81 106.43C354.81 106.43 362.47 98.09 369.81 101.43C377.15 104.77 391.47 117.09 382.81 137.76C382.81 137.76 401.47 135.43 410.14 150.09C410.14 150.09 424.14 169.76 407.14 184.76C407.14 184.76 428.47 183.09 432.47 209.09C432.47 209.09 434.14 229.76 413.47 236.43C413.47 236.43 430.81 237.76 430.47 253.76C430.47 253.76 432.77 269.76 411.77 276.76C411.77 276.76 423.43 285.76 421.1 297.43C417.1 317.43 391.43 315.43 391.43 315.43C391.43 315.43 405.43 320.09 403.43 328.43C403.43 328.43 399.77 338.43 386.1 336.43C386.1 336.43 395.1 345.43 387.1 352.76C379.1 360.09 369.43 352.76 369.43 352.76C369.43 352.76 374.81 368.89 364.08 373.23C353.56 377.49 344.52 369.41 342.86 367.08C341.2 364.75 346.43 377.76 332.1 379.08C317.77 380.4 316.43 366.08 316.43 366.08C316.43 366.08 317.43 352.75 329.1 355.42C329.1 355.42 320.43 352.42 321.1 344.42C321.77 336.42 330.77 333.42 330.77 333.42C330.77 333.42 318.77 339.08 317.1 329.42C315.43 319.76 325.43 309.75 325.43 309.75C325.43 309.75 311.43 311.42 310.1 297.75C310.1 297.75 309.77 282.75 325.77 277.08C325.77 277.08 309.1 274.75 309.43 256.08C309.76 237.41 326.1 231.42 326.1 231.42C326.1 231.42 316.77 238.42 307.43 230.75C298.09 223.08 302.1 210.08 302.1 210.08C302.1 210.08 298.43 221.84 283.1 218.08C267.77 214.32 271.77 200.08 271.77 200.08C271.77 200.08 263.43 215.08 250.77 213.42C238.11 211.76 237.1 196.75 237.1 196.75C237.1 196.75 232.1 205.42 225.43 204.75C225.43 204.75 212.77 205.08 213.43 186.75C213.43 186.75 213.1 204.08 195.77 207.42C178.44 210.76 177.1 193.75 177.1 193.75C177.1 193.75 179.1 208.42 162.43 214.75C145.76 221.08 143.43 206.42 143.43 206.42C143.43 206.42 148.1 217.08 139.43 229.42C130.76 241.76 131.1 236.42 131.1 236.42C132.79 243.396 133.353 250.596 132.77 257.75C131.77 269.42 125.1 286.08 121.77 305.42C119.756 317.082 118.972 328.924 119.43 340.75L113.14 341.08Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M331.19 339.25C335.93 337.62 335.63 334.81 333.31 333.19C330.99 331.57 327.13 332.56 326.19 333.94C325.25 335.32 323.05 335.05 321.75 332.81C317.63 325.7 324.31 316.12 328.69 314.69C334.69 312.69 332.38 309.25 332.38 309.25C332.011 308.646 331.463 308.172 330.813 307.893C330.163 307.613 329.442 307.543 328.75 307.69C326.94 308.12 326.13 308.88 324.75 308.88C320 308.88 315.75 304.5 315.88 298.44C316.01 292.38 321.56 283.31 326.81 282.5C332.06 281.69 331.28 278.95 331.28 278.95C331.28 278.95 331.91 276.88 326.81 275.81C317.34 273.81 314 270 313.63 258C313.26 246 318.75 238.56 324.44 237.21C333.68 235.02 332 229.31 331.38 228.62C330.828 228.254 330.156 228.114 329.503 228.232C328.851 228.349 328.27 228.714 327.88 229.25C327.19 230.5 324.1 235.15 315.04 232.08C300.68 227.23 306.94 212.08 307.94 209.38C308.94 206.68 304.88 206.94 304.88 206.94C304.88 206.94 300.5 206.62 299.81 208.62C298.94 211.17 294.44 219.81 285.69 218.06C274.85 215.9 274.81 205.94 275.13 202.56C275.45 199.18 274.81 198.88 273.13 198.88C271.45 198.88 269.69 198.81 269 201.69C266.43 212.44 251.94 212.88 251.94 212.88C238.44 212.56 239.18 196.15 239.75 191.88C240.44 186.75 240.44 186.79 239.49 186.88C238 186.97 236.38 190.28 236.38 192.41C236.486 194.485 235.985 196.545 234.94 198.34C233.75 200.34 231.25 204.65 226.31 204.47C221.37 204.29 218.75 199.47 218.69 194.59C218.63 190.34 218.88 185.59 218.88 185.59C218.81 184.28 218 182.72 216.69 183.28C215.38 183.84 214.19 184.41 212.31 185.41C211.77 185.729 211.339 186.202 211.071 186.769C210.804 187.336 210.713 187.971 210.81 188.59L211 191.97C211.06 195.47 207.89 199.66 205.63 201.72C198.75 207.97 191.7 207.91 188.27 204.41C184.84 200.91 184.19 199.34 183.27 195.47C182.74 193.28 181.27 192.72 180.46 193.03C179.65 193.34 176.71 194.65 176.71 194.65C176.437 194.798 176.198 195 176.007 195.244C175.816 195.489 175.678 195.77 175.601 196.071C175.523 196.371 175.509 196.684 175.559 196.991C175.609 197.297 175.722 197.589 175.89 197.85C176.62 199.29 174.82 202.85 174.08 203.91C171.94 207.13 165.4 213.47 160.02 214.36C154.43 215.29 151.35 209.36 149.85 206.36C148.35 203.36 145.02 204.53 142.02 206.36C139.02 208.19 139.85 210.36 139.85 210.36C141.85 216.64 139.64 224.91 137.52 228.7C132.19 238.21 125.52 235.7 123.52 235.15C121.52 234.6 120.02 236.7 122.69 239.86C125.36 243.02 132.1 241.61 132.1 241.61C132.1 241.61 126.85 272.2 122.19 289.7C117.53 307.2 114.52 329.36 114.52 340.7C114.52 352.04 116.69 382.86 145.35 399.7C174.01 416.54 199.52 413.53 208.35 412.03C208.35 412.03 221.02 409.52 226.19 406.53C226.19 406.53 228.98 404.65 229.19 408.53C229.52 414.78 227.5 426.53 222.85 433.2C222.85 433.2 218.19 440.63 240.52 440.63C249.19 440.63 254.69 439.54 264.85 432.03C264.85 432.03 266.85 431.36 266.35 426.36C265.85 421.36 265.68 410.86 266.43 407.36C266.43 407.36 304.08 399.59 322.07 381.44L322.45 374.6C316.79 366.23 326.15 359.78 327.21 360.34C328.27 360.9 328.96 361.03 331.21 360.78C333.46 360.53 333.71 359.41 333.27 358.03C332.83 356.65 330.96 355.15 327.4 353.72C323.84 352.29 327.38 340.56 331.19 339.25ZM321.08 340.19C317.65 342.62 317.33 349.33 320.4 352.52C321.34 353.52 321.28 354.52 320.4 354.77C311.92 357.03 310.87 366.54 315.81 374.85C317.46 377.62 316.56 378.27 316.56 378.27C300.31 394.02 278.19 400.5 267.63 402.56C264.52 403.17 262.38 404.44 261.15 409.1C259.63 414.86 259.23 427.27 259.23 427.27C247.23 437.27 230.31 434.69 232.31 432.19C234.31 429.69 235.31 424.93 235.31 424.93C235.31 424.93 236.54 421.26 238.25 408.38C239.44 399.44 232.82 394.82 222.53 398.5C200.6 406.33 174.98 406.25 159.81 399.44C137.55 389.44 127.42 374.23 125.31 352.94C123.2 331.65 123.92 324.5 131.31 288.86C138.7 253.22 136.56 238.15 136.56 238.15C147.19 234.15 148.5 217.25 148.5 217.25C162.81 227.88 180.75 204.88 180.75 204.88C188.81 216.31 206.06 212.14 213.88 200.31C215.25 208.88 232.88 213.44 237.25 199.75C235.19 216.06 255.94 224.25 269.5 208.88C270.18 208.1 270.82 208.5 271 209.44C273.65 223.5 288.73 226.35 297.4 221.44C299.99 219.96 299.94 221.5 299.94 221.5C298.86 232.24 313.88 236.5 317.09 236.2C317.9 236.14 318.09 236.75 317.69 236.98C311.59 240.98 308.39 247.54 307.46 254.37C306.13 264.17 310.63 274.04 316.88 277.14C320.16 278.76 318.23 280.14 318.23 280.14C310.9 286.06 308.56 293.93 308.48 297.73C308.4 301.53 310.81 311.14 317.81 311.98C320.3 312.27 318.98 313.89 318.98 313.89C313.13 321.33 313.65 335.56 319.48 338.23C322.07 339.37 322.21 339.39 321.08 340.19Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M116.48 346.51C97.26 345.19 83.38 330.08 93 312.67C80.38 308.62 75.25 293.25 85.25 285.12C73.13 279.62 63.63 258.5 84.63 238.38C72.24 230.38 73.75 213.38 82 205.5C74 199.25 72.88 159 98.75 154.5C92.88 135.12 111 115.24 122.25 118C124.88 103.76 144.25 87 165.63 99C167.92 100.29 168.51 100.54 169.5 97.25C172.25 88.12 198.5 56.01 223.13 73.88C227.5 76.38 227.93 75.88 228 72.25C228.25 59.88 256.53 45.5 270.75 67.25C272.88 70.5 273.13 70 275.25 67.38C277.37 64.76 303.13 48.25 315.75 86.88C339.13 74.12 359.25 84 359.25 101.38C378.13 94.5 394 116.75 390.13 136.62C413.5 136.88 428.5 160.25 417.88 184.12C442.88 194.25 447.5 226.5 422.5 236.12C442.88 246.75 438.92 271.48 420.13 280.88C418.63 281.62 419.03 282.19 419.5 282.5C425.5 286.5 434.85 317.44 404.5 319.12C400 319.38 400.38 320.88 401.75 322.12C403.12 323.36 415.13 338.12 394.5 340.75C401.38 356.12 384.5 363.5 377.38 362.25C379.25 370.75 366 381.88 348.63 376.5C346.38 385.73 324.88 385 319 379.5C317.873 378.546 316.965 377.36 316.34 376.022C315.715 374.684 315.388 373.227 315.38 371.75L320.63 369.6C320.63 369.6 320.89 377.84 329.73 377.73C338.73 377.61 340.54 373.98 340.02 368.73C339.75 365.98 341.75 366.6 341.75 366.6C341.75 366.6 344.63 367.1 345.75 365.86C346.87 364.62 348.25 364.86 348.75 366.1C349.25 367.34 352.88 375.1 362.63 370.48C372.38 365.86 368 359.25 367 357.38C366 355.51 366.13 353.38 367 353.12C368.605 353.008 370.109 352.294 371.21 351.12C372.6 349.48 373.97 353.4 375.28 354.83C376.41 356.04 385.28 355.56 385.28 345.92C385.28 339.16 381.78 336.04 381.78 336.04C381.78 336.04 399.66 335.54 399.91 329.16C400.16 322.78 393.51 321.76 391.28 321.16C390.227 320.903 389.312 320.254 388.721 319.345C388.13 318.437 387.908 317.337 388.099 316.271C388.29 315.204 388.881 314.25 389.75 313.603C390.62 312.957 391.703 312.666 392.78 312.79C396.89 313.18 417.16 312.17 417.16 295.79C417.16 284.29 410.28 283.16 410.28 283.16C405.53 281.79 407.03 273.36 411.96 273.44C418.53 273.54 426.91 264.66 426.66 253.92C426.66 253.92 428.2 236.68 412.03 239.79C405.53 241.04 406.78 236.92 409.16 234.79C411.54 232.66 437.69 216.58 424.91 199.54C415.53 187.04 407.86 190.54 407.86 190.54C403.53 191.93 400.57 184.54 404.91 181.3C410.16 177.43 421.53 145.93 389.28 143.43C387.15 143.26 386.34 143.95 385.66 145.43C384.98 146.91 382.91 146.15 382.78 144.75C382.68 143.63 382.25 143.39 381.27 143.75C380.29 144.11 376.78 143.22 380.66 137.35C385.49 130.02 378.91 105.88 366.16 105.6C360.53 105.48 355.49 110.43 353.16 110.69C351.5 110.88 349.62 109.89 350.78 103.1C351.53 98.72 348.56 86.18 331.28 88.1C324.53 88.85 318.94 90.25 315.53 95.1C313.9 97.41 312.46 97.96 310.16 97.85C306.97 97.7 305.41 95.1 305.91 89.35C306.41 83.6 299.66 68.48 287.91 68.48C282.16 68.48 275.91 73.1 272.03 77.35C268.28 71.72 258.61 55.77 241.53 63.48C231.25 68 230.63 78.62 230.75 80.75C230.87 82.88 228.5 83.88 226.13 82C223.76 80.12 198 57.5 174.88 99C170.74 106.43 169.48 108.56 163.88 104.88C159.5 102 133.34 95.49 129.75 118.38C128.75 124.75 127.25 125.38 119.15 125.13C113.28 124.95 100.88 136.75 108.88 157.75C88.38 162.12 83.56 169.75 83.88 185.25C84.13 197.75 85.62 202.79 88.63 205.12C88.63 205.12 89.75 205.25 87.88 208.38C86.01 211.51 77.82 228.91 102.13 240.12C103.75 240.88 104 244.38 101 244.38C98 244.38 81.88 250.25 82.13 263.25C82.38 276.25 94 282 99.13 282.25C104.26 282.5 102.5 287.88 99.25 287.75C96 287.62 90.25 291.38 90.13 297.12C90.01 302.86 93 307.22 101.5 307.5C107.37 307.69 111.21 310.14 111.88 311.62C113 314.12 108 312.38 105.63 313.62C103.26 314.86 95.48 333.94 115.16 340.22L116.48 346.51Z" fill="#060500" style="fill:#060500;fill:color(display-p3 0.0235 0.0196 0.0000);fill-opacity:1;"/>
|
||||
<path d="M266.69 341.67C283.43 341.67 297 332.268 297 320.67C297 309.072 283.43 299.67 266.69 299.67C249.95 299.67 236.38 309.072 236.38 320.67C236.38 332.268 249.95 341.67 266.69 341.67Z" fill="url(#paint0_radial_1_32)" style=""/>
|
||||
<path d="M365.31 267.69C360.31 267.44 353.69 268.25 350.69 271C351.75 261.75 348 261.18 345 261C342 260.82 342.25 263.79 342.25 263.79C342.25 263.79 349.5 274.54 346.25 315.79C349.238 314.474 351.828 312.396 353.758 309.763C355.689 307.13 356.893 304.035 357.25 300.79C357.25 300.79 362 298.34 365.5 298.44C369 298.54 380 295.54 379.75 284.04C379.5 272.54 370.31 267.94 365.31 267.69ZM360.56 293C357.06 294.69 357 292.81 357.06 290.88C357.12 288.95 357.06 284.25 355.69 282.69C355.295 282.349 355.038 281.875 354.968 281.357C354.898 280.839 355.019 280.314 355.31 279.88C355.31 279.88 369.69 272.56 370.13 279.38C370.57 286.2 364.06 291.31 360.56 293Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M355.31 279.88C355.02 280.314 354.898 280.839 354.968 281.357C355.038 281.875 355.295 282.349 355.69 282.69C357.06 284.25 357.13 288.94 357.06 290.88C356.99 292.82 357.06 294.69 360.56 293C364.06 291.31 370.56 286.19 370.13 279.38C369.7 272.57 355.31 279.88 355.31 279.88Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M145.882 341.388C162.397 344.121 177.32 337.06 179.213 325.618C181.107 314.176 169.253 302.684 152.738 299.952C136.223 297.219 121.3 304.28 119.407 315.722C117.513 327.164 129.367 338.656 145.882 341.388Z" fill="url(#paint1_radial_1_32)" style=""/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_1_32" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(266.726 321.564) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_1_32" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(149.715 321.643) rotate(9.39525) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_1_32">
|
||||
<rect width="497" height="497" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 22 KiB |
|
|
@ -1,17 +0,0 @@
|
|||
<svg width="86" height="202" viewBox="0 0 86 202" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M56.6854 195.667C63.0485 191.195 71.8296 179.809 77.087 168.9C89.0428 144.091 92.2289 102.907 65.7859 86.2014C45.9684 73.9065 19.7285 85.9377 9.43434 106.872C2.48924 121.207 -0.100899 137.51 0.00299303 151.553C0.0550549 158.587 0.783319 165.136 2.01973 170.671C3.2404 176.136 5.01771 180.892 7.33744 184.111L7.42625 184.234L7.52449 184.348C13.8609 191.696 24.8872 200.228 37.3464 201.171C43.6642 201.649 50.288 200.162 56.6854 195.667Z" fill="#D9D9D9" style="fill:#D9D9D9;fill:color(display-p3 0.8510 0.8510 0.8510);fill-opacity:1;"/>
|
||||
<path d="M59.5 190.756C69.2084 179.099 75.5 169.756 79.5 158.256C55.0216 178.249 18.4869 138.632 7 122.256L2.5 150.256L6 175.756L14 188.756C22.988 194.631 28.2191 197.535 39.5 198.756C47.3937 198.19 51.7776 196.59 59.5 190.756Z" fill="#C4C4C4" style="fill:#C4C4C4;fill:color(display-p3 0.7675 0.7675 0.7675);fill-opacity:1;"/>
|
||||
<path d="M65.7859 86.2014C92.2289 102.907 89.0428 144.091 77.087 168.9C71.8296 179.809 63.0485 191.195 56.6854 195.667C50.288 200.162 43.6642 201.649 37.3464 201.171C24.8872 200.228 14 193.756 7.52449 184.348L7.42625 184.234L7.33744 184.111C5.01771 180.892 3.2404 176.136 2.01973 170.671C0.783319 165.136 0.0550549 158.587 0.00299303 151.553C-0.100899 137.51 2.48924 121.207 9.43434 106.872C19.7285 85.9377 45.9684 73.9065 65.7859 86.2014ZM62.7309 92.4309C18.5 64.7561 -5.5 152.756 14 184.111C19.5 191.756 26.5 195.256 37 196.756C44.1521 197.778 49.5 196.256 55.5 191.756C88.5 161.756 91.5 115.756 62.7309 92.4309Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<ellipse cx="52.9243" cy="148.889" rx="23.6644" ry="34.6366" transform="rotate(15.2136 52.9243 148.889)" fill="url(#paint0_linear_2705_199)" style=""/>
|
||||
<path d="M61.8821 115.948C67.9817 117.607 72.5741 122.742 75.0255 129.81C77.4759 136.875 77.7642 145.823 75.2772 154.968C72.7902 164.113 68.0103 171.682 62.3192 176.533C56.6259 181.386 50.0655 183.488 43.9659 181.829C37.8664 180.17 33.2739 175.035 30.8225 167.968C28.3721 160.902 28.0848 151.955 30.5717 142.81C33.0587 133.665 37.8377 126.096 43.5288 121.245C49.2221 116.392 55.7825 114.29 61.8821 115.948Z" stroke="black" stroke-opacity="0.04" style="stroke:black;stroke-opacity:0.04;"/>
|
||||
<path d="M63.4999 105.756C59.6929 111.276 52.1372 110.743 52.4999 105.756C54.5 78.256 48.8535 51.051 44.4999 31.756C43.2386 26.1657 50.0012 22.3638 56.4999 31.756C65.6128 44.9263 69.8913 96.4896 63.4999 105.756Z" fill="#CCCCCC" style="fill:#CCCCCC;fill:color(display-p3 0.8000 0.8000 0.8000);fill-opacity:1;"/>
|
||||
<path d="M47 24.756C48.5 24.256 51 23.7561 53.231 25.7452C73.5171 46.5549 69.6524 96.6664 65.4819 105.987C64.4309 110.126 60.9683 112.483 57.8619 112.256C56.3101 112.143 54.7629 111.597 53.5962 110.62C52.3998 109.618 50.8727 107.446 51.0035 105.648L51.1763 103.081C52.777 76.9845 49.876 50.8066 43.4175 33.1486C42.1049 29.6189 43.5884 26.4618 47 24.756ZM52 28.256C50.8336 26.9568 49.2098 26.1611 47.5 26.756C44.6798 27.7374 44.5 30.756 45.5 32.756C51.752 42.5329 55.5593 69.5264 54.1695 103.273L53.9956 105.865C53.9453 106.56 54.2225 107.143 54.7964 107.624C55.4003 108.13 56.3169 108.492 57.354 108.568C59.4256 108.719 61.447 107.747 62.0464 105.387C65.1582 98.5154 69.6073 47.8691 52 28.256Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M50.5 22.2561L47.5 38.2561L44.5 42.2561L41 40.7561L36 26.2561L45.5 14.2561L50.5 22.2561Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M11.8771 5.23835C11.3044 4.46433 11.4186 3.36941 12.216 2.82978C20.0877 -2.49724 33.4255 -0.56012 32.1836 10.8355C53.6363 5.53796 57.7794 29.8565 47.8743 41.3154C47.3637 41.9061 46.5303 42.0748 45.8019 41.7937V41.7937C44.3261 41.2242 44.022 39.123 44.9497 37.8418C51.4507 28.8638 48.3771 9.19044 29.916 17.316L28.5706 18.241C26.9453 19.3584 24.8549 17.6975 25.576 15.8616L26.1729 14.3424C29.7019 2.94735 22.8674 0.0733882 14.5317 5.64813C13.6712 6.22364 12.4928 6.07057 11.8771 5.23835V5.23835Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2705_199" x1="52.9243" y1="114.252" x2="59.7348" y2="183.17" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BABABA" style="stop-color:#BABABA;stop-color:color(display-p3 0.7275 0.7275 0.7275);stop-opacity:1;"/>
|
||||
<stop offset="0.829668" stop-color="#B3B3B3" style="stop-color:#B3B3B3;stop-color:color(display-p3 0.7020 0.7020 0.7020);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.5 KiB |
|
|
@ -1,59 +0,0 @@
|
|||
<svg width="497" height="497" viewBox="0 0 497 497" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2705_62)">
|
||||
<path d="M497 0H0V497H497V0Z" fill="#FF2602" style="fill:#FF2602;fill:color(display-p3 1.0000 0.1490 0.0078);fill-opacity:1;"/>
|
||||
<path d="M155.92 505C155.92 505 171.19 456 215.69 439C234.91 431.66 269.19 429.5 291.92 441.5C305.42 448.63 312.92 461.5 316.92 469.73C322.375 480.659 324.956 492.797 324.42 505H155.92Z" fill="#BCBCBC" style="fill:#BCBCBC;fill:color(display-p3 0.7373 0.7373 0.7373);fill-opacity:1;"/>
|
||||
<path d="M264.33 432.17C292.5 432.17 327.21 453.17 331.83 493.17C332.98 503.06 333.12 505.88 333.12 505.88H322.5C322.5 505.88 325.65 444.08 265 434.83C245.92 431.92 179.5 445.83 158.33 505.88H145.83C145.83 505.88 157 450 226.5 427.5C251.06 420.94 264.33 432.17 264.33 432.17Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M296 505.88C296 505.88 295.5 494.62 296.75 489.5C298 484.38 295.88 481.5 293.75 481.12C291.62 480.74 288.88 481.88 287.88 486.25C286.657 492.722 286.028 499.293 286 505.88H296Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M131.81 226.25C131.81 226.25 137.14 255.25 136.81 269.08C136.81 269.08 134.48 293.91 129.81 312.25C125.14 330.59 120.81 367.91 126.14 384.25C131.47 400.59 153.48 430.25 194.48 424.25C235.48 418.25 237.14 416.58 236.81 426.58C236.48 436.58 234.5 441.67 230.5 448C230.5 448 244.81 455.25 251.48 450.58L258.14 445.91C258.14 445.91 258.54 434.27 261.48 427.25C263.48 422.45 266.14 422.25 266.14 422.25C266.14 422.25 300.81 413.91 320.14 396.58C335.67 382.66 351.48 356.25 354.48 318.91C354.48 318.91 366.81 313.25 368.48 307.25L370.14 301.25C370.14 301.25 370.48 294.91 364.81 295.91C359.14 296.91 353.81 300.91 353.81 300.91C353.81 300.91 360.81 242.25 352.14 212.91C347.14 195.98 338.36 177.64 312.14 160.25C276.48 136.58 189.48 139.25 142.81 176.91C125.16 191.16 131.81 226.25 131.81 226.25Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M135.85 280C135.85 280 147.71 272.65 150.67 265.34C162.35 271.34 181.57 271.34 184.29 265.34C187.01 259.34 184.38 254.16 180.66 251.91C184 253.1 186.25 251.1 186.66 248.54C190.35 251.54 208.22 249.42 212.53 242.16C215.72 245.66 232.41 244.44 237.35 237.85C241.47 244.66 266.28 245.79 274.35 236.16C274.35 236.16 290.69 223.05 277.2 197.33C263.71 171.61 151 174.67 135.85 202.13C120.7 229.59 118.08 255.68 135.85 280Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M245.54 452.73C245.54 452.73 243.72 436 254 424.68C265 412.58 289 411.08 302.7 386.88C302.7 386.88 298.2 373.62 307.2 361.62C307.2 361.62 295.01 340 320.2 329.62L355.07 323.09C355.07 323.09 353.37 350.17 337.2 375.38C331.04 386.44 317.2 406.67 274.83 420.12C274.83 420.12 262.33 423.38 261.04 430.4C260.212 434.793 260.104 439.292 260.72 443.72C260.778 444.106 260.737 444.5 260.6 444.866C260.464 445.232 260.237 445.557 259.94 445.81C255.829 449.338 250.863 451.724 245.54 452.73V452.73Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M178.25 378.94C198 379.69 231.62 374.44 231.62 374.44C231.62 374.44 232 363.88 235.5 360C239 356.12 241.12 358.12 241.12 358.12C241.12 358.12 244 359.5 241.12 364.12C238.24 368.74 238.56 375.38 239.31 378.56C240.06 381.74 245 389.5 245 389.5C245.033 389.662 245.02 389.83 244.964 389.986C244.907 390.141 244.809 390.278 244.68 390.381C244.551 390.485 244.396 390.551 244.232 390.571C244.068 390.592 243.901 390.568 243.75 390.5C242.38 390.12 241.12 391.75 241.12 391.75C241.12 391.75 240.38 392.61 238.88 390.88C236.122 387.537 233.945 383.754 232.44 379.69C232.44 379.69 203.86 385.31 177.92 384.31C177.92 384.31 175.81 384.69 175.06 381.56C174.35 378.6 178.25 378.94 178.25 378.94Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M216.12 299C216.12 296.37 216.28 295 218.72 294.87C221.16 294.74 221.06 297.75 221.06 297.75C221.06 297.75 220.67 312.91 215.06 326.75C215.06 326.75 212.19 334.19 210.12 334.19C209.084 334.438 207.994 334.317 207.038 333.848C206.081 333.379 205.318 332.591 204.88 331.62C204.88 331.62 203.56 328.07 202.16 327.97C199.59 327.78 197.96 332.58 197.6 336.23C197.38 338.5 196.44 339.94 194.12 339.94C191.8 339.94 190.94 338.55 191.19 336.12C192.19 329.94 196.47 322.91 199.44 319.94C199.728 319.646 200.072 319.411 200.451 319.25C200.831 319.088 201.238 319.004 201.65 319C204.75 319 205.65 321.67 206.07 323.89C206.81 327.75 208.12 327.47 208.12 327.47C208.12 327.47 208.7 327.67 209.5 326.06C212.94 319.12 216.12 307.44 216.12 299Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M267.4 284.57C267.49 282.69 268.84 279.57 271.88 280C274.25 280.31 275.69 282 275.31 285.5C274.88 289.5 271.47 306.69 268.86 311.25C267.37 313.86 266 314.44 263.86 313.96C262.86 313.73 261.73 312.44 262.28 309.06C263 304.59 266.77 296.57 267.4 284.57Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M168.91 277.88C168.91 277.88 170.91 277.81 171.29 280.53C172.1 285.64 171.55 295.73 169.64 304.1C169.64 304.1 168.27 307.92 165.46 307.92C162.65 307.92 162.29 304.75 162.36 302.66C162.43 300.57 165.1 294.59 165.1 283.86C165.1 277.08 168.91 277.88 168.91 277.88Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M253.28 261.12C253.22 267.12 259.28 271.5 269.88 271.75C280.48 272 285.55 270.58 290.88 265.67C296.21 260.76 280.36 256.94 271.04 257.26C261.72 257.58 253.31 258 253.28 261.12Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M255.32 260C255.32 260 256.5 254.67 271.67 255.17C286.84 255.67 288.33 260 289.5 261C290.67 262 290.83 263.67 288 264.5C288 264.5 266.43 266.93 258.33 263.33C254.42 261.59 255.32 260 255.32 260Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M293.13 261.18C292.87 259.61 292.87 257.52 287.81 255.6C282.75 253.68 276.21 253.42 268.76 253.68C264.253 253.858 259.79 254.638 255.49 256C255.49 256 251.69 259.23 254.05 262.89C256.41 266.55 272.54 267.19 272.54 267.19C272.54 267.19 284.93 267.78 289.38 266.38C292.89 265.23 293.39 262.75 293.13 261.18ZM284.23 262.93C284.23 262.93 267.23 264.08 259.23 261.88C255.44 260.88 256.7 258.39 260.8 257.69C264.191 257.337 267.601 257.193 271.01 257.26C271.01 257.26 283.68 256.51 286.28 261.1C287.46 263.1 284.23 262.93 284.23 262.93Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M170 252.33C175 252.33 178.17 252.6 179.5 255.13C180.83 257.66 180.62 260.63 177.67 261.13C174.31 261.73 171.08 261.8 163.81 260.84C160.5 260.39 157.14 260.4 154.94 261.58C152.74 262.76 151.25 260.58 153.75 257.71C156.11 255 165 252.33 170 252.33Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M183.62 255.13C183.62 255.13 182.38 250 171.62 249.88C160.86 249.76 150.22 255.13 150.22 255.13C150.22 255.13 145.72 258.5 147.35 261.75C148.98 265 154.85 264.08 154.85 264.08C157.643 263.474 160.484 263.12 163.34 263.02C166.19 263.1 173.59 263.88 173.59 263.88C176.466 263.962 179.299 263.175 181.72 261.62C182.288 261.337 182.792 260.939 183.199 260.452C183.606 259.965 183.909 259.399 184.087 258.79C184.265 258.181 184.316 257.541 184.236 256.911C184.155 256.281 183.946 255.675 183.62 255.13ZM176.38 259.62C173.585 260.282 170.675 260.282 167.88 259.62C163.5 258.5 157.88 259.5 155.5 260.12C153.12 260.74 152.62 258.75 153.62 258C154.62 257.25 158.62 254.12 169.25 254.12C169.25 254.12 176.88 254.04 178.12 256.38C179.68 259.28 176.38 259.62 176.38 259.62Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M113.14 360.08C113.14 360.08 97.1401 353.75 97.4701 341.08C97.4701 341.08 98.4701 329.42 106.81 328.42C106.81 328.42 86.1401 328.75 86.1401 314.75C86.1401 314.75 86.1401 302.08 97.4701 302.42C97.4701 302.42 78.4701 294.42 78.4701 280.75C78.4701 268.08 85.8101 259.75 95.8101 259.75C95.8101 259.75 80.4701 251.08 80.4701 239.08C80.4701 239.08 79.4701 228.75 85.1401 223.75C85.1401 223.75 76.1401 203.08 83.4701 186.42C83.4701 186.42 86.1401 175.75 104.14 172.75C104.14 172.75 98.1401 156.42 107.47 145.75C107.47 145.75 112.47 137.75 125.47 137.08C125.47 137.08 127.47 121.26 141.47 117.08C141.47 117.08 157.03 116.08 168.69 121.08C168.69 121.08 177.14 98.4301 195.14 91.7601C195.14 91.7601 214.34 84.7601 228.34 97.3701C228.34 97.3701 230.47 76.0901 247.97 76.0901C265.47 76.0901 272.66 91.1701 272.66 91.1701C272.66 91.1701 277.72 78.0401 294.47 83.0901C312.14 88.4301 312.47 112.43 312.47 112.43C312.47 112.43 326.47 97.7601 345.81 103.09C353.92 105.33 357.14 116.76 354.81 125.43C354.81 125.43 362.47 117.09 369.81 120.43C377.15 123.77 391.47 136.09 382.81 156.76C382.81 156.76 401.47 154.43 410.14 169.09C410.14 169.09 424.14 188.76 407.14 203.76C407.14 203.76 428.47 202.09 432.47 228.09C432.47 228.09 434.14 248.76 413.47 255.43C413.47 255.43 430.81 256.76 430.47 272.76C430.47 272.76 432.77 288.76 411.77 295.76C411.77 295.76 423.43 304.76 421.1 316.43C417.1 336.43 391.43 334.43 391.43 334.43C391.43 334.43 405.43 339.09 403.43 347.43C403.43 347.43 399.77 357.43 386.1 355.43C386.1 355.43 395.1 364.43 387.1 371.76C379.1 379.09 369.43 371.76 369.43 371.76C369.43 371.76 374.81 387.89 364.08 392.23C353.56 396.49 344.52 388.41 342.86 386.08C341.2 383.75 346.43 396.76 332.1 398.08C317.77 399.4 316.43 385.08 316.43 385.08C316.43 385.08 317.43 371.75 329.1 374.42C329.1 374.42 320.43 371.42 321.1 363.42C321.77 355.42 330.77 352.42 330.77 352.42C330.77 352.42 318.77 358.08 317.1 348.42C315.43 338.76 325.43 328.75 325.43 328.75C325.43 328.75 311.43 330.42 310.1 316.75C310.1 316.75 309.77 301.75 325.77 296.08C325.77 296.08 309.1 293.75 309.43 275.08C309.76 256.41 326.1 250.42 326.1 250.42C326.1 250.42 316.77 257.42 307.43 249.75C298.09 242.08 302.1 229.08 302.1 229.08C302.1 229.08 298.43 240.84 283.1 237.08C267.77 233.32 271.77 219.08 271.77 219.08C271.77 219.08 263.43 234.08 250.77 232.42C238.11 230.76 237.1 215.75 237.1 215.75C237.1 215.75 232.1 224.42 225.43 223.75C225.43 223.75 212.77 224.08 213.43 205.75C213.43 205.75 213.1 223.08 195.77 226.42C178.44 229.76 177.1 212.75 177.1 212.75C177.1 212.75 179.1 227.42 162.43 233.75C145.76 240.08 143.43 225.42 143.43 225.42C143.43 225.42 148.1 236.08 139.43 248.42C130.76 260.76 131.1 255.42 131.1 255.42C132.79 262.396 133.354 269.597 132.77 276.75C131.77 288.42 125.1 305.08 121.77 324.42C119.756 336.082 118.972 347.924 119.43 359.75L113.14 360.08Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M331.19 358.25C335.93 356.62 335.62 353.81 333.31 352.19C331 350.57 327.13 351.56 326.19 352.94C325.25 354.32 323.05 354.05 321.75 351.81C317.63 344.7 324.31 335.12 328.69 333.69C334.69 331.69 332.38 328.25 332.38 328.25C332.01 327.648 331.462 327.174 330.812 326.895C330.162 326.616 329.442 326.544 328.75 326.69C326.94 327.12 326.12 327.88 324.75 327.88C320 327.88 315.75 323.5 315.88 317.44C316.01 311.38 321.56 302.31 326.81 301.5C332.06 300.69 331.28 297.95 331.28 297.95C331.28 297.95 331.91 295.88 326.81 294.81C317.34 292.81 314 289 313.63 277C313.26 265 318.75 257.56 324.44 256.21C333.68 254.02 332 248.31 331.38 247.62C330.828 247.254 330.156 247.115 329.503 247.232C328.851 247.349 328.27 247.714 327.88 248.25C327.19 249.5 324.1 254.15 315.04 251.08C300.68 246.23 306.94 231.08 307.94 228.38C308.94 225.68 304.88 225.94 304.88 225.94C304.88 225.94 300.5 225.62 299.81 227.62C298.94 230.17 294.44 238.81 285.69 237.06C274.85 234.9 274.81 224.94 275.12 221.56C275.43 218.18 274.81 217.88 273.12 217.88C271.43 217.88 269.69 217.81 269 220.69C266.43 231.44 251.94 231.88 251.94 231.88C238.44 231.56 239.18 215.15 239.75 210.88C240.44 205.75 240.44 205.79 239.49 205.88C238 205.97 236.38 209.28 236.38 211.41C236.481 213.484 235.981 215.543 234.94 217.34C233.75 219.34 231.25 223.65 226.31 223.47C221.37 223.29 218.75 218.47 218.69 213.59C218.63 209.34 218.88 204.59 218.88 204.59C218.81 203.28 218 201.72 216.69 202.28C215.38 202.84 214.19 203.41 212.31 204.41C211.77 204.729 211.339 205.203 211.071 205.77C210.804 206.337 210.713 206.971 210.81 207.59L211 210.97C211.06 214.47 207.89 218.66 205.62 220.72C198.75 226.97 191.7 226.91 188.27 223.41C184.84 219.91 184.19 218.34 183.27 214.47C182.74 212.28 181.27 211.72 180.46 212.03C179.65 212.34 176.71 213.65 176.71 213.65C176.437 213.798 176.198 214 176.007 214.244C175.816 214.489 175.678 214.77 175.601 215.071C175.523 215.371 175.509 215.685 175.559 215.991C175.609 216.297 175.722 216.589 175.89 216.85C176.62 218.29 174.82 221.85 174.08 222.91C171.94 226.13 165.4 232.47 160.02 233.36C154.43 234.29 151.35 228.36 149.85 225.36C148.35 222.36 145.02 223.53 142.02 225.36C139.02 227.19 139.85 229.36 139.85 229.36C141.85 235.64 139.64 243.91 137.52 247.7C132.19 257.21 125.52 254.7 123.52 254.15C121.52 253.6 120.02 255.7 122.69 258.86C125.36 262.02 132.1 260.61 132.1 260.61C132.1 260.61 126.85 291.2 122.19 308.7C117.53 326.2 114.52 348.36 114.52 359.7C114.52 371.04 116.69 401.86 145.35 418.7C174.01 435.54 199.52 432.53 208.35 431.03C208.35 431.03 221.02 428.52 226.19 425.53C226.19 425.53 228.98 423.65 229.19 427.53C229.52 433.78 228.5 440.53 223.85 447.2C223.85 447.2 219.19 454.63 241.52 454.63C250.19 454.63 255.69 453.54 265.85 446.03C265.85 446.03 267.85 445.36 267.35 440.36C266.85 435.36 265.68 429.86 266.43 426.36C266.43 426.36 304.08 418.59 322.07 400.44L322.45 393.6C316.79 385.23 326.14 378.78 327.21 379.34C328.28 379.9 328.96 380.03 331.21 379.78C333.46 379.53 333.71 378.41 333.27 377.03C332.83 375.65 330.96 374.15 327.4 372.72C323.84 371.29 327.38 359.56 331.19 358.25ZM321.08 359.19C317.65 361.62 317.33 368.33 320.4 371.52C321.34 372.52 321.28 373.52 320.4 373.77C311.92 376.03 310.87 385.54 315.81 393.85C317.46 396.62 316.56 397.27 316.56 397.27C300.31 413.02 278.19 419.5 267.62 421.56C264.52 422.17 262.38 423.44 261.15 428.1C259.63 433.86 260.23 441.27 260.23 441.27C248.23 451.27 231.31 448.69 233.31 446.19C237.122 441.425 237.512 432.936 238.25 427.38C239.44 418.44 232.82 413.82 222.53 417.5C200.6 425.33 174.98 425.25 159.81 418.44C137.55 408.44 127.42 393.23 125.31 371.94C123.2 350.65 123.92 343.5 131.31 307.86C138.7 272.22 136.56 257.15 136.56 257.15C147.19 253.15 148.5 236.25 148.5 236.25C162.81 246.88 180.75 223.88 180.75 223.88C188.81 235.31 206.06 231.14 213.88 219.31C215.25 227.88 232.88 232.44 237.25 218.75C235.19 235.06 255.94 243.25 269.5 227.88C270.18 227.1 270.82 227.5 271 228.44C273.65 242.5 288.73 245.35 297.4 240.44C299.99 238.96 299.94 240.5 299.94 240.5C298.86 251.24 313.88 255.5 317.09 255.2C317.9 255.14 318.09 255.75 317.69 255.98C311.59 259.98 308.39 266.54 307.46 273.37C306.13 283.17 310.63 293.04 316.88 296.14C320.16 297.76 318.23 299.14 318.23 299.14C310.9 305.06 308.56 312.93 308.48 316.73C308.4 320.53 310.81 330.14 317.81 330.98C320.3 331.27 318.98 332.89 318.98 332.89C313.13 340.33 313.65 354.56 319.48 357.23C322.07 358.37 322.21 358.39 321.08 359.19Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M116.48 365.51C97.26 364.19 83.38 349.08 93 331.67C80.38 327.62 75.25 312.25 85.25 304.12C73.12 298.62 63.62 277.5 84.62 257.38C72.24 249.38 73.75 232.38 82 224.5C74 218.25 72.88 178 98.75 173.5C92.88 154.12 111 134.24 122.25 137C124.88 122.76 144.25 106 165.62 118C167.92 119.29 168.51 119.54 169.5 116.25C172.25 107.12 198.5 75.01 223.12 92.88C227.5 95.38 227.93 94.88 228 91.25C228.25 78.88 256.53 64.5 270.75 86.25C272.88 89.5 273.12 89 275.25 86.38C277.38 83.76 303.12 67.25 315.75 105.88C339.12 93.12 359.25 103 359.25 120.38C378.12 113.5 394 135.75 390.12 155.62C413.5 155.88 428.5 179.25 417.88 203.12C442.88 213.25 447.5 245.5 422.5 255.12C442.88 265.75 438.92 290.48 420.12 299.88C418.62 300.62 419.03 301.19 419.5 301.5C425.5 305.5 434.85 336.44 404.5 338.12C400 338.38 400.38 339.88 401.75 341.12C403.12 342.36 415.12 357.12 394.5 359.75C401.38 375.12 384.5 382.5 377.38 381.25C379.25 389.75 366 400.88 348.62 395.5C346.38 404.73 324.88 404 319 398.5C317.873 397.546 316.965 396.36 316.34 395.022C315.715 393.684 315.388 392.227 315.38 390.75L320.63 388.6C320.63 388.6 320.89 396.84 329.73 396.73C338.73 396.61 340.54 392.98 340.02 387.73C339.75 384.98 341.75 385.6 341.75 385.6C341.75 385.6 344.62 386.1 345.75 384.86C346.88 383.62 348.25 383.86 348.75 385.1C349.25 386.34 352.88 394.1 362.62 389.48C372.36 384.86 368 378.25 367 376.38C366 374.51 366.12 372.38 367 372.12C368.605 372.008 370.109 371.294 371.21 370.12C372.6 368.48 373.97 372.4 375.28 373.83C376.41 375.04 385.28 374.56 385.28 364.92C385.28 358.16 381.78 355.04 381.78 355.04C381.78 355.04 399.65 354.54 399.91 348.16C400.17 341.78 393.51 340.76 391.28 340.16C390.227 339.903 389.312 339.254 388.721 338.345C388.13 337.437 387.908 336.337 388.099 335.271C388.29 334.204 388.881 333.25 389.75 332.603C390.62 331.957 391.703 331.666 392.78 331.79C396.89 332.18 417.15 331.17 417.15 314.79C417.15 303.29 410.28 302.16 410.28 302.16C405.53 300.79 407.03 292.36 411.96 292.44C418.53 292.54 426.91 283.66 426.65 272.92C426.65 272.92 428.2 255.68 412.03 258.79C405.53 260.04 406.78 255.92 409.15 253.79C411.52 251.66 437.69 235.58 424.91 218.54C415.53 206.04 407.86 209.54 407.86 209.54C403.53 210.93 400.57 203.54 404.91 200.3C410.15 196.43 421.53 164.93 389.28 162.43C387.15 162.26 386.34 162.95 385.65 164.43C384.96 165.91 382.91 165.15 382.78 163.75C382.68 162.63 382.25 162.39 381.27 162.75C380.29 163.11 376.78 162.22 380.65 156.35C385.49 149.02 378.91 124.88 366.15 124.6C360.53 124.48 355.49 129.43 353.15 129.69C351.5 129.88 349.62 128.89 350.78 122.1C351.53 117.72 348.56 105.18 331.28 107.1C324.53 107.85 318.94 109.25 315.53 114.1C313.9 116.41 312.46 116.96 310.15 116.85C306.97 116.7 305.41 114.1 305.91 108.35C306.41 102.6 299.66 87.48 287.91 87.48C282.15 87.48 275.91 92.1 272.03 96.35C268.28 90.72 258.61 74.77 241.53 82.48C231.25 87 230.62 97.62 230.75 99.75C230.88 101.88 228.5 102.88 226.12 101C223.74 99.12 198 76.5 174.88 118C170.74 125.43 169.48 127.56 163.88 123.88C159.5 121 133.34 114.49 129.75 137.38C128.75 143.75 127.25 144.38 119.15 144.13C113.28 143.95 100.88 155.75 108.88 176.75C88.38 181.12 83.56 188.75 83.88 204.25C84.12 216.75 85.6201 221.79 88.6201 224.12C88.6201 224.12 89.75 224.25 87.88 227.38C86.01 230.51 77.8201 247.91 102.12 259.12C103.75 259.88 104 263.38 101 263.38C98 263.38 81.88 269.25 82.12 282.25C82.36 295.25 94.0001 301 99.1201 301.25C104.24 301.5 102.5 306.88 99.25 306.75C96 306.62 90.2501 310.38 90.1201 316.12C89.9901 321.86 93 326.22 101.5 326.5C107.37 326.69 111.21 329.14 111.88 330.62C113 333.12 108 331.38 105.62 332.62C103.24 333.86 95.48 352.94 115.16 359.22L116.48 365.51Z" fill="#060500" style="fill:#060500;fill:color(display-p3 0.0235 0.0196 0.0000);fill-opacity:1;"/>
|
||||
<path d="M266.69 341.67C283.43 341.67 297 332.268 297 320.67C297 309.072 283.43 299.67 266.69 299.67C249.95 299.67 236.38 309.072 236.38 320.67C236.38 332.268 249.95 341.67 266.69 341.67Z" fill="url(#paint0_radial_2705_62)" style=""/>
|
||||
<path d="M365.31 267.69C360.31 267.44 353.69 268.25 350.69 271C351.75 261.75 348 261.18 345 261C342 260.82 342.25 263.79 342.25 263.79C342.25 263.79 349.5 274.54 346.25 315.79C349.238 314.474 351.828 312.396 353.758 309.763C355.689 307.13 356.893 304.036 357.25 300.79C357.25 300.79 362 298.34 365.5 298.44C369 298.54 380 295.54 379.75 284.04C379.5 272.54 370.31 267.94 365.31 267.69ZM360.56 293C357.06 294.69 357 292.81 357.06 290.88C357.12 288.95 357.06 284.25 355.69 282.69C355.295 282.349 355.038 281.875 354.968 281.357C354.898 280.839 355.019 280.314 355.31 279.88C355.31 279.88 369.69 272.56 370.12 279.38C370.55 286.2 364.06 291.31 360.56 293Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M355.31 279.88C355.02 280.314 354.898 280.839 354.968 281.357C355.038 281.875 355.295 282.349 355.69 282.69C357.06 284.25 357.12 288.94 357.06 290.88C357 292.82 357.06 294.69 360.56 293C364.06 291.31 370.56 286.19 370.12 279.38C369.68 272.57 355.31 279.88 355.31 279.88Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M145.882 341.388C162.397 344.121 177.32 337.06 179.213 325.618C181.107 314.176 169.253 302.684 152.738 299.952C136.223 297.219 121.3 304.28 119.407 315.722C117.513 327.164 129.367 338.656 145.882 341.388Z" fill="url(#paint1_radial_2705_62)" style=""/>
|
||||
<path d="M379.685 365.407C386.049 360.935 394.83 349.549 400.087 338.64C412.043 313.831 415.229 272.647 388.786 255.941C368.968 243.646 342.728 255.678 332.434 276.612C325.489 290.947 322.899 307.25 323.003 321.293C323.055 328.327 323.783 334.876 325.02 340.411C326.24 345.876 328.018 350.632 330.337 353.851L330.426 353.974L330.524 354.088C336.861 361.436 347.887 369.968 360.346 370.911C366.664 371.389 373.288 369.902 379.685 365.407Z" fill="#D9D9D9" style="fill:#D9D9D9;fill:color(display-p3 0.8510 0.8510 0.8510);fill-opacity:1;"/>
|
||||
<path d="M382.5 360.496C392.208 348.839 398.5 339.496 402.5 327.996C378.022 347.989 341.487 308.372 330 291.996L325.5 319.996L329 345.496L337 358.496C345.988 364.371 351.219 367.275 362.5 368.496C370.394 367.93 374.778 366.33 382.5 360.496Z" fill="#C4C4C4" style="fill:#C4C4C4;fill:color(display-p3 0.7675 0.7675 0.7675);fill-opacity:1;"/>
|
||||
<path d="M388.786 255.941C415.229 272.647 412.043 313.831 400.087 338.64C394.83 349.549 386.049 360.935 379.685 365.407C373.288 369.902 366.664 371.389 360.346 370.911C347.887 369.968 337 363.496 330.524 354.088L330.426 353.974L330.337 353.851C328.018 350.632 326.24 345.876 325.02 340.411C323.783 334.876 323.055 328.327 323.003 321.293C322.899 307.25 325.489 290.947 332.434 276.612C342.728 255.678 368.968 243.646 388.786 255.941ZM385.731 262.171C341.5 234.496 317.5 322.496 337 353.851C342.5 361.496 349.5 364.996 360 366.496C367.152 367.518 372.5 365.996 378.5 361.496C411.5 331.496 414.5 285.496 385.731 262.171Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<ellipse cx="375.924" cy="318.629" rx="23.6644" ry="34.6366" transform="rotate(15.2136 375.924 318.629)" fill="url(#paint2_linear_2705_62)" style=""/>
|
||||
<path d="M384.882 285.688C390.982 287.347 395.574 292.482 398.026 299.55C400.476 306.615 400.764 315.563 398.277 324.708C395.79 333.853 391.01 341.422 385.319 346.273C379.626 351.126 373.065 353.228 366.966 351.569C360.866 349.91 356.274 344.775 353.823 337.708C351.372 330.642 351.085 321.695 353.572 312.55C356.059 303.405 360.838 295.836 366.529 290.985C372.222 286.132 378.782 284.03 384.882 285.688Z" stroke="black" stroke-opacity="0.04" style="stroke:black;stroke-opacity:0.04;"/>
|
||||
<path d="M386.5 275.496C382.693 281.016 375.137 280.483 375.5 275.496C377.5 247.996 371.854 220.791 367.5 201.496C366.239 195.906 373.001 192.104 379.5 201.496C388.613 214.666 392.891 266.23 386.5 275.496Z" fill="#CCCCCC" style="fill:#CCCCCC;fill:color(display-p3 0.8000 0.8000 0.8000);fill-opacity:1;"/>
|
||||
<path d="M370 194.496C371.5 193.996 374 193.496 376.231 195.485C396.517 216.295 392.652 266.406 388.482 275.727C387.431 279.866 383.968 282.223 380.862 281.996C379.31 281.883 377.763 281.337 376.596 280.36C375.4 279.358 373.873 277.186 374.003 275.388L374.176 272.821C375.777 246.725 372.876 220.547 366.418 202.889C365.105 199.359 366.588 196.202 370 194.496ZM375 197.996C373.834 196.697 372.21 195.901 370.5 196.496C367.68 197.477 367.5 200.496 368.5 202.496C374.752 212.273 378.559 239.266 377.169 273.013L376.996 275.605C376.945 276.3 377.222 276.883 377.796 277.364C378.4 277.87 379.317 278.232 380.354 278.308C382.426 278.459 384.447 277.487 385.046 275.127C388.158 268.255 392.607 217.609 375 197.996Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M373.5 191.996L370.5 207.996L367.5 211.996L364 210.496L359 195.996L368.5 183.996L373.5 191.996Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M334.877 174.978C334.304 174.204 334.419 173.109 335.216 172.57C343.088 167.243 356.425 169.18 355.184 180.576C376.636 175.278 380.779 199.596 370.874 211.055C370.364 211.646 369.53 211.815 368.802 211.534V211.534C367.326 210.964 367.022 208.863 367.95 207.582C374.451 198.604 371.377 178.93 352.916 187.056L351.571 187.981C349.945 189.098 347.855 187.437 348.576 185.602L349.173 184.082C352.702 172.687 345.867 169.813 337.532 175.388C336.671 175.964 335.493 175.811 334.877 174.978V174.978Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_2705_62" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(267.775 321.564) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_2705_62" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(149.715 321.643) rotate(9.39525) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_2705_62" x1="375.924" y1="283.992" x2="382.735" y2="352.91" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BABABA" style="stop-color:#BABABA;stop-color:color(display-p3 0.7275 0.7275 0.7275);stop-opacity:1;"/>
|
||||
<stop offset="0.829668" stop-color="#B3B3B3" style="stop-color:#B3B3B3;stop-color:color(display-p3 0.7020 0.7020 0.7020);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2705_62">
|
||||
<rect width="497" height="497" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
|
@ -1,59 +0,0 @@
|
|||
<svg width="497" height="497" viewBox="0 0 497 497" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2704_2)">
|
||||
<path d="M497 0H0V497H497V0Z" fill="#FF2602" style="fill:#FF2602;fill:color(display-p3 1.0000 0.1490 0.0078);fill-opacity:1;"/>
|
||||
<path d="M155.92 497C155.92 497 171.19 448 215.69 431C234.91 423.66 269.19 421.5 291.92 433.5C305.42 440.63 312.92 453.5 316.92 461.73C322.374 472.659 324.956 484.797 324.42 497H155.92Z" fill="#BCBCBC" style="fill:#BCBCBC;fill:color(display-p3 0.7373 0.7373 0.7373);fill-opacity:1;"/>
|
||||
<path d="M264.33 424.17C292.5 424.17 327.21 445.17 331.83 485.17C332.98 495.06 333.12 497.88 333.12 497.88H322.5C322.5 497.88 325.65 436.08 265 426.83C245.92 423.92 179.5 437.83 158.33 497.88H145.83C145.83 497.88 157 442 226.5 419.5C251.06 412.94 264.33 424.17 264.33 424.17Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M296 497.88C296 497.88 295.5 486.62 296.75 481.5C298 476.38 295.88 473.5 293.75 473.12C291.62 472.74 288.88 473.88 287.88 478.25C286.657 484.722 286.028 491.293 286 497.88H296Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M131.81 207.25C131.81 207.25 137.14 236.25 136.81 250.08C136.81 250.08 134.48 274.91 129.81 293.25C125.14 311.59 120.81 348.91 126.14 365.25C131.47 381.59 153.48 411.25 194.48 405.25C235.48 399.25 237.14 397.58 236.81 407.58C236.48 417.58 235.48 427.58 231.48 433.91C231.48 433.91 244.81 436.25 251.48 431.58L258.14 426.91C258.14 426.91 258.54 415.27 261.48 408.25C263.48 403.45 266.14 403.25 266.14 403.25C266.14 403.25 300.81 394.91 320.14 377.58C335.67 363.66 351.48 337.25 354.48 299.91C354.48 299.91 366.81 294.25 368.48 288.25L370.14 282.25C370.14 282.25 370.48 275.91 364.81 276.91C359.14 277.91 353.81 281.91 353.81 281.91C353.81 281.91 360.81 223.25 352.14 193.91C347.14 176.98 338.36 158.64 312.14 141.25C276.48 117.58 189.48 120.25 142.81 157.91C125.16 172.16 131.81 207.25 131.81 207.25Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M135.85 261C135.85 261 147.71 253.65 150.67 246.34C162.35 252.34 181.57 252.34 184.29 246.34C187.01 240.34 184.38 235.16 180.66 232.91C184 234.1 186.25 232.1 186.66 229.54C190.35 232.54 208.22 230.42 212.53 223.16C215.72 226.66 232.41 225.44 237.35 218.85C241.47 225.66 266.28 226.79 274.35 217.16C274.35 217.16 290.69 204.05 277.2 178.33C263.71 152.61 151 155.67 135.85 183.13C120.7 210.59 118.08 236.68 135.85 261Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M245.54 433.73C245.54 433.73 243.72 417 254 405.68C265 393.58 289 392.08 302.7 367.88C302.7 367.88 298.2 354.62 307.2 342.62C307.2 342.62 295.01 321 320.2 310.62L355.07 304.09C355.07 304.09 353.37 331.17 337.2 356.38C331.04 367.44 317.2 387.67 274.83 401.12C274.83 401.12 262.33 404.38 261.04 411.4C260.212 415.793 260.104 420.292 260.72 424.72C260.778 425.106 260.737 425.5 260.6 425.866C260.464 426.232 260.237 426.557 259.94 426.81C255.829 430.338 250.863 432.724 245.54 433.73Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M178.25 359.94C198 360.69 231.62 355.44 231.62 355.44C231.62 355.44 232 344.88 235.5 341C239 337.12 241.12 339.12 241.12 339.12C241.12 339.12 244 340.5 241.12 345.12C238.24 349.74 238.56 356.38 239.31 359.56C240.06 362.74 245 370.5 245 370.5C245.033 370.662 245.02 370.83 244.964 370.986C244.908 371.141 244.809 371.278 244.68 371.381C244.551 371.485 244.396 371.551 244.232 371.571C244.068 371.592 243.901 371.568 243.75 371.5C242.38 371.12 241.12 372.75 241.12 372.75C241.12 372.75 240.38 373.61 238.88 371.88C236.122 368.537 233.945 364.754 232.44 360.69C232.44 360.69 203.86 366.31 177.92 365.31C177.92 365.31 175.81 365.69 175.06 362.56C174.35 359.6 178.25 359.94 178.25 359.94Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M216.12 280C216.12 277.37 216.28 276 218.72 275.87C221.16 275.74 221.06 278.75 221.06 278.75C221.06 278.75 220.67 293.91 215.06 307.75C215.06 307.75 212.19 315.19 210.12 315.19C209.084 315.438 207.994 315.317 207.038 314.848C206.081 314.379 205.318 313.591 204.88 312.62C204.88 312.62 203.56 309.07 202.16 308.97C199.59 308.78 197.96 313.58 197.6 317.23C197.38 319.5 196.44 320.94 194.12 320.94C191.8 320.94 190.94 319.55 191.19 317.12C192.19 310.94 196.47 303.91 199.44 300.94C199.728 300.646 200.072 300.411 200.451 300.25C200.831 300.088 201.238 300.004 201.65 300C204.75 300 205.65 302.67 206.07 304.89C206.81 308.75 208.12 308.47 208.12 308.47C208.12 308.47 208.7 308.67 209.5 307.06C212.94 300.12 216.12 288.44 216.12 280Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M267.4 265.57C267.49 263.69 268.84 260.57 271.88 261C274.25 261.31 275.69 263 275.31 266.5C274.88 270.5 271.47 287.69 268.86 292.25C267.37 294.86 266 295.44 263.86 294.96C262.86 294.73 261.73 293.44 262.28 290.06C263 285.59 266.77 277.57 267.4 265.57Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M168.91 258.88C168.91 258.88 170.91 258.81 171.29 261.53C172.1 266.64 171.55 276.73 169.64 285.1C169.64 285.1 168.27 288.92 165.46 288.92C162.65 288.92 162.29 285.75 162.36 283.66C162.43 281.57 165.1 275.59 165.1 264.86C165.1 258.08 168.91 258.88 168.91 258.88Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M253.28 242.12C253.22 248.12 259.28 252.5 269.88 252.75C280.48 253 285.55 251.58 290.88 246.67C296.21 241.76 280.36 237.94 271.04 238.26C261.72 238.58 253.31 239 253.28 242.12Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M255.32 241C255.32 241 256.5 235.67 271.67 236.17C286.84 236.67 288.33 241 289.5 242C290.67 243 290.83 244.67 288 245.5C288 245.5 266.43 247.93 258.33 244.33C254.42 242.59 255.32 241 255.32 241Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M293.13 242.18C292.87 240.61 292.87 238.52 287.81 236.6C282.75 234.68 276.21 234.42 268.76 234.68C264.253 234.858 259.79 235.638 255.49 237C255.49 237 251.69 240.23 254.05 243.89C256.41 247.55 272.54 248.19 272.54 248.19C272.54 248.19 284.93 248.78 289.38 247.38C292.89 246.23 293.39 243.75 293.13 242.18ZM284.23 243.93C284.23 243.93 267.23 245.08 259.23 242.88C255.44 241.88 256.7 239.39 260.8 238.69C264.191 238.337 267.601 238.193 271.01 238.26C271.01 238.26 283.68 237.51 286.28 242.1C287.46 244.1 284.23 243.93 284.23 243.93Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M170 233.33C175 233.33 178.17 233.6 179.5 236.13C180.83 238.66 180.62 241.63 177.67 242.13C174.31 242.73 171.08 242.8 163.81 241.84C160.5 241.39 157.14 241.4 154.94 242.58C152.74 243.76 151.25 241.58 153.75 238.71C156.11 236 165 233.33 170 233.33Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M183.62 236.13C183.62 236.13 182.38 231 171.62 230.88C160.86 230.76 150.22 236.13 150.22 236.13C150.22 236.13 145.72 239.5 147.35 242.75C148.98 246 154.85 245.08 154.85 245.08C157.643 244.474 160.484 244.12 163.34 244.02C166.19 244.1 173.59 244.88 173.59 244.88C176.466 244.962 179.299 244.175 181.72 242.62C182.288 242.337 182.792 241.939 183.199 241.452C183.606 240.965 183.909 240.399 184.087 239.79C184.265 239.181 184.316 238.541 184.236 237.911C184.155 237.281 183.946 236.675 183.62 236.13ZM176.38 240.62C173.585 241.282 170.675 241.282 167.88 240.62C163.5 239.5 157.88 240.5 155.5 241.12C153.12 241.74 152.62 239.75 153.62 239C154.62 238.25 158.62 235.12 169.25 235.12C169.25 235.12 176.88 235.04 178.12 237.38C179.68 240.28 176.38 240.62 176.38 240.62Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M113.14 341.08C113.14 341.08 97.1401 334.75 97.4701 322.08C97.4701 322.08 98.4701 310.42 106.81 309.42C106.81 309.42 86.1401 309.75 86.1401 295.75C86.1401 295.75 86.1401 283.08 97.4701 283.42C97.4701 283.42 78.4701 275.42 78.4701 261.75C78.4701 249.08 85.8101 240.75 95.8101 240.75C95.8101 240.75 80.4701 232.08 80.4701 220.08C80.4701 220.08 79.4701 209.75 85.1401 204.75C85.1401 204.75 76.1401 184.08 83.4701 167.42C83.4701 167.42 86.1401 156.75 104.14 153.75C104.14 153.75 98.1401 137.42 107.47 126.75C107.47 126.75 112.47 118.75 125.47 118.08C125.47 118.08 127.47 102.26 141.47 98.0801C141.47 98.0801 157.03 97.0801 168.69 102.08C168.69 102.08 177.14 79.4301 195.14 72.7601C195.14 72.7601 214.34 65.7601 228.34 78.3701C228.34 78.3701 230.47 57.0901 247.97 57.0901C265.47 57.0901 272.66 72.1701 272.66 72.1701C272.66 72.1701 277.72 59.0401 294.47 64.0901C312.14 69.4301 312.47 93.4301 312.47 93.4301C312.47 93.4301 326.47 78.7601 345.81 84.0901C353.92 86.3301 357.14 97.7601 354.81 106.43C354.81 106.43 362.47 98.0901 369.81 101.43C377.15 104.77 391.47 117.09 382.81 137.76C382.81 137.76 401.47 135.43 410.14 150.09C410.14 150.09 424.14 169.76 407.14 184.76C407.14 184.76 428.47 183.09 432.47 209.09C432.47 209.09 434.14 229.76 413.47 236.43C413.47 236.43 430.81 237.76 430.47 253.76C430.47 253.76 432.77 269.76 411.77 276.76C411.77 276.76 423.43 285.76 421.1 297.43C417.1 317.43 391.43 315.43 391.43 315.43C391.43 315.43 405.43 320.09 403.43 328.43C403.43 328.43 399.77 338.43 386.1 336.43C386.1 336.43 395.1 345.43 387.1 352.76C379.1 360.09 369.43 352.76 369.43 352.76C369.43 352.76 374.81 368.89 364.08 373.23C353.56 377.49 344.52 369.41 342.86 367.08C341.2 364.75 346.43 377.76 332.1 379.08C317.77 380.4 316.43 366.08 316.43 366.08C316.43 366.08 317.43 352.75 329.1 355.42C329.1 355.42 320.43 352.42 321.1 344.42C321.77 336.42 330.77 333.42 330.77 333.42C330.77 333.42 318.77 339.08 317.1 329.42C315.43 319.76 325.43 309.75 325.43 309.75C325.43 309.75 311.43 311.42 310.1 297.75C310.1 297.75 309.77 282.75 325.77 277.08C325.77 277.08 309.1 274.75 309.43 256.08C309.76 237.41 326.1 231.42 326.1 231.42C326.1 231.42 316.77 238.42 307.43 230.75C298.09 223.08 302.1 210.08 302.1 210.08C302.1 210.08 298.43 221.84 283.1 218.08C267.77 214.32 271.77 200.08 271.77 200.08C271.77 200.08 263.43 215.08 250.77 213.42C238.11 211.76 237.1 196.75 237.1 196.75C237.1 196.75 232.1 205.42 225.43 204.75C225.43 204.75 212.77 205.08 213.43 186.75C213.43 186.75 213.1 204.08 195.77 207.42C178.44 210.76 177.1 193.75 177.1 193.75C177.1 193.75 179.1 208.42 162.43 214.75C145.76 221.08 143.43 206.42 143.43 206.42C143.43 206.42 148.1 217.08 139.43 229.42C130.76 241.76 131.1 236.42 131.1 236.42C132.79 243.396 133.354 250.597 132.77 257.75C131.77 269.42 125.1 286.08 121.77 305.42C119.756 317.082 118.972 328.924 119.43 340.75L113.14 341.08Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M331.19 339.25C335.93 337.62 335.62 334.81 333.31 333.19C331 331.57 327.13 332.56 326.19 333.94C325.25 335.32 323.05 335.05 321.75 332.81C317.63 325.7 324.31 316.12 328.69 314.69C334.69 312.69 332.38 309.25 332.38 309.25C332.01 308.648 331.462 308.174 330.812 307.895C330.162 307.616 329.442 307.544 328.75 307.69C326.94 308.12 326.12 308.88 324.75 308.88C320 308.88 315.75 304.5 315.88 298.44C316.01 292.38 321.56 283.31 326.81 282.5C332.06 281.69 331.28 278.95 331.28 278.95C331.28 278.95 331.91 276.88 326.81 275.81C317.34 273.81 314 270 313.63 258C313.26 246 318.75 238.56 324.44 237.21C333.68 235.02 332 229.31 331.38 228.62C330.828 228.254 330.156 228.115 329.503 228.232C328.851 228.349 328.27 228.714 327.88 229.25C327.19 230.5 324.1 235.15 315.04 232.08C300.68 227.23 306.94 212.08 307.94 209.38C308.94 206.68 304.88 206.94 304.88 206.94C304.88 206.94 300.5 206.62 299.81 208.62C298.94 211.17 294.44 219.81 285.69 218.06C274.85 215.9 274.81 205.94 275.12 202.56C275.43 199.18 274.81 198.88 273.12 198.88C271.43 198.88 269.69 198.81 269 201.69C266.43 212.44 251.94 212.88 251.94 212.88C238.44 212.56 239.18 196.15 239.75 191.88C240.44 186.75 240.44 186.79 239.49 186.88C238 186.97 236.38 190.28 236.38 192.41C236.481 194.484 235.981 196.543 234.94 198.34C233.75 200.34 231.25 204.65 226.31 204.47C221.37 204.29 218.75 199.47 218.69 194.59C218.63 190.34 218.88 185.59 218.88 185.59C218.81 184.28 218 182.72 216.69 183.28C215.38 183.84 214.19 184.41 212.31 185.41C211.77 185.729 211.339 186.203 211.071 186.77C210.804 187.337 210.713 187.971 210.81 188.59L211 191.97C211.06 195.47 207.89 199.66 205.62 201.72C198.75 207.97 191.7 207.91 188.27 204.41C184.84 200.91 184.19 199.34 183.27 195.47C182.74 193.28 181.27 192.72 180.46 193.03C179.65 193.34 176.71 194.65 176.71 194.65C176.437 194.798 176.198 195 176.007 195.244C175.816 195.489 175.678 195.77 175.601 196.071C175.523 196.371 175.509 196.685 175.559 196.991C175.609 197.297 175.722 197.589 175.89 197.85C176.62 199.29 174.82 202.85 174.08 203.91C171.94 207.13 165.4 213.47 160.02 214.36C154.43 215.29 151.35 209.36 149.85 206.36C148.35 203.36 145.02 204.53 142.02 206.36C139.02 208.19 139.85 210.36 139.85 210.36C141.85 216.64 139.64 224.91 137.52 228.7C132.19 238.21 125.52 235.7 123.52 235.15C121.52 234.6 120.02 236.7 122.69 239.86C125.36 243.02 132.1 241.61 132.1 241.61C132.1 241.61 126.85 272.2 122.19 289.7C117.53 307.2 114.52 329.36 114.52 340.7C114.52 352.04 116.69 382.86 145.35 399.7C174.01 416.54 199.52 413.53 208.35 412.03C208.35 412.03 221.02 409.52 226.19 406.53C226.19 406.53 228.98 404.65 229.19 408.53C229.52 414.78 227.5 426.53 222.85 433.2C222.85 433.2 218.19 440.63 240.52 440.63C249.19 440.63 254.69 439.54 264.85 432.03C264.85 432.03 266.85 431.36 266.35 426.36C265.85 421.36 265.68 410.86 266.43 407.36C266.43 407.36 304.08 399.59 322.07 381.44L322.45 374.6C316.79 366.23 326.14 359.78 327.21 360.34C328.28 360.9 328.96 361.03 331.21 360.78C333.46 360.53 333.71 359.41 333.27 358.03C332.83 356.65 330.96 355.15 327.4 353.72C323.84 352.29 327.38 340.56 331.19 339.25ZM321.08 340.19C317.65 342.62 317.33 349.33 320.4 352.52C321.34 353.52 321.28 354.52 320.4 354.77C311.92 357.03 310.87 366.54 315.81 374.85C317.46 377.62 316.56 378.27 316.56 378.27C300.31 394.02 278.19 400.5 267.62 402.56C264.52 403.17 262.38 404.44 261.15 409.1C259.63 414.86 259.23 427.27 259.23 427.27C247.23 437.27 230.31 434.69 232.31 432.19C234.31 429.69 235.31 424.93 235.31 424.93C235.31 424.93 236.54 421.26 238.25 408.38C239.44 399.44 232.82 394.82 222.53 398.5C200.6 406.33 174.98 406.25 159.81 399.44C137.55 389.44 127.42 374.23 125.31 352.94C123.2 331.65 123.92 324.5 131.31 288.86C138.7 253.22 136.56 238.15 136.56 238.15C147.19 234.15 148.5 217.25 148.5 217.25C162.81 227.88 180.75 204.88 180.75 204.88C188.81 216.31 206.06 212.14 213.88 200.31C215.25 208.88 232.88 213.44 237.25 199.75C235.19 216.06 255.94 224.25 269.5 208.88C270.18 208.1 270.82 208.5 271 209.44C273.65 223.5 288.73 226.35 297.4 221.44C299.99 219.96 299.94 221.5 299.94 221.5C298.86 232.24 313.88 236.5 317.09 236.2C317.9 236.14 318.09 236.75 317.69 236.98C311.59 240.98 308.39 247.54 307.46 254.37C306.13 264.17 310.63 274.04 316.88 277.14C320.16 278.76 318.23 280.14 318.23 280.14C310.9 286.06 308.56 293.93 308.48 297.73C308.4 301.53 310.81 311.14 317.81 311.98C320.3 312.27 318.98 313.89 318.98 313.89C313.13 321.33 313.65 335.56 319.48 338.23C322.07 339.37 322.21 339.39 321.08 340.19Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M116.48 346.51C97.26 345.19 83.38 330.08 93 312.67C80.38 308.62 75.25 293.25 85.25 285.12C73.12 279.62 63.62 258.5 84.62 238.38C72.24 230.38 73.75 213.38 82 205.5C74 199.25 72.88 159 98.75 154.5C92.88 135.12 111 115.24 122.25 118C124.88 103.76 144.25 87 165.62 99C167.92 100.29 168.51 100.54 169.5 97.25C172.25 88.12 198.5 56.01 223.12 73.88C227.5 76.38 227.93 75.88 228 72.25C228.25 59.88 256.53 45.5 270.75 67.25C272.88 70.5 273.12 70 275.25 67.38C277.38 64.76 303.12 48.25 315.75 86.88C339.12 74.12 359.25 84 359.25 101.38C378.12 94.5 394 116.75 390.12 136.62C413.5 136.88 428.5 160.25 417.88 184.12C442.88 194.25 447.5 226.5 422.5 236.12C442.88 246.75 438.92 271.48 420.12 280.88C418.62 281.62 419.03 282.19 419.5 282.5C425.5 286.5 434.85 317.44 404.5 319.12C400 319.38 400.38 320.88 401.75 322.12C403.12 323.36 415.12 338.12 394.5 340.75C401.38 356.12 384.5 363.5 377.38 362.25C379.25 370.75 366 381.88 348.62 376.5C346.38 385.73 324.88 385 319 379.5C317.873 378.546 316.965 377.36 316.34 376.022C315.715 374.684 315.388 373.227 315.38 371.75L320.63 369.6C320.63 369.6 320.89 377.84 329.73 377.73C338.73 377.61 340.54 373.98 340.02 368.73C339.75 365.98 341.75 366.6 341.75 366.6C341.75 366.6 344.62 367.1 345.75 365.86C346.88 364.62 348.25 364.86 348.75 366.1C349.25 367.34 352.88 375.1 362.62 370.48C372.36 365.86 368 359.25 367 357.38C366 355.51 366.12 353.38 367 353.12C368.605 353.008 370.109 352.294 371.21 351.12C372.6 349.48 373.97 353.4 375.28 354.83C376.41 356.04 385.28 355.56 385.28 345.92C385.28 339.16 381.78 336.04 381.78 336.04C381.78 336.04 399.65 335.54 399.91 329.16C400.17 322.78 393.51 321.76 391.28 321.16C390.227 320.903 389.312 320.254 388.721 319.345C388.13 318.437 387.908 317.337 388.099 316.271C388.29 315.204 388.881 314.25 389.75 313.603C390.62 312.957 391.703 312.666 392.78 312.79C396.89 313.18 417.15 312.17 417.15 295.79C417.15 284.29 410.28 283.16 410.28 283.16C405.53 281.79 407.03 273.36 411.96 273.44C418.53 273.54 426.91 264.66 426.65 253.92C426.65 253.92 428.2 236.68 412.03 239.79C405.53 241.04 406.78 236.92 409.15 234.79C411.52 232.66 437.69 216.58 424.91 199.54C415.53 187.04 407.86 190.54 407.86 190.54C403.53 191.93 400.57 184.54 404.91 181.3C410.15 177.43 421.53 145.93 389.28 143.43C387.15 143.26 386.34 143.95 385.65 145.43C384.96 146.91 382.91 146.15 382.78 144.75C382.68 143.63 382.25 143.39 381.27 143.75C380.29 144.11 376.78 143.22 380.65 137.35C385.49 130.02 378.91 105.88 366.15 105.6C360.53 105.48 355.49 110.43 353.15 110.69C351.5 110.88 349.62 109.89 350.78 103.1C351.53 98.72 348.56 86.18 331.28 88.1C324.53 88.85 318.94 90.25 315.53 95.1C313.9 97.41 312.46 97.96 310.15 97.85C306.97 97.7 305.41 95.1 305.91 89.35C306.41 83.6 299.66 68.48 287.91 68.48C282.15 68.48 275.91 73.1 272.03 77.35C268.28 71.72 258.61 55.77 241.53 63.48C231.25 68 230.62 78.62 230.75 80.75C230.88 82.88 228.5 83.88 226.12 82C223.74 80.12 198 57.5 174.88 99C170.74 106.43 169.48 108.56 163.88 104.88C159.5 102 133.34 95.49 129.75 118.38C128.75 124.75 127.25 125.38 119.15 125.13C113.28 124.95 100.88 136.75 108.88 157.75C88.38 162.12 83.56 169.75 83.88 185.25C84.12 197.75 85.6201 202.79 88.6201 205.12C88.6201 205.12 89.75 205.25 87.88 208.38C86.01 211.51 77.8201 228.91 102.12 240.12C103.75 240.88 104 244.38 101 244.38C98 244.38 81.88 250.25 82.12 263.25C82.36 276.25 94 282 99.1201 282.25C104.24 282.5 102.5 287.88 99.25 287.75C96 287.62 90.25 291.38 90.1201 297.12C89.9901 302.86 93 307.22 101.5 307.5C107.37 307.69 111.21 310.14 111.88 311.62C113 314.12 108 312.38 105.62 313.62C103.24 314.86 95.48 333.94 115.16 340.22L116.48 346.51Z" fill="#060500" style="fill:#060500;fill:color(display-p3 0.0235 0.0196 0.0000);fill-opacity:1;"/>
|
||||
<path d="M266.69 341.67C283.43 341.67 297 332.268 297 320.67C297 309.072 283.43 299.67 266.69 299.67C249.95 299.67 236.38 309.072 236.38 320.67C236.38 332.268 249.95 341.67 266.69 341.67Z" fill="url(#paint0_radial_2704_2)" style=""/>
|
||||
<path d="M365.31 267.69C360.31 267.44 353.69 268.25 350.69 271C351.75 261.75 348 261.18 345 261C342 260.82 342.25 263.79 342.25 263.79C342.25 263.79 349.5 274.54 346.25 315.79C349.238 314.474 351.828 312.396 353.758 309.763C355.689 307.13 356.893 304.036 357.25 300.79C357.25 300.79 362 298.34 365.5 298.44C369 298.54 380 295.54 379.75 284.04C379.5 272.54 370.31 267.94 365.31 267.69ZM360.56 293C357.06 294.69 357 292.81 357.06 290.88C357.12 288.95 357.06 284.25 355.69 282.69C355.295 282.349 355.038 281.875 354.968 281.357C354.898 280.839 355.019 280.314 355.31 279.88C355.31 279.88 369.69 272.56 370.12 279.38C370.55 286.2 364.06 291.31 360.56 293Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M355.31 279.88C355.02 280.314 354.898 280.839 354.968 281.357C355.038 281.875 355.295 282.349 355.69 282.69C357.06 284.25 357.12 288.94 357.06 290.88C357 292.82 357.06 294.69 360.56 293C364.06 291.31 370.56 286.19 370.12 279.38C369.68 272.57 355.31 279.88 355.31 279.88Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M145.882 341.388C162.397 344.121 177.32 337.06 179.213 325.618C181.107 314.176 169.253 302.684 152.738 299.952C136.223 297.219 121.3 304.28 119.407 315.722C117.513 327.164 129.367 338.656 145.882 341.388Z" fill="url(#paint1_radial_2704_2)" style=""/>
|
||||
<path d="M379.685 347.407C386.049 342.935 394.83 331.549 400.087 320.64C412.043 295.831 415.229 254.647 388.786 237.941C368.968 225.646 342.728 237.678 332.434 258.612C325.489 272.947 322.899 289.25 323.003 303.293C323.055 310.327 323.783 316.876 325.02 322.411C326.24 327.876 328.018 332.632 330.337 335.851L330.426 335.974L330.524 336.088C336.861 343.436 347.887 351.968 360.346 352.911C366.664 353.389 373.288 351.902 379.685 347.407Z" fill="#D9D9D9" style="fill:#D9D9D9;fill:color(display-p3 0.8510 0.8510 0.8510);fill-opacity:1;"/>
|
||||
<path d="M382.5 342.496C392.208 330.839 398.5 321.496 402.5 309.996C378.022 329.989 341.487 290.372 330 273.996L325.5 301.996L329 327.496L337 340.496C345.988 346.371 351.219 349.275 362.5 350.496C370.394 349.93 374.778 348.33 382.5 342.496Z" fill="#C4C4C4" style="fill:#C4C4C4;fill:color(display-p3 0.7675 0.7675 0.7675);fill-opacity:1;"/>
|
||||
<path d="M388.786 237.941C415.229 254.647 412.043 295.831 400.087 320.64C394.83 331.549 386.049 342.935 379.685 347.407C373.288 351.902 366.664 353.389 360.346 352.911C347.887 351.968 337 345.496 330.524 336.088L330.426 335.974L330.337 335.851C328.018 332.632 326.24 327.876 325.02 322.411C323.783 316.876 323.055 310.327 323.003 303.293C322.899 289.25 325.489 272.947 332.434 258.612C342.728 237.678 368.968 225.646 388.786 237.941ZM385.731 244.171C341.5 216.496 317.5 304.496 337 335.851C342.5 343.496 349.5 346.996 360 348.496C367.152 349.518 372.5 347.996 378.5 343.496C411.5 313.496 414.5 267.496 385.731 244.171Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<ellipse cx="375.924" cy="300.629" rx="23.6644" ry="34.6366" transform="rotate(15.2136 375.924 300.629)" fill="url(#paint2_linear_2704_2)" style=""/>
|
||||
<path d="M384.882 267.688C390.982 269.347 395.574 274.482 398.026 281.55C400.476 288.615 400.764 297.563 398.277 306.708C395.79 315.853 391.01 323.422 385.319 328.273C379.626 333.126 373.065 335.228 366.966 333.569C360.866 331.91 356.274 326.775 353.823 319.708C351.372 312.642 351.085 303.695 353.572 294.55C356.059 285.405 360.838 277.836 366.529 272.985C372.222 268.132 378.782 266.03 384.882 267.688Z" stroke="black" stroke-opacity="0.04" style="stroke:black;stroke-opacity:0.04;"/>
|
||||
<path d="M386.5 257.496C382.693 263.016 375.137 262.483 375.5 257.496C377.5 229.996 371.854 202.791 367.5 183.496C366.239 177.906 373.001 174.104 379.5 183.496C388.613 196.666 392.891 248.23 386.5 257.496Z" fill="#CCCCCC" style="fill:#CCCCCC;fill:color(display-p3 0.8000 0.8000 0.8000);fill-opacity:1;"/>
|
||||
<path d="M370 176.496C371.5 175.996 374 175.496 376.231 177.485C396.517 198.295 392.652 248.406 388.482 257.727C387.431 261.866 383.968 264.223 380.862 263.996C379.31 263.883 377.763 263.337 376.596 262.36C375.4 261.358 373.873 259.186 374.003 257.388L374.176 254.821C375.777 228.725 372.876 202.547 366.418 184.889C365.105 181.359 366.588 178.202 370 176.496ZM375 179.996C373.834 178.697 372.21 177.901 370.5 178.496C367.68 179.477 367.5 182.496 368.5 184.496C374.752 194.273 378.559 221.266 377.169 255.013L376.996 257.605C376.945 258.3 377.222 258.883 377.796 259.364C378.4 259.87 379.317 260.232 380.354 260.308C382.426 260.459 384.447 259.487 385.046 257.127C388.158 250.255 392.607 199.609 375 179.996Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M373.5 173.996L370.5 189.996L367.5 193.996L364 192.496L359 177.996L368.5 165.996L373.5 173.996Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M334.877 156.978C334.304 156.204 334.419 155.109 335.216 154.57C343.088 149.243 356.425 151.18 355.184 162.576C376.636 157.278 380.779 181.596 370.874 193.055C370.364 193.646 369.53 193.815 368.802 193.534V193.534C367.326 192.964 367.022 190.863 367.95 189.582C374.451 180.604 371.377 160.93 352.916 169.056L351.571 169.981C349.945 171.098 347.855 169.437 348.576 167.602L349.173 166.082C352.702 154.687 345.867 151.813 337.532 157.388C336.671 157.964 335.493 157.811 334.877 156.978V156.978Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_2704_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(267.775 321.564) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_2704_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(149.715 321.643) rotate(9.39525) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="paint2_linear_2704_2" x1="375.924" y1="265.992" x2="382.735" y2="334.91" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#BABABA" style="stop-color:#BABABA;stop-color:color(display-p3 0.7275 0.7275 0.7275);stop-opacity:1;"/>
|
||||
<stop offset="0.829668" stop-color="#B3B3B3" style="stop-color:#B3B3B3;stop-color:color(display-p3 0.7020 0.7020 0.7020);stop-opacity:1;"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2704_2">
|
||||
<rect width="497" height="497" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 26 KiB |
|
|
@ -1,53 +0,0 @@
|
|||
<!-- jedmund-open -->
|
||||
<svg width="497" height="497" viewBox="0 0 497 497" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_705_2)">
|
||||
<path d="M497 0H0V497H497V0Z" fill="#FF2602" style="fill:#FF2602;fill:color(display-p3 1.0000 0.1490 0.0078);fill-opacity:1;"/>
|
||||
<path d="M155.92 497C155.92 497 171.19 448 215.69 431C234.91 423.66 269.19 421.5 291.92 433.5C305.42 440.63 312.92 453.5 316.92 461.73C322.374 472.659 324.956 484.797 324.42 497H155.92Z" fill="#BCBCBC" style="fill:#BCBCBC;fill:color(display-p3 0.7373 0.7373 0.7373);fill-opacity:1;"/>
|
||||
<path d="M264.33 424.17C292.5 424.17 327.21 445.17 331.83 485.17C332.98 495.06 333.12 497.88 333.12 497.88H322.5C322.5 497.88 325.65 436.08 265 426.83C245.92 423.92 179.5 437.83 158.33 497.88H145.83C145.83 497.88 157 442 226.5 419.5C251.06 412.94 264.33 424.17 264.33 424.17Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M296 497.88C296 497.88 295.5 486.62 296.75 481.5C298 476.38 295.88 473.5 293.75 473.12C291.62 472.74 288.88 473.88 287.88 478.25C286.657 484.722 286.028 491.293 286 497.88H296Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M131.81 207.25C131.81 207.25 137.14 236.25 136.81 250.08C136.81 250.08 134.48 274.91 129.81 293.25C125.14 311.59 120.81 348.91 126.14 365.25C131.47 381.59 153.48 411.25 194.48 405.25C235.48 399.25 237.14 397.58 236.81 407.58C236.48 417.58 235.48 427.58 231.48 433.91C231.48 433.91 244.81 436.25 251.48 431.58L258.14 426.91C258.14 426.91 258.54 415.27 261.48 408.25C263.48 403.45 266.14 403.25 266.14 403.25C266.14 403.25 300.81 394.91 320.14 377.58C335.67 363.66 351.48 337.25 354.48 299.91C354.48 299.91 366.81 294.25 368.48 288.25L370.14 282.25C370.14 282.25 370.48 275.91 364.81 276.91C359.14 277.91 353.81 281.91 353.81 281.91C353.81 281.91 360.81 223.25 352.14 193.91C347.14 176.98 338.36 158.64 312.14 141.25C276.48 117.58 189.48 120.25 142.81 157.91C125.16 172.16 131.81 207.25 131.81 207.25Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M256.217 362.498C256.813 344.624 226.749 338.344 212.256 336.895C190.517 336.895 165.88 343.175 165.88 362.498C165.88 377.987 186.67 387.171 212.256 388.101C238.826 389.068 255.734 376.991 256.217 362.498Z" fill="#F96A6A" style="fill:#F96A6A;fill:color(display-p3 0.9750 0.4144 0.4144);fill-opacity:1;"/>
|
||||
<path d="M174.576 377.474C167.813 365.88 178.924 345.107 203.561 335.928C186.17 335.928 167.33 347.136 165.397 355.252C163.465 363.368 169.262 373.609 174.576 377.474Z" fill="#821818" style="fill:#821818;fill:color(display-p3 0.5087 0.0936 0.0936);fill-opacity:1;"/>
|
||||
<path d="M220.573 347.039L221.539 338.827L215.742 337.378L209.462 337.861L208.979 346.073L220.573 347.039Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path d="M206.081 346.556L207.047 338.344L201.25 336.895L194.97 337.378L194.487 345.59L206.081 346.556Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M210.324 333.996C235.928 335.445 257.806 346.099 258.633 359.116C260.565 389.551 224.817 391 210.324 391C195.832 391 161.237 385.203 162.499 359.116C163.154 345.59 184.721 332.547 210.324 333.996ZM255.251 362.015C251.869 345.107 232.546 340.276 211.29 338.344C201.284 337.434 169.712 340.759 167.813 359.116C165.464 381.821 200.856 387.522 211.29 387.135C224.334 386.652 255.251 383.754 255.251 362.015Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M208.979 348.006C206.943 347.039 206.621 345.59 206.943 344.141L207.53 337.861L210.429 337.378C210.267 338.988 209.946 343.271 209.946 343.658C209.946 344.624 210.419 345.126 211.395 345.59C213.052 346.378 218.269 347.115 219.607 345.107C220.474 343.807 220.573 339.793 220.573 338.344L224.438 338.344C224.2 339.889 223.738 344.407 222.989 346.073C222.5 347.014 220.879 348.693 220.09 348.972C217.605 349.505 211.145 349.033 208.979 348.006Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M194.486 347.522C192.071 346.073 191.749 343.175 192.071 341.725L193.037 337.378L195.936 336.895C195.775 338.505 195.453 342.788 195.453 343.175C195.453 344.141 195.927 344.643 196.902 345.107C198.559 345.895 203.776 346.632 205.114 344.624C205.981 343.324 206.08 339.31 206.08 337.861L209.945 337.861C209.707 339.406 208.175 345.373 207.425 347.039C206.937 347.98 206.386 348.21 205.597 348.489C203.112 349.022 196.541 348.756 194.486 347.522Z" fill="black" style="fill:black;fill-opacity:1;"/>
|
||||
<path d="M135.85 261C135.85 261 147.71 253.65 150.67 246.34C162.35 252.34 181.57 252.34 184.29 246.34C187.01 240.34 184.38 235.16 180.66 232.91C184 234.1 186.25 232.1 186.66 229.54C190.35 232.54 208.22 230.42 212.53 223.16C215.72 226.66 232.41 225.44 237.35 218.85C241.47 225.66 266.28 226.79 274.35 217.16C274.35 217.16 290.69 204.05 277.2 178.33C263.71 152.61 151 155.67 135.85 183.13C120.7 210.59 118.08 236.68 135.85 261Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M245.54 433.73C245.54 433.73 243.72 417 254 405.68C265 393.58 289 392.08 302.7 367.88C302.7 367.88 298.2 354.62 307.2 342.62C307.2 342.62 295.01 321 320.2 310.62L355.07 304.09C355.07 304.09 353.37 331.17 337.2 356.38C331.04 367.44 317.2 387.67 274.83 401.12C274.83 401.12 262.33 404.38 261.04 411.4C260.212 415.793 260.104 420.292 260.72 424.72C260.778 425.106 260.737 425.5 260.6 425.866C260.464 426.232 260.237 426.556 259.94 426.81C255.829 430.338 250.863 432.724 245.54 433.73V433.73Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M216.12 280C216.12 277.37 216.28 276 218.72 275.87C221.16 275.74 221.06 278.75 221.06 278.75C221.06 278.75 220.67 293.91 215.06 307.75C215.06 307.75 212.19 315.19 210.12 315.19C209.084 315.438 207.994 315.317 207.038 314.848C206.081 314.379 205.318 313.591 204.88 312.62C204.88 312.62 203.56 309.07 202.16 308.97C199.59 308.78 197.96 313.58 197.6 317.23C197.38 319.5 196.44 320.94 194.12 320.94C191.8 320.94 190.94 319.55 191.19 317.12C192.19 310.94 196.47 303.91 199.44 300.94C199.729 300.646 200.072 300.411 200.451 300.25C200.831 300.088 201.238 300.003 201.65 300C204.75 300 205.65 302.67 206.07 304.89C206.81 308.75 208.12 308.47 208.12 308.47C208.12 308.47 208.7 308.67 209.5 307.06C212.94 300.12 216.12 288.44 216.12 280Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M267.4 265.57C267.49 263.69 268.841 260.57 271.881 261C274.251 261.31 275.69 263 275.31 266.5C274.88 270.5 271.47 287.69 268.86 292.25C267.37 294.86 266 295.44 263.86 294.96C262.86 294.73 261.73 293.44 262.28 290.06C263 285.59 266.77 277.57 267.4 265.57Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M168.91 258.88C168.91 258.88 170.91 258.81 171.29 261.53C172.1 266.64 171.55 276.73 169.64 285.1C169.64 285.1 168.27 288.92 165.46 288.92C162.65 288.92 162.29 285.75 162.36 283.66C162.43 281.57 165.1 275.59 165.1 264.86C165.1 258.08 168.91 258.88 168.91 258.88Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M253.28 242.12C253.22 248.12 259.28 252.5 269.88 252.75C280.48 253 285.55 251.58 290.88 246.67C296.21 241.76 280.36 237.94 271.04 238.26C261.72 238.58 253.31 239 253.28 242.12Z" fill="#AD723B" style="fill:#AD723B;fill:color(display-p3 0.6784 0.4471 0.2314);fill-opacity:1;"/>
|
||||
<path d="M255.32 241C255.32 241 256.5 235.67 271.67 236.17C286.84 236.67 288.33 241 289.5 242C290.67 243 290.83 244.67 288 245.5C288 245.5 266.43 247.93 258.33 244.33C254.42 242.59 255.32 241 255.32 241Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M293.13 242.18C292.87 240.61 292.87 238.52 287.81 236.6C282.75 234.68 276.21 234.42 268.76 234.68C264.253 234.858 259.79 235.638 255.49 237C255.49 237 251.69 240.23 254.05 243.89C256.41 247.55 272.54 248.19 272.54 248.19C272.54 248.19 284.93 248.78 289.38 247.38C292.89 246.23 293.39 243.75 293.13 242.18ZM284.23 243.93C284.23 243.93 267.23 245.08 259.23 242.88C255.44 241.88 256.7 239.39 260.8 238.69C264.192 238.337 267.601 238.193 271.01 238.26C271.01 238.26 283.68 237.51 286.28 242.1C287.46 244.1 284.23 243.93 284.23 243.93Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M170 233.33C175 233.33 178.17 233.6 179.5 236.13C180.83 238.66 180.62 241.63 177.67 242.13C174.31 242.73 171.08 242.8 163.81 241.84C160.5 241.39 157.14 241.4 154.94 242.58C152.74 243.76 151.25 241.58 153.75 238.71C156.11 236 165 233.33 170 233.33Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M183.62 236.13C183.62 236.13 182.38 231 171.62 230.88C160.86 230.76 150.22 236.13 150.22 236.13C150.22 236.13 145.72 239.5 147.35 242.75C148.98 246 154.85 245.08 154.85 245.08C157.643 244.474 160.484 244.12 163.34 244.02C166.19 244.1 173.59 244.88 173.59 244.88C176.466 244.962 179.3 244.175 181.72 242.62C182.289 242.337 182.792 241.939 183.199 241.452C183.607 240.965 183.909 240.399 184.087 239.79C184.266 239.181 184.316 238.541 184.236 237.911C184.156 237.281 183.946 236.675 183.62 236.13ZM176.38 240.62C173.586 241.282 170.675 241.282 167.88 240.62C163.5 239.5 157.88 240.5 155.5 241.12C153.12 241.74 152.62 239.75 153.62 239C154.62 238.25 158.62 235.12 169.25 235.12C169.25 235.12 176.88 235.04 178.12 237.38C179.68 240.28 176.38 240.62 176.38 240.62Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M113.14 341.08C113.14 341.08 97.1402 334.75 97.4702 322.08C97.4702 322.08 98.4702 310.42 106.81 309.42C106.81 309.42 86.1402 309.75 86.1402 295.75C86.1402 295.75 86.1402 283.08 97.4702 283.42C97.4702 283.42 78.4702 275.42 78.4702 261.75C78.4702 249.08 85.8102 240.75 95.8102 240.75C95.8102 240.75 80.4702 232.08 80.4702 220.08C80.4702 220.08 79.4702 209.75 85.1402 204.75C85.1402 204.75 76.1402 184.08 83.4702 167.42C83.4702 167.42 86.1402 156.75 104.14 153.75C104.14 153.75 98.1402 137.42 107.47 126.75C107.47 126.75 112.47 118.75 125.47 118.08C125.47 118.08 127.47 102.26 141.47 98.08C141.47 98.08 157.03 97.08 168.69 102.08C168.69 102.08 177.14 79.43 195.14 72.76C195.14 72.76 214.34 65.76 228.34 78.37C228.34 78.37 230.47 57.09 247.97 57.09C265.47 57.09 272.66 72.17 272.66 72.17C272.66 72.17 277.72 59.04 294.47 64.09C312.14 69.43 312.47 93.43 312.47 93.43C312.47 93.43 326.47 78.76 345.81 84.09C353.92 86.33 357.14 97.76 354.81 106.43C354.81 106.43 362.47 98.09 369.81 101.43C377.15 104.77 391.47 117.09 382.81 137.76C382.81 137.76 401.47 135.43 410.14 150.09C410.14 150.09 424.14 169.76 407.14 184.76C407.14 184.76 428.47 183.09 432.47 209.09C432.47 209.09 434.14 229.76 413.47 236.43C413.47 236.43 430.81 237.76 430.47 253.76C430.47 253.76 432.77 269.76 411.77 276.76C411.77 276.76 423.43 285.76 421.1 297.43C417.1 317.43 391.43 315.43 391.43 315.43C391.43 315.43 405.43 320.09 403.43 328.43C403.43 328.43 399.77 338.43 386.1 336.43C386.1 336.43 395.1 345.43 387.1 352.76C379.1 360.09 369.43 352.76 369.43 352.76C369.43 352.76 374.81 368.89 364.08 373.23C353.56 377.49 344.52 369.41 342.86 367.08C341.2 364.75 346.43 377.76 332.1 379.08C317.77 380.4 316.43 366.08 316.43 366.08C316.43 366.08 317.43 352.75 329.1 355.42C329.1 355.42 320.43 352.42 321.1 344.42C321.77 336.42 330.77 333.42 330.77 333.42C330.77 333.42 318.77 339.08 317.1 329.42C315.43 319.76 325.43 309.75 325.43 309.75C325.43 309.75 311.43 311.42 310.1 297.75C310.1 297.75 309.77 282.75 325.77 277.08C325.77 277.08 309.1 274.75 309.43 256.08C309.76 237.41 326.1 231.42 326.1 231.42C326.1 231.42 316.77 238.42 307.43 230.75C298.09 223.08 302.1 210.08 302.1 210.08C302.1 210.08 298.43 221.84 283.1 218.08C267.77 214.32 271.77 200.08 271.77 200.08C271.77 200.08 263.43 215.08 250.77 213.42C238.11 211.76 237.1 196.75 237.1 196.75C237.1 196.75 232.1 205.42 225.43 204.75C225.43 204.75 212.77 205.08 213.43 186.75C213.43 186.75 213.1 204.08 195.77 207.42C178.44 210.76 177.1 193.75 177.1 193.75C177.1 193.75 179.1 208.42 162.43 214.75C145.76 221.08 143.43 206.42 143.43 206.42C143.43 206.42 148.1 217.08 139.43 229.42C130.76 241.76 131.1 236.42 131.1 236.42C132.79 243.396 133.354 250.596 132.77 257.75C131.77 269.42 125.1 286.08 121.77 305.42C119.756 317.082 118.972 328.924 119.43 340.75L113.14 341.08Z" fill="#935C0A" style="fill:#935C0A;fill:color(display-p3 0.5765 0.3608 0.0392);fill-opacity:1;"/>
|
||||
<path d="M331.19 339.25C335.93 337.62 335.62 334.81 333.31 333.19C331 331.57 327.13 332.56 326.19 333.94C325.25 335.32 323.05 335.05 321.75 332.81C317.63 325.7 324.31 316.12 328.69 314.69C334.69 312.69 332.38 309.25 332.38 309.25C332.01 308.647 331.462 308.174 330.812 307.895C330.162 307.616 329.442 307.544 328.75 307.69C326.94 308.12 326.12 308.88 324.75 308.88C320 308.88 315.75 304.5 315.88 298.44C316.01 292.38 321.56 283.31 326.81 282.5C332.06 281.69 331.28 278.95 331.28 278.95C331.28 278.95 331.91 276.88 326.81 275.81C317.34 273.81 314 270 313.63 258C313.26 246 318.75 238.56 324.44 237.21C333.68 235.02 332 229.31 331.38 228.62C330.828 228.254 330.156 228.114 329.503 228.232C328.851 228.349 328.27 228.714 327.88 229.25C327.19 230.5 324.1 235.15 315.04 232.08C300.68 227.23 306.94 212.08 307.94 209.38C308.94 206.68 304.88 206.94 304.88 206.94C304.88 206.94 300.5 206.62 299.81 208.62C298.94 211.17 294.44 219.81 285.69 218.06C274.85 215.9 274.81 205.94 275.12 202.56C275.43 199.18 274.81 198.88 273.12 198.88C271.43 198.88 269.69 198.81 269 201.69C266.43 212.44 251.94 212.88 251.94 212.88C238.44 212.56 239.18 196.15 239.75 191.88C240.44 186.75 240.44 186.79 239.49 186.88C238 186.97 236.38 190.28 236.38 192.41C236.481 194.484 235.981 196.543 234.94 198.34C233.75 200.34 231.25 204.65 226.31 204.47C221.37 204.29 218.75 199.47 218.69 194.59C218.63 190.34 218.88 185.59 218.88 185.59C218.81 184.28 218 182.72 216.69 183.28C215.38 183.84 214.19 184.41 212.31 185.41C211.77 185.729 211.339 186.202 211.071 186.769C210.804 187.336 210.713 187.971 210.81 188.59L211 191.97C211.06 195.47 207.89 199.66 205.62 201.72C198.75 207.97 191.7 207.91 188.27 204.41C184.84 200.91 184.19 199.34 183.27 195.47C182.74 193.28 181.27 192.72 180.46 193.03C179.65 193.34 176.71 194.65 176.71 194.65C176.437 194.798 176.198 195 176.007 195.244C175.816 195.489 175.678 195.77 175.601 196.071C175.523 196.371 175.509 196.684 175.559 196.991C175.609 197.297 175.722 197.589 175.89 197.85C176.62 199.29 174.82 202.85 174.08 203.91C171.94 207.13 165.4 213.47 160.02 214.36C154.43 215.29 151.35 209.36 149.85 206.36C148.35 203.36 145.02 204.53 142.02 206.36C139.02 208.19 139.85 210.36 139.85 210.36C141.85 216.64 139.64 224.91 137.52 228.7C132.19 238.21 125.52 235.7 123.52 235.15C121.52 234.6 120.02 236.7 122.69 239.86C125.36 243.02 132.1 241.61 132.1 241.61C132.1 241.61 126.85 272.2 122.19 289.7C117.53 307.2 114.52 329.36 114.52 340.7C114.52 352.04 116.69 382.86 145.35 399.7C174.01 416.54 199.52 413.53 208.35 412.03C208.35 412.03 221.02 409.52 226.19 406.53C226.19 406.53 228.98 404.65 229.19 408.53C229.52 414.78 227.5 426.53 222.85 433.2C222.85 433.2 218.19 440.63 240.52 440.63C249.19 440.63 254.69 439.54 264.85 432.03C264.85 432.03 266.85 431.36 266.35 426.36C265.85 421.36 265.68 410.86 266.43 407.36C266.43 407.36 304.08 399.59 322.07 381.44L322.45 374.6C316.79 366.23 326.14 359.78 327.21 360.34C328.28 360.9 328.96 361.03 331.21 360.78C333.46 360.53 333.71 359.41 333.27 358.03C332.83 356.65 330.96 355.15 327.4 353.72C323.84 352.29 327.38 340.56 331.19 339.25ZM321.08 340.19C317.65 342.62 317.33 349.33 320.4 352.52C321.34 353.52 321.28 354.52 320.4 354.77C311.92 357.03 310.87 366.54 315.81 374.85C317.46 377.62 316.56 378.27 316.56 378.27C300.31 394.02 278.19 400.5 267.62 402.56C264.52 403.17 262.38 404.44 261.15 409.1C259.63 414.86 259.23 427.27 259.23 427.27C247.23 437.27 230.31 434.69 232.31 432.19C234.31 429.69 235.31 424.93 235.31 424.93C235.31 424.93 236.54 421.26 238.25 408.38C239.44 399.44 232.82 394.82 222.53 398.5C200.6 406.33 174.98 406.25 159.81 399.44C137.55 389.44 127.42 374.23 125.31 352.94C123.2 331.65 123.92 324.5 131.31 288.86C138.7 253.22 136.56 238.15 136.56 238.15C147.19 234.15 148.5 217.25 148.5 217.25C162.81 227.88 180.75 204.88 180.75 204.88C188.81 216.31 206.06 212.14 213.88 200.31C215.25 208.88 232.88 213.44 237.25 199.75C235.19 216.06 255.94 224.25 269.5 208.88C270.18 208.1 270.82 208.5 271 209.44C273.65 223.5 288.73 226.35 297.4 221.44C299.99 219.96 299.94 221.5 299.94 221.5C298.86 232.24 313.88 236.5 317.09 236.2C317.9 236.14 318.09 236.75 317.69 236.98C311.59 240.98 308.39 247.54 307.46 254.37C306.13 264.17 310.63 274.04 316.88 277.14C320.16 278.76 318.23 280.14 318.23 280.14C310.9 286.06 308.56 293.93 308.48 297.73C308.4 301.53 310.81 311.14 317.81 311.98C320.3 312.27 318.98 313.89 318.98 313.89C313.13 321.33 313.65 335.56 319.48 338.23C322.07 339.37 322.21 339.39 321.08 340.19Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M116.48 346.51C97.2602 345.19 83.3802 330.08 93.0002 312.67C80.3802 308.62 75.2502 293.25 85.2502 285.12C73.1202 279.62 63.6202 258.5 84.6202 238.38C72.2402 230.38 73.7502 213.38 82.0002 205.5C74.0002 199.25 72.8802 159 98.7502 154.5C92.8802 135.12 111 115.24 122.25 118C124.88 103.76 144.25 87 165.62 99C167.92 100.29 168.51 100.54 169.5 97.25C172.25 88.12 198.5 56.01 223.12 73.88C227.5 76.38 227.93 75.88 228 72.25C228.25 59.88 256.53 45.5 270.75 67.25C272.88 70.5 273.12 70 275.25 67.38C277.38 64.76 303.12 48.25 315.75 86.88C339.12 74.12 359.25 84 359.25 101.38C378.12 94.5 394 116.75 390.12 136.62C413.5 136.88 428.5 160.25 417.88 184.12C442.88 194.25 447.5 226.5 422.5 236.12C442.88 246.75 438.92 271.48 420.12 280.88C418.62 281.62 419.03 282.19 419.5 282.5C425.5 286.5 434.85 317.44 404.5 319.12C400 319.38 400.38 320.88 401.75 322.12C403.12 323.36 415.12 338.12 394.5 340.75C401.38 356.12 384.5 363.5 377.38 362.25C379.25 370.75 366 381.88 348.62 376.5C346.38 385.73 324.88 385 319 379.5C317.873 378.546 316.965 377.36 316.34 376.022C315.715 374.684 315.388 373.227 315.38 371.75L320.63 369.6C320.63 369.6 320.89 377.84 329.73 377.73C338.73 377.61 340.54 373.98 340.02 368.73C339.75 365.98 341.75 366.6 341.75 366.6C341.75 366.6 344.62 367.1 345.75 365.86C346.88 364.62 348.25 364.86 348.75 366.1C349.25 367.34 352.88 375.1 362.62 370.48C372.36 365.86 368 359.25 367 357.38C366 355.51 366.12 353.38 367 353.12C368.606 353.008 370.109 352.294 371.21 351.12C372.6 349.48 373.97 353.4 375.28 354.83C376.41 356.04 385.28 355.56 385.28 345.92C385.28 339.16 381.78 336.04 381.78 336.04C381.78 336.04 399.65 335.54 399.91 329.16C400.17 322.78 393.51 321.76 391.28 321.16C390.227 320.903 389.312 320.254 388.721 319.345C388.13 318.437 387.908 317.337 388.099 316.271C388.29 315.204 388.881 314.25 389.75 313.603C390.62 312.957 391.704 312.666 392.78 312.79C396.89 313.18 417.15 312.17 417.15 295.79C417.15 284.29 410.28 283.16 410.28 283.16C405.53 281.79 407.03 273.36 411.96 273.44C418.53 273.54 426.91 264.66 426.65 253.92C426.65 253.92 428.2 236.68 412.03 239.79C405.53 241.04 406.78 236.92 409.15 234.79C411.52 232.66 437.69 216.58 424.91 199.54C415.53 187.04 407.86 190.54 407.86 190.54C403.53 191.93 400.57 184.54 404.91 181.3C410.15 177.43 421.53 145.93 389.28 143.43C387.15 143.26 386.34 143.95 385.65 145.43C384.96 146.91 382.91 146.15 382.78 144.75C382.68 143.63 382.25 143.39 381.27 143.75C380.29 144.11 376.78 143.22 380.65 137.35C385.49 130.02 378.91 105.88 366.15 105.6C360.53 105.48 355.49 110.43 353.15 110.69C351.5 110.88 349.62 109.89 350.78 103.1C351.53 98.72 348.56 86.18 331.28 88.1C324.53 88.85 318.94 90.25 315.53 95.1C313.9 97.41 312.46 97.96 310.15 97.85C306.97 97.7 305.41 95.1 305.91 89.35C306.41 83.6 299.66 68.48 287.91 68.48C282.15 68.48 275.91 73.1 272.03 77.35C268.28 71.72 258.61 55.77 241.53 63.48C231.25 68 230.62 78.62 230.75 80.75C230.88 82.88 228.5 83.88 226.12 82C223.74 80.12 198 57.5 174.88 99C170.74 106.43 169.48 108.56 163.88 104.88C159.5 102 133.34 95.49 129.75 118.38C128.75 124.75 127.25 125.38 119.15 125.13C113.28 124.95 100.88 136.75 108.88 157.75C88.3802 162.12 83.5602 169.75 83.8802 185.25C84.1202 197.75 85.6202 202.79 88.6202 205.12C88.6202 205.12 89.7502 205.25 87.8802 208.38C86.0102 211.51 77.8202 228.91 102.12 240.12C103.75 240.88 104 244.38 101 244.38C98.0002 244.38 81.8802 250.25 82.1202 263.25C82.3602 276.25 94.0002 282 99.1202 282.25C104.24 282.5 102.5 287.88 99.2502 287.75C96.0002 287.62 90.2502 291.38 90.1202 297.12C89.9902 302.86 93.0002 307.22 101.5 307.5C107.37 307.69 111.21 310.14 111.88 311.62C113 314.12 108 312.38 105.62 313.62C103.24 314.86 95.4802 333.94 115.16 340.22L116.48 346.51Z" fill="#060500" style="fill:#060500;fill:color(display-p3 0.0235 0.0196 0.0000);fill-opacity:1;"/>
|
||||
<path d="M266.69 341.67C283.43 341.67 297 332.268 297 320.67C297 309.072 283.43 299.67 266.69 299.67C249.95 299.67 236.38 309.072 236.38 320.67C236.38 332.268 249.95 341.67 266.69 341.67Z" fill="url(#paint0_radial_705_2)" style=""/>
|
||||
<path d="M365.31 267.69C360.31 267.44 353.69 268.25 350.69 271C351.75 261.75 348 261.18 345 261C342 260.82 342.25 263.79 342.25 263.79C342.25 263.79 349.5 274.54 346.25 315.79C349.238 314.474 351.828 312.396 353.759 309.763C355.69 307.13 356.894 304.035 357.25 300.79C357.25 300.79 362 298.34 365.5 298.44C369 298.54 380 295.54 379.75 284.04C379.5 272.54 370.31 267.94 365.31 267.69ZM360.56 293C357.06 294.69 357 292.81 357.06 290.88C357.12 288.95 357.06 284.25 355.69 282.69C355.295 282.349 355.038 281.875 354.968 281.357C354.898 280.839 355.02 280.314 355.31 279.88C355.31 279.88 369.69 272.56 370.12 279.38C370.55 286.2 364.06 291.31 360.56 293Z" fill="#070610" style="fill:#070610;fill:color(display-p3 0.0275 0.0235 0.0627);fill-opacity:1;"/>
|
||||
<path d="M355.31 279.88C355.019 280.314 354.898 280.839 354.968 281.357C355.038 281.875 355.294 282.349 355.69 282.69C357.06 284.25 357.12 288.94 357.06 290.88C357 292.82 357.06 294.69 360.56 293C364.06 291.31 370.56 286.19 370.12 279.38C369.68 272.57 355.31 279.88 355.31 279.88Z" fill="#C3915E" style="fill:#C3915E;fill:color(display-p3 0.7647 0.5686 0.3686);fill-opacity:1;"/>
|
||||
<path d="M145.882 341.388C162.397 344.121 177.32 337.06 179.214 325.618C181.107 314.176 169.254 302.684 152.738 299.952C136.223 297.219 121.3 304.28 119.407 315.722C117.514 327.164 129.367 338.656 145.882 341.388Z" fill="url(#paint1_radial_705_2)" style=""/>
|
||||
</g>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial_705_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(267.775 321.564) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial_705_2" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(149.715 321.643) rotate(9.39525) scale(28.341 22.842)">
|
||||
<stop stop-color="#E86A58" stop-opacity="0.18" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.18;"/>
|
||||
<stop offset="0.3" stop-color="#E86A58" stop-opacity="0.16" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.16;"/>
|
||||
<stop offset="0.63" stop-color="#E86A58" stop-opacity="0.1" style="stop-color:#E86A58;stop-color:color(display-p3 0.9098 0.4157 0.3451);stop-opacity:0.1;"/>
|
||||
<stop offset="0.99" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
<stop offset="1" stop-color="#E86A58" stop-opacity="0" style="stop-color:none;stop-opacity:0;"/>
|
||||
</radialGradient>
|
||||
<clipPath id="clip0_705_2">
|
||||
<rect width="497" height="497" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
|
@ -1,14 +0,0 @@
|
|||
<svg width="33" height="49" viewBox="0 0 33 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_305_48)">
|
||||
<path d="M8.30782 48.1362C12.7238 48.1364 16.308 44.5526 16.3082 40.1366L16.3085 32.1366L8.3085 32.1362C3.8925 32.136 0.308349 35.7199 0.308161 40.1359C0.307972 44.5519 3.89182 48.136 8.30782 48.1362Z" fill="#0ACF83" style="fill:#0ACF83;fill:color(display-p3 0.0392 0.8118 0.5137);fill-opacity:1;"/>
|
||||
<path d="M0.308863 24.1359C0.309051 19.7199 3.8932 16.136 8.3092 16.1362L16.3092 16.1366L16.3085 32.1366L8.30852 32.1362C3.89252 32.136 0.308674 28.5519 0.308863 24.1359Z" fill="#A259FF" style="fill:#A259FF;fill:color(display-p3 0.6353 0.3490 1.0000);fill-opacity:1;"/>
|
||||
<path d="M0.309534 8.13588C0.309723 3.71988 3.89388 0.13603 8.30988 0.136219L16.3099 0.13656L16.3092 16.1366L8.30919 16.1362C3.89319 16.136 0.309346 12.5519 0.309534 8.13588Z" fill="#F24E1E" style="fill:#F24E1E;fill:color(display-p3 0.9490 0.3059 0.1176);fill-opacity:1;"/>
|
||||
<path d="M16.3099 0.13656L24.3099 0.136901C28.7259 0.13709 32.3097 3.72124 32.3095 8.13724C32.3093 12.5532 28.7252 16.1371 24.3092 16.1369L16.3092 16.1366L16.3099 0.13656Z" fill="#FF7262" style="fill:#FF7262;fill:color(display-p3 1.0000 0.4471 0.3843);fill-opacity:1;"/>
|
||||
<path d="M32.3089 24.1372C32.3087 28.5532 28.7245 32.1371 24.3085 32.1369C19.8925 32.1367 16.3087 28.5526 16.3089 24.1366C16.3091 19.7206 19.8932 16.1367 24.3092 16.1369C28.7252 16.1371 32.3091 19.7212 32.3089 24.1372Z" fill="#1ABCFE" style="fill:#1ABCFE;fill:color(display-p3 0.1020 0.7373 0.9961);fill-opacity:1;"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_305_48">
|
||||
<rect width="32.0064" height="48" fill="white" style="fill:white;fill-opacity:1;" transform="translate(0.306671 0.135876)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,8 +0,0 @@
|
|||
<svg width="44" height="49" viewBox="0 0 44 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M31.3525 19.1607C29.1027 18.8192 27.2547 18.6384 26.9869 17.0582C26.9065 16.6029 26.9869 15.1365 26.9333 13.6166V13.4023L26.5048 13.6902C25.0477 14.7236 23.5094 15.6372 21.9048 16.4221C20.8352 16.9172 19.675 17.1862 18.4966 17.2122C17.9085 17.2168 17.3232 17.1287 16.7624 16.9511C16.1336 16.7089 15.5697 16.3238 15.1153 15.8262C15.0751 16.7033 15.0617 17.3394 15.0617 17.5671C15.0617 18.712 14.6198 18.6853 12.4772 19.3147C10.7429 19.8168 6.04252 21.5912 6.41079 25.5149C6.77906 29.4387 12.2093 28.903 13.582 28.8628C14.9546 28.8226 15.8652 28.8628 16.4009 29.3315C16.9365 29.8002 16.9901 31.1662 16.508 31.3403C15.8786 31.6215 12.9124 31.7755 9.66493 34.3266C6.41749 36.8776 5.37295 42.6762 8.32578 45.8634C11.2786 49.0506 16.97 49.0171 20.0233 47.9792C23.0765 46.9414 25.3397 44.4506 25.8553 43.0177C26.4378 41.3906 26.9735 40.7478 27.7167 40.6808C28.7364 40.5981 29.7393 40.3727 30.6963 40.0113C32.7452 39.4086 34.0442 37.8218 34.0442 35.9269C33.9973 32.4049 31.0847 32.2977 29.1563 31.8424C27.6431 31.4943 27.6631 30.6037 27.6631 29.4654C27.6631 28.3272 27.9712 27.8384 29.4107 27.4567C32.8591 26.4523 33.5889 25.3408 34.0978 23.7339C34.4928 22.3278 34.0174 19.5624 31.3525 19.1607ZM17.961 40.1184C17.037 40.1184 16.4344 39.7836 16.4344 39.0806C16.4344 38.3775 17.2713 37.9758 17.9744 37.9758C18.6774 37.9758 19.4876 38.5114 19.4876 39.0806C19.4876 39.6497 18.885 40.0648 17.961 40.1184Z" fill="#3CBBCD" style="fill:#3CBBCD;fill:color(display-p3 0.2353 0.7333 0.8039);fill-opacity:1;"/>
|
||||
<path d="M16.6055 5.09331C18.6142 5.09331 20.0538 5.88341 20.8707 7.44353C21.0882 7.85829 21.2138 8.31503 21.239 8.78268C22.7943 7.74034 24.4777 6.90314 26.2474 6.29185C26.4416 6.2249 26.6358 6.17133 26.8299 6.11776C26.8299 5.65576 26.8299 5.33436 26.7831 5.23392C26.6096 3.9064 25.9416 2.69321 24.9126 1.83673C23.8835 0.980248 22.5693 0.543539 21.2323 0.613846C18.554 0.674108 16.7662 2.97076 16.0364 5.09331C16.2306 5.1067 16.4247 5.09331 16.6055 5.09331Z" fill="#3CBBCD" style="fill:#3CBBCD;fill:color(display-p3 0.2353 0.7333 0.8039);fill-opacity:1;"/>
|
||||
<path d="M1.27529 14.7078C4.78387 10.4493 11.4595 7.10815 14.8208 6.42518C18.1821 5.74221 19.3539 6.96754 19.8627 7.9786C20.1709 8.72954 20.1829 9.56939 19.8962 10.3288C19.8627 10.4627 19.9699 10.8779 20.6127 10.5498C22.4687 9.2209 24.4958 8.14881 26.6388 7.36259C30.1541 6.15735 31.7075 7.26215 32.2298 7.77772C32.8994 8.40713 33.4484 9.67262 32.6717 10.5832C32.203 11.1189 32.9262 11.8086 33.4618 11.6211C33.9975 11.4336 35.4706 10.9113 36.9704 10.523C38.718 10.0677 39.7826 10.2685 40.0906 10.8377C40.3116 11.2528 40.1911 11.5943 39.5215 11.7885C38.3059 12.1856 37.1142 12.6529 35.9527 13.1879C33.4886 14.2391 32.8123 14.8217 31.3661 15.0426C29.7189 15.2971 28.0182 14.0048 28.8016 12.4781C29.123 11.8086 28.0249 11.38 26.9 12.0898C25.1715 13.3156 23.3592 14.4186 21.4764 15.3908C20.1374 16.075 18.5952 16.2488 17.1376 15.8796C16.2403 15.5917 15.0887 14.2391 15.1288 13.5361C15.169 12.833 14.1847 12.525 13.1871 13.0875C12.1894 13.6499 9.83917 15.2301 9.11603 15.7658C8.39289 16.3014 7.72331 16.6496 7.34835 16.3818C6.97338 16.1139 7.22782 15.6185 7.422 15.3105C7.61618 15.0025 7.20104 14.9087 6.75242 15.2703C6.30381 15.6319 5.14544 16.971 4.34865 16.7099C3.55185 16.4487 3.96699 15.6252 4.4156 15.1966C4.86422 14.7681 4.24821 14.4601 3.83307 14.8083C3.41793 15.1565 1.93147 16.817 1.24181 16.6429C0.552146 16.4688 0.237446 15.9666 1.27529 14.7078Z" fill="#F77754" style="fill:#F77754;fill:color(display-p3 0.9686 0.4667 0.3294);fill-opacity:1;"/>
|
||||
<path d="M42.1203 11.1858C42.7297 11.1555 43.2056 10.7657 43.1833 10.3151C43.1609 9.86455 42.6488 9.52377 42.0394 9.554C41.4299 9.58424 40.954 9.97403 40.9764 10.4246C40.9987 10.8752 41.5109 11.216 42.1203 11.1858Z" fill="#F77754" style="fill:#F77754;fill:color(display-p3 0.9686 0.4667 0.3294);fill-opacity:1;"/>
|
||||
<path d="M12.7414 9.61292C12.5205 9.41874 12.708 8.94334 13.7659 8.49472C14.8238 8.04611 15.6474 8.13315 15.942 8.41437C16.2366 8.6956 16.1563 8.95673 15.7813 9.03708C15.4063 9.11743 14.9042 9.12413 14.4422 9.24465C14.15 9.33936 13.8776 9.48687 13.6387 9.67987C13.5027 9.76943 13.341 9.81143 13.1786 9.79932C13.0163 9.7872 12.8626 9.72166 12.7414 9.61292ZM28.2756 8.64873C28.4828 8.52371 28.7089 8.43325 28.9452 8.3809C29.3201 8.32733 29.7085 8.3809 30.0165 8.34072C30.3245 8.30055 30.4249 8.11307 30.2308 7.85863C30.0366 7.60419 29.4005 7.48366 28.51 7.72471C27.6194 7.96576 27.4118 8.34072 27.5592 8.51481C27.6424 8.6197 27.7607 8.69101 27.8924 8.71562C28.024 8.74022 28.1601 8.71647 28.2756 8.64873ZM17.7298 8.23359C17.6296 8.14105 17.5012 8.08482 17.3652 8.07389C17.2293 8.06297 17.0936 8.09798 16.9798 8.17333C16.9263 8.20226 16.8801 8.24298 16.8446 8.2924C16.8091 8.34183 16.7853 8.39867 16.7749 8.45864C16.7646 8.51861 16.7681 8.58014 16.785 8.63859C16.8019 8.69704 16.8319 8.75087 16.8727 8.79603C16.9721 8.8896 17.1001 8.94706 17.2361 8.9592C17.3721 8.97134 17.5082 8.93746 17.6226 8.86299C17.6731 8.83293 17.7164 8.79234 17.7498 8.744C17.7831 8.69566 17.8056 8.64071 17.8158 8.58288C17.826 8.52505 17.8235 8.4657 17.8087 8.40889C17.7939 8.35208 17.7669 8.29914 17.7298 8.25368V8.23359Z" fill="white" style="fill:white;fill-opacity:1;"/>
|
||||
<path d="M26.9291 13.6166V13.4023L26.5006 13.6902C25.0436 14.7236 23.5052 15.6372 21.9006 16.4221C20.8311 16.9172 19.6708 17.1862 18.4925 17.2122C17.9043 17.2168 17.319 17.1287 16.7583 16.9511C16.1294 16.7089 15.5655 16.3238 15.1111 15.8262C15.0709 16.7033 15.0576 17.3394 15.0576 17.5671C15.4474 17.8539 15.8786 18.0797 16.3364 18.2366C17.0333 18.4593 17.761 18.5701 18.4925 18.5647C19.86 18.5412 21.2076 18.2334 22.4497 17.6608C23.9094 16.9677 25.3092 16.1547 26.6345 15.2302L26.7149 15.3441C26.7914 15.4622 26.8741 15.5761 26.9626 15.6856C26.9425 15.0762 26.9492 14.3531 26.9291 13.6166Z" fill="#288693" style="fill:#288693;fill:color(display-p3 0.1569 0.5255 0.5765);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.8 KiB |
|
|
@ -1,3 +0,0 @@
|
|||
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.253555 24.6088C0.253555 34.4362 6.16302 42.8787 14.6192 46.5905C14.5517 44.9147 14.6072 42.9029 15.037 41.0795C15.4984 39.1313 18.125 28.0021 18.125 28.0021C18.125 28.0021 17.3583 26.4697 17.3583 24.205C17.3583 20.6484 19.4197 17.9921 21.9868 17.9921C24.1698 17.9921 25.2245 19.6318 25.2245 21.5952C25.2245 23.7897 23.8248 27.0721 23.1051 30.1124C22.5038 32.6582 24.3815 34.7347 26.893 34.7347C31.4401 34.7347 34.5027 28.8944 34.5027 21.9747C34.5027 16.7147 30.96 12.7777 24.5163 12.7777C17.2363 12.7777 12.701 18.2068 12.701 24.2712C12.701 26.3621 13.3174 27.8366 14.283 28.9784C14.727 29.5028 14.7887 29.7138 14.628 30.316C14.5129 30.7575 14.2485 31.8206 14.1391 32.2419C13.9793 32.8498 13.4868 33.0671 12.9374 32.8427C9.58424 31.4738 8.02259 27.8017 8.02259 23.6738C8.02259 16.8563 13.7723 8.68152 25.175 8.68152C34.3379 8.68152 40.3686 15.3121 40.3686 22.4296C40.3686 31.8443 35.1345 38.8778 27.4191 38.8778C24.8281 38.8778 22.3909 37.4772 21.556 35.8863C21.556 35.8863 20.1627 41.4159 19.8676 42.4837C19.3587 44.334 18.3627 46.1835 17.4521 47.625C19.6105 48.2621 21.8907 48.6091 24.2538 48.6091C37.5067 48.6091 48.2524 37.864 48.2524 24.6088C48.2524 11.3543 37.5067 0.609118 24.2538 0.609118C10.9996 0.609118 0.253555 11.3543 0.253555 24.6088Z" fill="#CB1F27" style="fill:#CB1F27;fill:color(display-p3 0.7961 0.1216 0.1529);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,6 +0,0 @@
|
|||
<svg width="49" height="49" viewBox="0 0 49 49" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.9415 30.9531C10.9415 33.7429 8.68854 35.9977 5.90095 35.9977C3.11337 35.9977 0.860382 33.7429 0.860382 30.9531C0.860382 28.1633 3.11337 25.9085 5.90095 25.9085H10.9415V30.9531ZM13.4618 30.9531C13.4618 28.1633 15.7148 25.9085 18.5024 25.9085C21.29 25.9085 23.543 28.1633 23.543 30.9531V43.5646C23.543 46.3544 21.29 48.6092 18.5024 48.6092C15.7148 48.6092 13.4618 46.3544 13.4618 43.5646V30.9531Z" fill="#E01E5A" style="fill:#E01E5A;fill:color(display-p3 0.8784 0.1176 0.3529);fill-opacity:1;"/>
|
||||
<path d="M18.5024 10.6983C15.7148 10.6983 13.4618 8.44357 13.4618 5.65376C13.4618 2.86395 15.7148 0.609178 18.5024 0.609178C21.29 0.609178 23.543 2.86395 23.543 5.65376V10.6983H18.5024ZM18.5024 13.2589C21.29 13.2589 23.543 15.5136 23.543 18.3034C23.543 21.0933 21.29 23.348 18.5024 23.348H5.86278C3.07519 23.348 0.822205 21.0933 0.822205 18.3034C0.822205 15.5136 3.07519 13.2589 5.86278 13.2589H18.5024Z" fill="#36C5F0" style="fill:#36C5F0;fill:color(display-p3 0.2118 0.7725 0.9412);fill-opacity:1;"/>
|
||||
<path d="M38.7029 18.3034C38.7029 15.5136 40.9559 13.2589 43.7434 13.2589C46.531 13.2589 48.784 15.5136 48.784 18.3034C48.784 21.0933 46.531 23.348 43.7434 23.348H38.7029V18.3034ZM36.1826 18.3034C36.1826 21.0933 33.9296 23.348 31.142 23.348C28.3544 23.348 26.1014 21.0933 26.1014 18.3034V5.65376C26.1014 2.86395 28.3544 0.609178 31.142 0.609178C33.9296 0.609178 36.1826 2.86395 36.1826 5.65376V18.3034Z" fill="#2EB67D" style="fill:#2EB67D;fill:color(display-p3 0.1804 0.7137 0.4902);fill-opacity:1;"/>
|
||||
<path d="M31.142 38.52C33.9296 38.52 36.1826 40.7748 36.1826 43.5646C36.1826 46.3544 33.9296 48.6092 31.142 48.6092C28.3544 48.6092 26.1014 46.3544 26.1014 43.5646V38.52H31.142ZM31.142 35.9977C28.3544 35.9977 26.1014 33.7429 26.1014 30.9531C26.1014 28.1633 28.3544 25.9085 31.142 25.9085H43.7816C46.5692 25.9085 48.8222 28.1633 48.8222 30.9531C48.8222 33.7429 46.5692 35.9977 43.7816 35.9977H31.142Z" fill="#ECB22E" style="fill:#ECB22E;fill:color(display-p3 0.9255 0.6980 0.1804);fill-opacity:1;"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
39
src/assets/styles/_z-index.scss
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/* Z-Index System
|
||||
* --------------------------------------------------------------------------
|
||||
* A systematic approach to z-index values to maintain consistent layering
|
||||
* throughout the application.
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
// Base layers
|
||||
$z-index-base: 1;
|
||||
$z-index-above: 2;
|
||||
$z-index-hover: 3;
|
||||
|
||||
// Interactive elements
|
||||
$z-index-dropdown: 10;
|
||||
$z-index-sticky: 100;
|
||||
$z-index-fixed: 200;
|
||||
|
||||
// Overlays and modals
|
||||
$z-index-overlay: 1000;
|
||||
$z-index-modal-backdrop: 1000;
|
||||
$z-index-modal: 1050;
|
||||
$z-index-modal-content: 1100;
|
||||
|
||||
// Top-level elements
|
||||
$z-index-popover: 1200;
|
||||
$z-index-tooltip: 1400;
|
||||
$z-index-notification: 10000;
|
||||
|
||||
// Component-specific z-indexes
|
||||
$z-index-header: 100;
|
||||
$z-index-navigation: 150;
|
||||
$z-index-sidebar: 200;
|
||||
$z-index-media-modal: 1050;
|
||||
$z-index-lightbox: 1100;
|
||||
$z-index-toast: 10000;
|
||||
|
||||
// Admin-specific z-indexes
|
||||
$z-index-admin-nav: 100;
|
||||
$z-index-admin-sidebar: 200;
|
||||
$z-index-admin-modal: 1050;
|
||||
|
|
@ -2,5 +2,6 @@
|
|||
// It should NOT contain any actual CSS rules to avoid duplication
|
||||
|
||||
@import './variables.scss';
|
||||
@import './z-index.scss';
|
||||
@import './fonts.scss';
|
||||
@import './themes.scss';
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
:root {
|
||||
--bg-color: #{$grey-80};
|
||||
--page-color: #{$grey-100};
|
||||
--card-color: #{$grey-90};
|
||||
--mention-bg-color: #{$grey-90};
|
||||
--bg-color: #{$gray-80};
|
||||
--page-color: #{$gray-100};
|
||||
--card-color: #{$gray-90};
|
||||
--mention-bg-color: #{$gray-90};
|
||||
|
||||
--text-color: #{$grey-20};
|
||||
--text-color: #{$gray-20};
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
|
|
|
|||
|
|
@ -16,13 +16,41 @@ $unit-3x: $unit * 3;
|
|||
$unit-4x: $unit * 4;
|
||||
$unit-5x: $unit * 5;
|
||||
$unit-6x: $unit * 6;
|
||||
$unit-7x: $unit * 7;
|
||||
$unit-8x: $unit * 8;
|
||||
$unit-9x: $unit * 9;
|
||||
$unit-10x: $unit * 10;
|
||||
$unit-11x: $unit * 11;
|
||||
$unit-12x: $unit * 12;
|
||||
$unit-13x: $unit * 13;
|
||||
$unit-14x: $unit * 14;
|
||||
$unit-15x: $unit * 15;
|
||||
$unit-16x: $unit * 16;
|
||||
$unit-17x: $unit * 17;
|
||||
$unit-18x: $unit * 18;
|
||||
$unit-19x: $unit * 19;
|
||||
$unit-20x: $unit * 20;
|
||||
$unit-22x: $unit * 22;
|
||||
|
||||
// Common pixel values
|
||||
$unit-1px: 1px;
|
||||
$unit-2px: 2px;
|
||||
$unit-3px: 3px;
|
||||
$unit-4px: 4px;
|
||||
$unit-5px: 5px;
|
||||
$unit-6px: 6px;
|
||||
$unit-10px: 10px;
|
||||
$unit-12px: 12px;
|
||||
$unit-14px: 14px;
|
||||
$unit-18px: 18px;
|
||||
$unit-20px: 20px;
|
||||
$unit-24px: 24px;
|
||||
$unit-28px: 28px;
|
||||
$unit-30px: 30px;
|
||||
$unit-36px: 36px;
|
||||
$unit-48px: 48px;
|
||||
$unit-56px: 56px;
|
||||
$unit-64px: 64px;
|
||||
|
||||
/* Corner Radius
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
|
@ -76,77 +104,158 @@ $line-height: 1.3;
|
|||
|
||||
$letter-spacing: -0.02em;
|
||||
|
||||
/* Colors
|
||||
/* Color Scales
|
||||
* -------------------------------------------------------------------------- */
|
||||
$grey-100: #ffffff;
|
||||
$grey-97: #fafafa;
|
||||
$grey-95: #f5f5f5;
|
||||
$grey-90: #f7f7f7;
|
||||
$grey-85: #ebebeb;
|
||||
$grey-80: #e8e8e8;
|
||||
$grey-70: #dfdfdf;
|
||||
$grey-60: #cccccc;
|
||||
$grey-5: #f9f9f9;
|
||||
$grey-50: #b2b2b2;
|
||||
$grey-40: #999999;
|
||||
$grey-30: #808080;
|
||||
$grey-20: #666666;
|
||||
$grey-10: #4d4d4d;
|
||||
$grey-00: #333333;
|
||||
|
||||
$red-90: #ff9d8f;
|
||||
$red-80: #ff6a54;
|
||||
$red-60: #e33d3d;
|
||||
$red-50: #d33;
|
||||
$red-40: #d31919;
|
||||
// Gray scale - from darkest to lightest
|
||||
$gray-00: #333333;
|
||||
$gray-10: #4d4d4d;
|
||||
$gray-20: #666666;
|
||||
$gray-30: #808080;
|
||||
$gray-40: #999999;
|
||||
$gray-50: #b2b2b2;
|
||||
$gray-60: #cccccc;
|
||||
$gray-70: #dfdfdf;
|
||||
$gray-80: #e8e8e8;
|
||||
$gray-85: #ebebeb;
|
||||
$gray-90: #f0f0f0;
|
||||
$gray-95: #f5f5f5;
|
||||
$gray-97: #fafafa;
|
||||
$gray-100: #ffffff;
|
||||
|
||||
// Red scale - from darkest to lightest
|
||||
$red-00: #3d0c0c;
|
||||
$red-10: #7d1919;
|
||||
$red-20: #a31919;
|
||||
$red-30: #c31919;
|
||||
$red-40: #d31919;
|
||||
$red-50: #dd3333;
|
||||
$red-60: #e33d3d;
|
||||
$red-70: #e86a58;
|
||||
$red-80: #ff6a54;
|
||||
$red-90: #ff9d8f;
|
||||
$red-95: #ffcdc5;
|
||||
$red-100: #ffe5e0;
|
||||
|
||||
$blue-60: #2e8bc0;
|
||||
$blue-50: #1482c1;
|
||||
$blue-40: #126fa8;
|
||||
$blue-20: #0f5d8f;
|
||||
$blue-10: #e6f3ff;
|
||||
// Blue scale - from darkest to lightest
|
||||
$blue-00: #0a2540;
|
||||
$blue-10: #0f5d8f;
|
||||
$blue-20: #126fa8;
|
||||
$blue-30: #1279b5;
|
||||
$blue-40: #1482c1;
|
||||
$blue-50: #2e8bc0;
|
||||
$blue-60: #4d9fd0;
|
||||
$blue-70: #70b5de;
|
||||
$blue-80: #9ccde9;
|
||||
$blue-90: #c5eaff;
|
||||
$blue-95: #dff4ff;
|
||||
$blue-100: #f0f9ff;
|
||||
|
||||
$yellow-90: #fff9e6;
|
||||
$yellow-80: #ffeb99;
|
||||
$yellow-70: #ffdd4d;
|
||||
$yellow-60: #ffcc00;
|
||||
$yellow-50: #f5c500;
|
||||
$yellow-40: #e6b800;
|
||||
$yellow-30: #cc9900;
|
||||
$yellow-20: #996600;
|
||||
// Yellow scale - from darkest to lightest
|
||||
$yellow-00: #3d2600;
|
||||
$yellow-10: #664400;
|
||||
$yellow-20: #996600;
|
||||
$yellow-30: #cc9900;
|
||||
$yellow-40: #e6b800;
|
||||
$yellow-50: #f5c500;
|
||||
$yellow-60: #ffcc00;
|
||||
$yellow-70: #ffdd4d;
|
||||
$yellow-80: #ffeb99;
|
||||
$yellow-90: #fff9e6;
|
||||
$yellow-95: #fffcf0;
|
||||
$yellow-100: #fffef9;
|
||||
|
||||
$salmon-pink: #ffd5cf; // Desaturated salmon pink for hover states
|
||||
// Green scale - from darkest to lightest
|
||||
$green-00: #0a3d28;
|
||||
$green-10: #065f46;
|
||||
$green-20: #047857;
|
||||
$green-30: #059669;
|
||||
$green-40: #10b981;
|
||||
$green-50: #34d399;
|
||||
$green-60: #6ee7b7;
|
||||
$green-70: #a7f3d0;
|
||||
$green-80: #d1fae5;
|
||||
$green-90: #ecfdf5;
|
||||
$green-95: #f0fdf9;
|
||||
$green-100: #f9fffc;
|
||||
|
||||
$bg-color: #e8e8e8;
|
||||
$page-color: #ffffff;
|
||||
$card-color: #f7f7f7;
|
||||
$card-color-hover: #f0f0f0;
|
||||
// Orange scale - from darkest to lightest
|
||||
$orange-00: #3d1a0c;
|
||||
$orange-10: #7c2d12;
|
||||
$orange-20: #c2410c;
|
||||
$orange-30: #ea580c;
|
||||
$orange-40: #f97316;
|
||||
$orange-50: #fb923c;
|
||||
$orange-60: #fdba74;
|
||||
$orange-70: #fed7aa;
|
||||
$orange-80: #ffedd5;
|
||||
$orange-90: #fff7ed;
|
||||
$orange-95: #fffbf7;
|
||||
$orange-100: #fffdfa;
|
||||
|
||||
$text-color: #4d4d4d;
|
||||
$text-color-subdued: #666666;
|
||||
$text-color-light: #b2b2b2;
|
||||
// Special colors
|
||||
$black: #000000;
|
||||
$white: #ffffff;
|
||||
$dark-blue: #070610; // Brand specific dark color
|
||||
|
||||
$accent-color: #e33d3d;
|
||||
$grey-color: #f0f0f0;
|
||||
$primary-color: #1482c1; // Using labs color as primary
|
||||
/* Semantic Color Assignments
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
// Backgrounds
|
||||
$bg-color: $gray-80;
|
||||
$page-color: $white;
|
||||
$card-color: $gray-90;
|
||||
$card-color-hover: $gray-85;
|
||||
|
||||
// Text colors
|
||||
$text-color: $gray-10;
|
||||
$text-color-subdued: $gray-20;
|
||||
$text-color-light: $gray-50;
|
||||
|
||||
// Brand colors
|
||||
$accent-color: $red-60;
|
||||
$primary-color: $blue-40;
|
||||
|
||||
// Status colors
|
||||
$success-color: $green-40;
|
||||
$error-color: $red-60;
|
||||
$warning-color: $yellow-50;
|
||||
$info-color: $blue-50;
|
||||
|
||||
// Component specific
|
||||
$image-border-color: rgba(0, 0, 0, 0.03);
|
||||
|
||||
/* Shadows
|
||||
/* Shadows and Overlays
|
||||
* -------------------------------------------------------------------------- */
|
||||
$card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
$card-shadow-hover: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
|
||||
// Shadow utilities
|
||||
$shadow-subtle: rgba(0, 0, 0, 0.08);
|
||||
$shadow-light: rgba(0, 0, 0, 0.1);
|
||||
$shadow-medium: rgba(0, 0, 0, 0.15);
|
||||
$shadow-dark: rgba(0, 0, 0, 0.2);
|
||||
$shadow-heavy: rgba(0, 0, 0, 0.25);
|
||||
|
||||
// Overlay utilities
|
||||
$overlay-light: rgba(255, 255, 255, 0.9);
|
||||
$overlay-white-subtle: rgba(255, 255, 255, 0.95);
|
||||
$overlay-medium: rgba(0, 0, 0, 0.5);
|
||||
$overlay-dark: rgba(0, 0, 0, 0.7);
|
||||
|
||||
// Border utilities
|
||||
$border-light: rgba(0, 0, 0, 0.05);
|
||||
$border-medium: rgba(0, 0, 0, 0.1);
|
||||
$border-dark: rgba(0, 0, 0, 0.2);
|
||||
|
||||
/* Pill colors
|
||||
* -------------------------------------------------------------------------- */
|
||||
$work-bg: #ffcdc5;
|
||||
$work-color: #d0290d;
|
||||
$universe-bg: #ffebc5;
|
||||
$universe-color: #b97d14;
|
||||
$labs-bg: #c5eaff;
|
||||
$labs-color: #1482c1;
|
||||
$work-bg: $red-95;
|
||||
$work-color: $red-30;
|
||||
$universe-bg: $orange-80;
|
||||
$universe-color: $orange-20;
|
||||
$labs-bg: $blue-90;
|
||||
$labs-color: $blue-40;
|
||||
|
||||
$facebook-color: #3b5998;
|
||||
$twitter-color: #55acee;
|
||||
|
|
@ -167,16 +276,29 @@ $mobile-corner-radius: $unit-2x;
|
|||
/* Inputs
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
||||
$input-background-color: #f7f7f7;
|
||||
$input-background-color-hover: #f0f0f0;
|
||||
$input-text-color: #666666;
|
||||
$input-text-color-hover: #4d4d4d;
|
||||
$input-background-color: $gray-90;
|
||||
$input-background-color-hover: $gray-85;
|
||||
$input-text-color: $gray-20;
|
||||
$input-text-color-hover: $gray-10;
|
||||
|
||||
/* Avatar header
|
||||
* -------------------------------------------------------------------------- */
|
||||
$avatar-radius: 2rem;
|
||||
$avatar-url: url('images/header.png');
|
||||
|
||||
/* Animation and Transitions
|
||||
* -------------------------------------------------------------------------- */
|
||||
$transition-instant: 0.1s;
|
||||
$transition-fast: 0.15s;
|
||||
$transition-normal: 0.2s;
|
||||
$transition-medium: 0.3s;
|
||||
$transition-slow: 0.5s;
|
||||
|
||||
$animation-fast: 0.5s;
|
||||
$animation-normal: 1s;
|
||||
$animation-slow: 2s;
|
||||
$animation-very-slow: 3s;
|
||||
|
||||
/* Media queries breakpoints
|
||||
* These needs to be revisited
|
||||
* -------------------------------------------------------------------------- */
|
||||
|
|
@ -184,3 +306,15 @@ $avatar-url: url('images/header.png');
|
|||
$screen-sm-min: 768px;
|
||||
$screen-md-min: 992px;
|
||||
$screen-lg-min: 1200px;
|
||||
|
||||
// Map old color variables to new scale
|
||||
$orange-red: $red-70;
|
||||
$salmon-pink: $red-95; // Desaturated salmon pink for hover states
|
||||
$gray-5: $gray-97; // Was an old variable between 95 and 100
|
||||
$red-error: #dc2626; // Error state color
|
||||
|
||||
// Shadow variables
|
||||
$shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
$shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
$shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
$shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.15);
|
||||
|
|
|
|||
52
src/lib/actions/tooltip.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import tippy, { type Props as TippyProps, type Instance } from 'tippy.js'
|
||||
|
||||
export interface TooltipOptions extends Partial<TippyProps> {
|
||||
content: string
|
||||
enabled?: boolean
|
||||
}
|
||||
|
||||
export function tooltip(element: HTMLElement, options: TooltipOptions | string) {
|
||||
let instance: Instance | undefined
|
||||
|
||||
function createTooltip(opts: TooltipOptions | string) {
|
||||
// Normalize options
|
||||
const config: TooltipOptions = typeof opts === 'string' ? { content: opts } : opts
|
||||
|
||||
// Skip if disabled
|
||||
if (config.enabled === false) return
|
||||
|
||||
// Create tippy instance with sensible defaults
|
||||
instance = tippy(element, {
|
||||
content: config.content,
|
||||
placement: config.placement || 'top',
|
||||
arrow: config.arrow !== false,
|
||||
animation: config.animation || 'scale',
|
||||
theme: config.theme || 'link-tooltip',
|
||||
delay: config.delay || [200, 0],
|
||||
duration: config.duration || [200, 150],
|
||||
offset: config.offset || [0, 10],
|
||||
...config
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize tooltip
|
||||
createTooltip(options)
|
||||
|
||||
return {
|
||||
update(newOptions: TooltipOptions | string) {
|
||||
// Destroy existing instance
|
||||
if (instance) {
|
||||
instance.destroy()
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
// Create new instance with updated options
|
||||
createTooltip(newOptions)
|
||||
},
|
||||
destroy() {
|
||||
if (instance) {
|
||||
instance.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,9 +165,9 @@
|
|||
class:playing={isPlaying}
|
||||
>
|
||||
{#if isPlaying}
|
||||
<PauseIcon />
|
||||
<svelte:component this={PauseIcon} />
|
||||
{:else}
|
||||
<PlayIcon />
|
||||
<svelte:component this={PlayIcon} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
|
|
@ -205,7 +205,7 @@
|
|||
flex-direction: column;
|
||||
gap: $unit * 1.5;
|
||||
text-decoration: none;
|
||||
transition: gap 0.125s ease-in-out;
|
||||
transition: gap $transition-fast ease-in-out;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
|
|
@ -213,14 +213,14 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: $grey-5;
|
||||
background-color: $gray-5;
|
||||
border-radius: $unit;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid $shadow-light;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 0 8px $shadow-light;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
|
|
@ -243,7 +243,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
transition: all 0.3s ease;
|
||||
transition: all $transition-medium ease;
|
||||
backdrop-filter: blur(10px);
|
||||
|
||||
&.corner {
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: $overlay-medium;
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
|
||||
&.corner {
|
||||
|
|
@ -294,7 +294,7 @@
|
|||
.artist-name {
|
||||
font-size: $font-size-extra-small;
|
||||
font-weight: $font-weight-med;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
/>
|
||||
<path
|
||||
d="M65.7859 86.2014C92.2289 102.907 89.0428 144.091 77.087 168.9C71.8296 179.809 63.0485 191.195 56.6854 195.667C50.288 200.162 43.6642 201.649 37.3464 201.171C24.8872 200.228 14 193.756 7.52449 184.348L7.42625 184.234L7.33744 184.111C5.01771 180.892 3.2404 176.136 2.01973 170.671C0.783319 165.136 0.0550549 158.587 0.00299303 151.553C-0.100899 137.51 2.48924 121.207 9.43434 106.872C19.7285 85.9377 45.9684 73.9065 65.7859 86.2014ZM62.7309 92.4309C18.5 64.7561 -5.5 152.756 14 184.111C19.5 191.756 26.5 195.256 37 196.756C44.1521 197.778 49.5 196.256 55.5 191.756C88.5 161.756 91.5 115.756 62.7309 92.4309Z"
|
||||
fill="#070610"
|
||||
fill="$dark-blue"
|
||||
/>
|
||||
<ellipse
|
||||
cx="52.9243"
|
||||
|
|
@ -80,12 +80,12 @@
|
|||
left: 65%;
|
||||
top: 30.6%;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
z-index: $z-index-dropdown;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
animation: fadeIn $transition-medium ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -235,8 +235,8 @@
|
|||
:global(blockquote) {
|
||||
margin: $unit-4x 0;
|
||||
padding: $unit-3x;
|
||||
background: $grey-97;
|
||||
border-left: 4px solid $grey-80;
|
||||
background: $gray-97;
|
||||
border-left: 4px solid $gray-80;
|
||||
border-radius: $unit;
|
||||
color: $text-color;
|
||||
font-style: italic;
|
||||
|
|
@ -247,7 +247,7 @@
|
|||
}
|
||||
|
||||
:global(code) {
|
||||
background: $grey-95;
|
||||
background: $gray-95;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
|
||||
|
|
@ -257,12 +257,12 @@
|
|||
}
|
||||
|
||||
:global(pre) {
|
||||
background: $grey-95;
|
||||
background: $gray-95;
|
||||
padding: $unit-3x;
|
||||
border-radius: $unit;
|
||||
overflow-x: auto;
|
||||
margin: 0 0 $unit-3x;
|
||||
border: 1px solid $grey-85;
|
||||
border: 1px solid $gray-85;
|
||||
|
||||
:global(code) {
|
||||
background: none;
|
||||
|
|
@ -283,7 +283,7 @@
|
|||
|
||||
:global(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid $grey-85;
|
||||
border-top: 1px solid $gray-85;
|
||||
margin: $unit-4x 0;
|
||||
}
|
||||
|
||||
|
|
@ -319,16 +319,16 @@
|
|||
:global(.url-embed-link) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $grey-97;
|
||||
background: $gray-97;
|
||||
border-radius: $card-corner-radius;
|
||||
overflow: hidden;
|
||||
border: 1px solid $grey-80;
|
||||
border: 1px solid $gray-80;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-80;
|
||||
border-color: $gray-80;
|
||||
transform: translateY(-1px);
|
||||
text-decoration: none;
|
||||
box-shadow: 0 0px 8px rgba(0, 0, 0, 0.08);
|
||||
|
|
@ -339,7 +339,7 @@
|
|||
width: 100%;
|
||||
aspect-ratio: 2 / 1;
|
||||
overflow: hidden;
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
}
|
||||
|
||||
:global(.url-embed-image img) {
|
||||
|
|
@ -362,7 +362,7 @@
|
|||
align-items: center;
|
||||
gap: $unit-half;
|
||||
font-size: 0.8125rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
:global(.url-embed-favicon) {
|
||||
|
|
@ -382,7 +382,7 @@
|
|||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
@ -393,7 +393,7 @@
|
|||
:global(.url-embed-description) {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
@ -406,7 +406,7 @@
|
|||
margin: $unit-3x 0;
|
||||
border-radius: $card-corner-radius;
|
||||
overflow: hidden;
|
||||
background: $grey-95;
|
||||
background: $gray-95;
|
||||
}
|
||||
|
||||
:global(.youtube-embed-wrapper) {
|
||||
|
|
|
|||
|
|
@ -52,14 +52,14 @@
|
|||
.copyright {
|
||||
margin: 0;
|
||||
font-size: 0.875rem; // 14px
|
||||
color: $grey-40; // #999
|
||||
color: $gray-40; // #999
|
||||
|
||||
.separator {
|
||||
margin: 0 $unit-half;
|
||||
}
|
||||
|
||||
.rss-link {
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
font-size: 0.875rem; // 14px
|
||||
|
||||
a {
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
}
|
||||
|
||||
.separator {
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@
|
|||
flex-direction: column;
|
||||
gap: $unit * 1.5;
|
||||
text-decoration: none;
|
||||
transition: gap 0.125s ease-in-out;
|
||||
transition: gap $transition-fast ease-in-out;
|
||||
|
||||
img {
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
.game-playtime {
|
||||
font-size: $font-size-extra-small;
|
||||
font-weight: $font-weight-med;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
273
src/lib/components/GeoCard.svelte
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import type { GeoLocation } from '@prisma/client'
|
||||
|
||||
interface Props {
|
||||
location: GeoLocation
|
||||
height?: number
|
||||
interactive?: boolean
|
||||
showPopup?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
let {
|
||||
location,
|
||||
height = 400,
|
||||
interactive = true,
|
||||
showPopup = true,
|
||||
class: className = ''
|
||||
}: Props = $props()
|
||||
|
||||
let mapContainer: HTMLDivElement
|
||||
let map: any
|
||||
let marker: any
|
||||
let leaflet: any
|
||||
|
||||
// Load Leaflet dynamically
|
||||
async function loadLeaflet() {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
// Check if already loaded
|
||||
if (window.L) {
|
||||
leaflet = window.L
|
||||
return
|
||||
}
|
||||
|
||||
// Load Leaflet CSS
|
||||
const link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'
|
||||
link.integrity = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='
|
||||
link.crossOrigin = ''
|
||||
document.head.appendChild(link)
|
||||
|
||||
// Load Leaflet JS
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js'
|
||||
script.integrity = 'sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo='
|
||||
script.crossOrigin = ''
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
|
||||
leaflet = window.L
|
||||
}
|
||||
|
||||
// Initialize map
|
||||
async function initMap() {
|
||||
if (!mapContainer || !leaflet) return
|
||||
|
||||
// Create map
|
||||
map = leaflet.map(mapContainer, {
|
||||
center: [location.latitude, location.longitude],
|
||||
zoom: 15,
|
||||
scrollWheelZoom: interactive,
|
||||
dragging: interactive,
|
||||
touchZoom: interactive,
|
||||
doubleClickZoom: interactive,
|
||||
boxZoom: interactive,
|
||||
keyboard: interactive,
|
||||
zoomControl: interactive
|
||||
})
|
||||
|
||||
// Add tile layer (using OpenStreetMap)
|
||||
leaflet
|
||||
.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: 19
|
||||
})
|
||||
.addTo(map)
|
||||
|
||||
// Create custom marker icon if color is specified
|
||||
let markerOptions = {}
|
||||
if (location.markerColor) {
|
||||
const markerIcon = leaflet.divIcon({
|
||||
className: 'custom-marker',
|
||||
html: `<div style="background-color: ${location.markerColor}; width: 24px; height: 24px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.3);"></div>`,
|
||||
iconSize: [30, 30],
|
||||
iconAnchor: [15, 15],
|
||||
popupAnchor: [0, -15]
|
||||
})
|
||||
markerOptions = { icon: markerIcon }
|
||||
}
|
||||
|
||||
// Add marker
|
||||
marker = leaflet.marker([location.latitude, location.longitude], markerOptions).addTo(map)
|
||||
|
||||
// Add popup if enabled
|
||||
if (showPopup && (location.title || location.description)) {
|
||||
const popupContent = `
|
||||
<div class="location-popup">
|
||||
${location.title ? `<h3>${location.title}</h3>` : ''}
|
||||
${location.description ? `<p>${location.description}</p>` : ''}
|
||||
</div>
|
||||
`
|
||||
marker.bindPopup(popupContent, {
|
||||
autoPan: true,
|
||||
keepInView: true
|
||||
})
|
||||
|
||||
// Open popup by default on non-interactive maps
|
||||
if (!interactive) {
|
||||
marker.openPopup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
function cleanup() {
|
||||
if (map) {
|
||||
map.remove()
|
||||
map = null
|
||||
}
|
||||
marker = null
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await loadLeaflet()
|
||||
await initMap()
|
||||
} catch (error) {
|
||||
console.error('Failed to load map:', error)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
cleanup()
|
||||
})
|
||||
|
||||
// Reinitialize if location changes
|
||||
$effect(() => {
|
||||
if (map && location) {
|
||||
cleanup()
|
||||
initMap()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="geo-card {className}">
|
||||
<div
|
||||
bind:this={mapContainer}
|
||||
class="map-container"
|
||||
style="height: {height}px"
|
||||
role="img"
|
||||
aria-label="Map showing {location.title ||
|
||||
'location'} at coordinates {location.latitude}, {location.longitude}"
|
||||
>
|
||||
<noscript>
|
||||
<div class="map-fallback">
|
||||
<div class="fallback-content">
|
||||
<h3>{location.title}</h3>
|
||||
{#if location.description}
|
||||
<p>{location.description}</p>
|
||||
{/if}
|
||||
<p class="coordinates">
|
||||
{location.latitude.toFixed(6)}, {location.longitude.toFixed(6)}
|
||||
</p>
|
||||
<a
|
||||
href="https://www.openstreetmap.org/?mlat={location.latitude}&mlon={location.longitude}#map=15/{location.latitude}/{location.longitude}"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View on OpenStreetMap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</noscript>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$styles/variables.scss';
|
||||
@import '$styles/mixins.scss';
|
||||
|
||||
.geo-card {
|
||||
width: 100%;
|
||||
border-radius: $image-corner-radius;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 8px $shadow-light;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
background: $gray-95;
|
||||
|
||||
:global(.leaflet-container) {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
:global(.location-popup h3) {
|
||||
margin: 0 0 $unit-half;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
:global(.location-popup p) {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-30;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:global(.leaflet-popup-content-wrapper) {
|
||||
border-radius: $corner-radius-md;
|
||||
box-shadow: 0 2px 8px $shadow-medium;
|
||||
}
|
||||
|
||||
:global(.leaflet-popup-content) {
|
||||
margin: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.map-fallback {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $gray-95;
|
||||
padding: $unit-3x;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fallback-content {
|
||||
h3 {
|
||||
margin: 0 0 $unit;
|
||||
font-size: 1.25rem;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $unit;
|
||||
color: $gray-40;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.coordinates {
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-60;
|
||||
margin-bottom: $unit-2x;
|
||||
}
|
||||
|
||||
a {
|
||||
color: $red-60;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Global styles for Leaflet */
|
||||
:global(.leaflet-control-attribution) {
|
||||
font-size: 0.75rem;
|
||||
background: $overlay-light !important;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -2,6 +2,10 @@
|
|||
import Avatar from './Avatar.svelte'
|
||||
import SegmentedController from './SegmentedController.svelte'
|
||||
import NavDropdown from './NavDropdown.svelte'
|
||||
import NowPlayingBar from './NowPlayingBar.svelte'
|
||||
import { albumStream } from '$lib/stores/album-stream'
|
||||
import { nowPlayingStream } from '$lib/stores/now-playing-stream'
|
||||
import type { Album } from '$lib/types/lastfm'
|
||||
|
||||
let scrollY = $state(0)
|
||||
// Smooth gradient opacity from 0 to 1 over the first 100px of scroll
|
||||
|
|
@ -9,6 +13,50 @@
|
|||
// Padding transition happens more quickly
|
||||
let paddingProgress = $derived(Math.min(scrollY / 50, 1))
|
||||
|
||||
// Now playing state
|
||||
let isHoveringAvatar = $state(false)
|
||||
let currentlyPlayingAlbum = $state<Album | null>(null)
|
||||
let isPlayingMusic = $state(false)
|
||||
|
||||
// Subscribe to album updates
|
||||
$effect(() => {
|
||||
const unsubscribe = albumStream.subscribe((state) => {
|
||||
const nowPlaying = state.albums.find((album) => album.isNowPlaying)
|
||||
currentlyPlayingAlbum = nowPlaying || null
|
||||
isPlayingMusic = !!nowPlaying
|
||||
|
||||
// Debug logging
|
||||
if (nowPlaying) {
|
||||
console.log('Header: Now playing detected:', {
|
||||
artist: nowPlaying.artist.name,
|
||||
album: nowPlaying.name,
|
||||
track: nowPlaying.nowPlayingTrack
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
// Also check now playing stream for updates
|
||||
$effect(() => {
|
||||
const unsubscribe = nowPlayingStream.subscribe((state) => {
|
||||
const hasNowPlaying = Array.from(state.updates.values()).some((update) => update.isNowPlaying)
|
||||
console.log('Header: nowPlayingStream update:', {
|
||||
hasNowPlaying,
|
||||
updatesCount: state.updates.size
|
||||
})
|
||||
// Only clear if we explicitly know music stopped
|
||||
if (!hasNowPlaying && currentlyPlayingAlbum && state.updates.size > 0) {
|
||||
// Music stopped
|
||||
currentlyPlayingAlbum = null
|
||||
isPlayingMusic = false
|
||||
}
|
||||
})
|
||||
|
||||
return unsubscribe
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
let ticking = false
|
||||
|
||||
|
|
@ -30,6 +78,18 @@
|
|||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
// Get the best available album artwork
|
||||
function getAlbumArtwork(album: Album): string {
|
||||
if (album.appleMusicData?.highResArtwork) {
|
||||
// Use smaller size for the header
|
||||
return album.appleMusicData.highResArtwork.replace('3000x3000', '100x100')
|
||||
}
|
||||
if (album.images.itunes) {
|
||||
return album.images.itunes.replace('3000x3000', '100x100')
|
||||
}
|
||||
return album.images.large || album.images.medium || ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<header
|
||||
|
|
@ -37,11 +97,28 @@
|
|||
style="--gradient-opacity: {gradientOpacity}; --padding-progress: {paddingProgress}"
|
||||
>
|
||||
<div class="header-content">
|
||||
<a href="/about" class="header-link" aria-label="@jedmund">
|
||||
<a
|
||||
href="/about"
|
||||
class="header-link"
|
||||
aria-label="@jedmund"
|
||||
onmouseenter={() => {
|
||||
isHoveringAvatar = true
|
||||
console.log('Header: Hovering avatar, showing now playing?', {
|
||||
isHoveringAvatar: true,
|
||||
isPlayingMusic,
|
||||
currentlyPlayingAlbum: currentlyPlayingAlbum?.name
|
||||
})
|
||||
}}
|
||||
onmouseleave={() => (isHoveringAvatar = false)}
|
||||
>
|
||||
<Avatar />
|
||||
</a>
|
||||
<div class="nav-desktop">
|
||||
<SegmentedController />
|
||||
{#if isHoveringAvatar && isPlayingMusic && currentlyPlayingAlbum}
|
||||
<NowPlayingBar album={currentlyPlayingAlbum} {getAlbumArtwork} />
|
||||
{:else}
|
||||
<SegmentedController />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="nav-mobile">
|
||||
<NavDropdown />
|
||||
|
|
@ -53,14 +130,14 @@
|
|||
.site-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
z-index: $z-index-header;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
// Smooth padding transition based on scroll
|
||||
padding: calc($unit-5x - ($unit-5x - $unit-2x) * var(--padding-progress)) $unit-2x;
|
||||
pointer-events: none;
|
||||
// Add a very subtle transition to smooth out any remaining jitter
|
||||
transition: padding 0.1s ease-out;
|
||||
transition: padding $transition-instant ease-out;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
padding: calc($unit-3x - ($unit-3x - $unit-2x) * var(--padding-progress)) $unit-2x;
|
||||
|
|
@ -73,7 +150,7 @@
|
|||
left: 0;
|
||||
right: 0;
|
||||
height: 120px;
|
||||
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.15), transparent);
|
||||
background: linear-gradient(to bottom, $shadow-medium, transparent);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
mask-image: linear-gradient(to bottom, black 0%, black 15%, transparent 90%);
|
||||
|
|
@ -82,7 +159,7 @@
|
|||
z-index: -1;
|
||||
opacity: var(--gradient-opacity);
|
||||
// Add a very subtle transition to smooth out any remaining jitter
|
||||
transition: opacity 0.1s ease-out;
|
||||
transition: opacity $transition-instant ease-out;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
67
src/lib/components/HorizontalPhotoScroll.svelte
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<script lang="ts">
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
interface Props {
|
||||
photos: Photo[]
|
||||
showCaptions?: boolean
|
||||
}
|
||||
|
||||
let { photos = [], showCaptions = true }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="horizontal-scroll">
|
||||
{#each photos as photo}
|
||||
{@const mediaId = photo.id.replace(/^(media|photo)-/, '')}
|
||||
<a href="/photos/{mediaId}" class="photo-link">
|
||||
<img src={photo.src} alt={photo.alt} />
|
||||
{#if showCaptions && photo.caption}
|
||||
<p class="caption">{photo.caption}</p>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.horizontal-scroll {
|
||||
display: flex;
|
||||
gap: $unit-3x;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0 $unit-3x;
|
||||
|
||||
// Hide scrollbar
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-link {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
img {
|
||||
height: 60vh;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
border-radius: $corner-radius-md;
|
||||
}
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
color: $gray-20;
|
||||
padding: $unit 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { PhotoItem as PhotoItemType } from '$lib/types/photos'
|
||||
import { isAlbum } from '$lib/types/photos'
|
||||
|
||||
const {
|
||||
photoItems,
|
||||
albumSlug
|
||||
}: {
|
||||
photoItems: PhotoItemType[]
|
||||
albumSlug?: string
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<div class="horizontal-scroll">
|
||||
{#each photoItems as item}
|
||||
{#if isAlbum(item)}
|
||||
<a href="/photos/{item.slug}" class="photo-link">
|
||||
<img src={item.coverPhoto.src} alt={item.title} />
|
||||
<p class="caption">{item.title}</p>
|
||||
</a>
|
||||
{:else}
|
||||
{@const mediaId = item.id.replace(/^(media|photo)-/, '')}
|
||||
<a href="/photos/{albumSlug ? `${albumSlug}/${mediaId}` : `p/${mediaId}`}" class="photo-link">
|
||||
<img src={item.src} alt={item.alt} />
|
||||
{#if item.caption}
|
||||
<p class="caption">{item.caption}</p>
|
||||
{/if}
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.horizontal-scroll {
|
||||
display: flex;
|
||||
gap: $unit-3x;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0 $unit-3x;
|
||||
|
||||
// Hide scrollbar
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-link {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
img {
|
||||
height: 60vh;
|
||||
width: auto;
|
||||
object-fit: contain;
|
||||
border-radius: $corner-radius-md;
|
||||
}
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
color: $grey-20;
|
||||
padding: $unit 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -176,7 +176,7 @@
|
|||
|
||||
<style lang="scss">
|
||||
.lab-card {
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
border-radius: $card-corner-radius;
|
||||
padding: $unit-3x;
|
||||
display: flex;
|
||||
|
|
@ -249,7 +249,7 @@
|
|||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
line-height: 1.3;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
|
||||
.project-year {
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -268,7 +268,7 @@
|
|||
margin: 0 0 $unit-3x 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -310,12 +310,12 @@
|
|||
|
||||
&.secondary {
|
||||
background: transparent;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
border-color: rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { fade, scale } from 'svelte/transition'
|
||||
import CloseButton from '$components/icons/CloseButton.svelte'
|
||||
|
||||
// Convert CSS transition durations to milliseconds for Svelte transitions
|
||||
const TRANSITION_NORMAL_MS = 200 // $transition-normal: 0.2s
|
||||
|
||||
let {
|
||||
images = [],
|
||||
|
|
@ -76,7 +80,7 @@
|
|||
<div
|
||||
class="lightbox-backdrop"
|
||||
onclick={handleBackgroundClick}
|
||||
transition:fade={{ duration: 200 }}
|
||||
transition:fade={{ duration: TRANSITION_NORMAL_MS }}
|
||||
role="button"
|
||||
tabindex="-1"
|
||||
>
|
||||
|
|
@ -85,7 +89,7 @@
|
|||
<img
|
||||
src={images[selectedIndex]}
|
||||
alt="{alt} {selectedIndex + 1}"
|
||||
transition:scale={{ duration: 200, start: 0.9 }}
|
||||
transition:scale={{ duration: TRANSITION_NORMAL_MS, start: 0.9 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -108,20 +112,7 @@
|
|||
</div>
|
||||
|
||||
<button class="lightbox-close" onclick={close} aria-label="Close lightbox">
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18 6L6 18M6 6l12 12"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<CloseButton />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -131,7 +122,7 @@
|
|||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
z-index: 1400;
|
||||
z-index: $z-index-lightbox;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
|
@ -208,7 +199,7 @@
|
|||
inset: 0;
|
||||
border-radius: $unit-2x;
|
||||
border: 2px solid transparent;
|
||||
z-index: 2;
|
||||
z-index: $z-index-above;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
|
@ -219,7 +210,7 @@
|
|||
inset: 2px;
|
||||
border-radius: calc($unit-2x - 2px);
|
||||
border: 2px solid transparent;
|
||||
z-index: 3;
|
||||
z-index: $z-index-hover;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
|
@ -237,7 +228,7 @@
|
|||
}
|
||||
|
||||
&::after {
|
||||
border-color: $grey-00; // Black inner border
|
||||
border-color: $gray-00; // Black inner border
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -246,7 +237,7 @@
|
|||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: $z-index-base;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
|
@ -261,7 +252,7 @@
|
|||
border-radius: 50%;
|
||||
border: none;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: $grey-100;
|
||||
color: $gray-100;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -101,10 +101,10 @@
|
|||
.link-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
border-radius: $image-corner-radius;
|
||||
overflow: hidden;
|
||||
border: 1px solid $grey-80;
|
||||
border: 1px solid $gray-80;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
transition: border-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-50;
|
||||
border-color: $gray-50;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-80;
|
||||
border-color: $gray-80;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
align-items: center;
|
||||
gap: $unit;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
.favicon {
|
||||
|
|
@ -180,7 +180,7 @@
|
|||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
@ -191,7 +191,7 @@
|
|||
.link-description {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
@ -200,9 +200,9 @@
|
|||
}
|
||||
|
||||
.skeleton {
|
||||
background: $grey-80;
|
||||
background: $gray-80;
|
||||
border-radius: 4px;
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
animation: pulse $animation-slow ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-meta {
|
||||
|
|
@ -232,7 +232,7 @@
|
|||
|
||||
.error-message {
|
||||
margin: 0 0 $unit;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,71 +1,13 @@
|
|||
<script lang="ts">
|
||||
import Masonry from 'svelte-bricks'
|
||||
import PhotoItem from './PhotoItem.svelte'
|
||||
import type { PhotoItem as PhotoItemType } from '$lib/types/photos'
|
||||
import PhotoGrid from './PhotoGrid.svelte'
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
const {
|
||||
photoItems,
|
||||
albumSlug
|
||||
}: {
|
||||
photoItems: PhotoItemType[]
|
||||
albumSlug?: string
|
||||
} = $props()
|
||||
interface Props {
|
||||
photos: Photo[]
|
||||
columns?: 1 | 2 | 3 | 'auto'
|
||||
}
|
||||
|
||||
// Responsive column configuration
|
||||
// These values work well with our existing design
|
||||
let minColWidth = 200 // Minimum column width in px
|
||||
let maxColWidth = 400 // Maximum column width in px
|
||||
let gap = 16 // Gap between items (equivalent to $unit-2x)
|
||||
|
||||
// On tablet/phone, we want larger minimum widths
|
||||
let windowWidth = $state(0)
|
||||
|
||||
$effect(() => {
|
||||
// Adjust column widths based on viewport
|
||||
if (windowWidth < 768) {
|
||||
// Phone: single column
|
||||
minColWidth = windowWidth - 48 // Account for padding
|
||||
maxColWidth = windowWidth - 48
|
||||
} else if (windowWidth < 1024) {
|
||||
// Tablet: 2 columns
|
||||
minColWidth = 300
|
||||
maxColWidth = 500
|
||||
} else {
|
||||
// Desktop: 3 columns
|
||||
minColWidth = 200
|
||||
maxColWidth = 400
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure unique IDs for keyed blocks to prevent shifting
|
||||
const getId = (item: PhotoItemType) => item.id
|
||||
let { photos = [], columns = 'auto' }: Props = $props()
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={windowWidth} />
|
||||
|
||||
<div class="masonry-container">
|
||||
<Masonry
|
||||
items={photoItems}
|
||||
{minColWidth}
|
||||
{maxColWidth}
|
||||
{gap}
|
||||
{getId}
|
||||
animate={false}
|
||||
duration={0}
|
||||
class="photo-masonry"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<PhotoItem {item} {albumSlug} />
|
||||
{/snippet}
|
||||
</Masonry>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.masonry-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(.photo-masonry) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<PhotoGrid {photos} {columns} masonry={true} gap="medium" />
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
}
|
||||
|
||||
footer {
|
||||
color: $grey-50;
|
||||
color: $gray-50;
|
||||
flex-grow: 0;
|
||||
font-weight: $font-weight-med;
|
||||
font-size: 0.9rem;
|
||||
|
|
|
|||
|
|
@ -1,350 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
interface Props {
|
||||
previewUrl: string
|
||||
albumName?: string
|
||||
artistName?: string
|
||||
onPlayStateChange?: (isPlaying: boolean) => void
|
||||
}
|
||||
|
||||
let { previewUrl, albumName = '', artistName = '', onPlayStateChange }: Props = $props()
|
||||
|
||||
let audio: HTMLAudioElement | null = $state(null)
|
||||
let isPlaying = $state(false)
|
||||
let isLoading = $state(false)
|
||||
let currentTime = $state(0)
|
||||
let duration = $state(30) // Apple Music previews are 30 seconds
|
||||
let volume = $state(1)
|
||||
let hasError = $state(false)
|
||||
|
||||
$effect(() => {
|
||||
if (audio) {
|
||||
audio.volume = volume
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
onPlayStateChange?.(isPlaying)
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
// Listen for other audio elements playing
|
||||
const handleAudioPlay = (e: Event) => {
|
||||
const playingAudio = e.target as HTMLAudioElement
|
||||
if (playingAudio !== audio && audio && !audio.paused) {
|
||||
pause()
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('play', handleAudioPlay, true)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('play', handleAudioPlay, true)
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
if (audio) {
|
||||
audio.pause()
|
||||
audio = null
|
||||
}
|
||||
})
|
||||
|
||||
function togglePlayPause() {
|
||||
if (isPlaying) {
|
||||
pause()
|
||||
} else {
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
async function play() {
|
||||
if (!audio || hasError) return
|
||||
|
||||
isLoading = true
|
||||
try {
|
||||
await audio.play()
|
||||
isPlaying = true
|
||||
} catch (error) {
|
||||
console.error('Failed to play preview:', error)
|
||||
hasError = true
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function pause() {
|
||||
if (!audio) return
|
||||
audio.pause()
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
function handleTimeUpdate() {
|
||||
if (audio) {
|
||||
currentTime = audio.currentTime
|
||||
}
|
||||
}
|
||||
|
||||
function handleEnded() {
|
||||
isPlaying = false
|
||||
currentTime = 0
|
||||
if (audio) {
|
||||
audio.currentTime = 0
|
||||
}
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
hasError = true
|
||||
isPlaying = false
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
function handleLoadedMetadata() {
|
||||
if (audio) {
|
||||
duration = audio.duration
|
||||
}
|
||||
}
|
||||
|
||||
function seek(e: MouseEvent) {
|
||||
const target = e.currentTarget as HTMLElement
|
||||
const rect = target.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const percentage = x / rect.width
|
||||
const newTime = percentage * duration
|
||||
|
||||
if (audio) {
|
||||
audio.currentTime = newTime
|
||||
currentTime = newTime
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.code === 'Space') {
|
||||
e.preventDefault()
|
||||
togglePlayPause()
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(time: number): string {
|
||||
const minutes = Math.floor(time / 60)
|
||||
const seconds = Math.floor(time % 60)
|
||||
return `${minutes}:${seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const progressPercentage = $derived((currentTime / duration) * 100)
|
||||
</script>
|
||||
|
||||
<div class="music-preview" role="region" aria-label="Music preview player">
|
||||
<audio
|
||||
bind:this={audio}
|
||||
src={previewUrl}
|
||||
onloadedmetadata={handleLoadedMetadata}
|
||||
ontimeupdate={handleTimeUpdate}
|
||||
onended={handleEnded}
|
||||
onerror={handleError}
|
||||
preload="metadata"
|
||||
/>
|
||||
|
||||
<div class="controls">
|
||||
<button
|
||||
class="play-button"
|
||||
onclick={togglePlayPause}
|
||||
onkeydown={handleKeydown}
|
||||
disabled={hasError || isLoading}
|
||||
aria-label={isPlaying ? 'Pause' : 'Play'}
|
||||
aria-pressed={isPlaying}
|
||||
>
|
||||
{#if isLoading}
|
||||
<span class="loading-spinner" aria-hidden="true">⟳</span>
|
||||
{:else if hasError}
|
||||
<span aria-hidden="true">⚠</span>
|
||||
{:else if isPlaying}
|
||||
<span aria-hidden="true">❚❚</span>
|
||||
{:else}
|
||||
<span aria-hidden="true">▶</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div class="progress-container">
|
||||
<div
|
||||
class="progress-bar"
|
||||
onclick={seek}
|
||||
role="slider"
|
||||
aria-label="Seek"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={duration}
|
||||
aria-valuenow={currentTime}
|
||||
tabindex="0"
|
||||
>
|
||||
<div class="progress-fill" style="width: {progressPercentage}%"></div>
|
||||
</div>
|
||||
<div class="time-display">
|
||||
<span>{formatTime(currentTime)}</span>
|
||||
<span>/</span>
|
||||
<span>{formatTime(duration)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="volume-control">
|
||||
<label for="volume" class="visually-hidden">Volume</label>
|
||||
<input
|
||||
id="volume"
|
||||
type="range"
|
||||
bind:value={volume}
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
aria-label="Volume control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if hasError}
|
||||
<p class="error-message" transition:fade={{ duration: 200 }}>Preview unavailable</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.music-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
padding: var(--spacing-sm);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.play-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-contrast);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: scale(1.05);
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
background: var(--color-surface-secondary);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover .progress-fill {
|
||||
background: var(--color-primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: var(--color-primary);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
display: flex;
|
||||
gap: var(--spacing-2xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.volume-control {
|
||||
width: 80px;
|
||||
|
||||
input[type='range'] {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background: var(--color-surface-secondary);
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: 0;
|
||||
padding: var(--spacing-xs);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-error);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -16,12 +16,22 @@
|
|||
text: string
|
||||
href: string
|
||||
variant: 'work' | 'universe' | 'labs' | 'photos' | 'about'
|
||||
subItems?: { text: string; href: string }[]
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ icon: WorkIcon, text: 'Work', href: '/', variant: 'work' },
|
||||
{ icon: UniverseIcon, text: 'Universe', href: '/universe', variant: 'universe' },
|
||||
{ icon: PhotosIcon, text: 'Photos', href: '/photos', variant: 'photos' },
|
||||
{
|
||||
icon: PhotosIcon,
|
||||
text: 'Photography',
|
||||
href: '/photos',
|
||||
variant: 'photos',
|
||||
subItems: [
|
||||
{ text: 'Photos', href: '/photos' },
|
||||
{ text: 'Albums', href: '/albums' }
|
||||
]
|
||||
},
|
||||
{ icon: LabsIcon, text: 'Labs', href: '/labs', variant: 'labs' },
|
||||
{ icon: AboutIcon, text: 'About', href: '/about', variant: 'about' }
|
||||
]
|
||||
|
|
@ -32,9 +42,11 @@
|
|||
? navItems[0]
|
||||
: currentPath === '/about'
|
||||
? navItems[4]
|
||||
: navItems.find((item) =>
|
||||
currentPath.startsWith(item.href === '/' ? '/work' : item.href)
|
||||
) || navItems[0]
|
||||
: currentPath.startsWith('/albums') || currentPath.startsWith('/photos')
|
||||
? navItems.find((item) => item.variant === 'photos')
|
||||
: navItems.find((item) =>
|
||||
currentPath.startsWith(item.href === '/' ? '/work' : item.href)
|
||||
) || navItems[0]
|
||||
)
|
||||
|
||||
// Get background color based on variant
|
||||
|
|
@ -120,15 +132,34 @@
|
|||
{#if isOpen}
|
||||
<div class="dropdown-menu">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
href={item.href}
|
||||
class="dropdown-item"
|
||||
class:active={item === activeItem}
|
||||
onclick={() => (isOpen = false)}
|
||||
>
|
||||
<item.icon class="nav-icon" />
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
{#if item.subItems}
|
||||
<div class="dropdown-section">
|
||||
<div class="dropdown-item section-header">
|
||||
<item.icon class="nav-icon" />
|
||||
<span>{item.text}</span>
|
||||
</div>
|
||||
{#each item.subItems as subItem}
|
||||
<a
|
||||
href={subItem.href}
|
||||
class="dropdown-item sub-item"
|
||||
class:active={currentPath === subItem.href}
|
||||
onclick={() => (isOpen = false)}
|
||||
>
|
||||
<span>{subItem.text}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<a
|
||||
href={item.href}
|
||||
class="dropdown-item"
|
||||
class:active={item === activeItem}
|
||||
onclick={() => (isOpen = false)}
|
||||
>
|
||||
<item.icon class="nav-icon" />
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -137,8 +168,8 @@
|
|||
<style lang="scss">
|
||||
.nav-dropdown {
|
||||
position: relative;
|
||||
height: 52px; // Match avatar height
|
||||
min-width: 180px; // Wider to better match dropdown menu
|
||||
height: $unit-6x + $unit-half; // Match avatar height
|
||||
min-width: $unit-22x + $unit-half; // Wider to better match dropdown menu
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
|
|
@ -149,17 +180,17 @@
|
|||
height: 100%;
|
||||
padding: 0 $unit-2x;
|
||||
border: none;
|
||||
border-radius: 100px;
|
||||
border-radius: $corner-radius-full;
|
||||
background: white;
|
||||
font-size: 1rem;
|
||||
font-size: $font-size;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
transition: all $transition-normal ease;
|
||||
box-shadow: 0 $unit-1px $unit-3px $shadow-light;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
transform: translateY(-$unit-1px);
|
||||
box-shadow: 0 $unit-2px $unit-6px $shadow-medium;
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
|
@ -167,21 +198,21 @@
|
|||
}
|
||||
|
||||
:global(svg.nav-icon) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: $unit-20px;
|
||||
height: $unit-20px;
|
||||
flex-shrink: 0;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
:global(svg.chevron) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: $unit-2x;
|
||||
height: $unit-2x;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.2s ease;
|
||||
transition: transform $transition-normal ease;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2px;
|
||||
stroke-width: $unit-2px;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
|
||||
|
|
@ -196,13 +227,22 @@
|
|||
top: calc(100% + $unit);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 180px;
|
||||
min-width: $unit-22x + $unit-half;
|
||||
background: white;
|
||||
border-radius: $unit-2x;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 $unit-half $unit-12px $shadow-medium;
|
||||
padding: $unit;
|
||||
z-index: 1000;
|
||||
animation: dropdownOpen 0.2s ease;
|
||||
z-index: 10;
|
||||
animation: dropdownOpen $transition-normal ease;
|
||||
}
|
||||
|
||||
.dropdown-section {
|
||||
& + .dropdown-section,
|
||||
& + .dropdown-item {
|
||||
margin-top: $unit;
|
||||
padding-top: $unit;
|
||||
border-top: $unit-1px solid $gray-95;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
|
@ -212,22 +252,37 @@
|
|||
padding: $unit-2x $unit-2x;
|
||||
border-radius: $unit;
|
||||
text-decoration: none;
|
||||
color: $grey-20;
|
||||
font-size: 1rem;
|
||||
transition: background-color 0.2s ease;
|
||||
color: $gray-20;
|
||||
font-size: $font-size;
|
||||
transition: background-color $transition-normal ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-97;
|
||||
&:hover:not(.section-header) {
|
||||
background-color: $gray-97;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.section-header {
|
||||
color: $gray-50;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: $unit $unit-2x;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.sub-item {
|
||||
padding-left: $unit-4x + $unit-2x;
|
||||
font-size: 0.9375rem; // 15px
|
||||
}
|
||||
|
||||
:global(svg.nav-icon) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
width: $unit-20px;
|
||||
height: $unit-20px;
|
||||
flex-shrink: 0;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
|
@ -236,7 +291,7 @@
|
|||
@keyframes dropdownOpen {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-8px);
|
||||
transform: translateX(-50%) translateY(-$unit);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
|
|
|
|||
|
|
@ -54,8 +54,8 @@
|
|||
border-radius: $unit * 2;
|
||||
font-size: $font-size-small;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 10;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
z-index: $z-index-dropdown;
|
||||
animation: fadeIn $transition-medium ease-out;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
.bar {
|
||||
width: 3px;
|
||||
background: $accent-color;
|
||||
animation: dance 0.6s ease-in-out infinite;
|
||||
animation: dance $animation-fast ease-in-out infinite;
|
||||
transform-origin: bottom;
|
||||
}
|
||||
|
||||
|
|
|
|||
144
src/lib/components/NowPlayingBar.svelte
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<script lang="ts">
|
||||
import type { Album } from '$lib/types/lastfm'
|
||||
|
||||
interface Props {
|
||||
album: Album
|
||||
getAlbumArtwork: (album: Album) => string
|
||||
}
|
||||
|
||||
let { album, getAlbumArtwork }: Props = $props()
|
||||
|
||||
const trackText = $derived(`${album.artist.name} — ${album.name}${
|
||||
album.appleMusicData?.releaseDate
|
||||
? ` (${new Date(album.appleMusicData.releaseDate).getFullYear()})`
|
||||
: ''
|
||||
} — ${album.nowPlayingTrack || album.name}`)
|
||||
</script>
|
||||
|
||||
<nav class="now-playing-bar">
|
||||
<div class="now-playing-content">
|
||||
{#if getAlbumArtwork(album)}
|
||||
<img src={getAlbumArtwork(album)} alt="{album.name} album cover" class="album-thumbnail" />
|
||||
{/if}
|
||||
<span class="track-info">
|
||||
<span class="now-playing-label">Now playing</span>
|
||||
<div class="marquee-container">
|
||||
<span class="now-playing-title">{trackText}</span>
|
||||
<span class="now-playing-title" aria-hidden="true">{trackText}</span>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
.now-playing-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: $gray-100;
|
||||
padding: calc($unit-half - 1px) $unit-half;
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 1px 3px $shadow-light;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
min-height: 58.4px;
|
||||
width: 404px;
|
||||
}
|
||||
|
||||
.now-playing-label {
|
||||
font-size: $font-size-extra-small;
|
||||
font-weight: $font-weight-med;
|
||||
color: $gray-50;
|
||||
}
|
||||
|
||||
.now-playing-content {
|
||||
display: grid;
|
||||
grid-template-columns: 32px 1fr;
|
||||
align-items: center;
|
||||
gap: $unit-2x;
|
||||
width: 100%;
|
||||
padding: $unit $unit-2x;
|
||||
color: $text-color-subdued;
|
||||
}
|
||||
|
||||
.music-note {
|
||||
font-size: $font-size-small;
|
||||
opacity: 0.8;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.album-thumbnail {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.track-info {
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-med;
|
||||
text-align: center;
|
||||
padding-right: 32px; // Balance out the image on the left
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.marquee-container {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 50px; // Space between repeated text
|
||||
|
||||
// Gradient overlays
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, $gray-100, transparent);
|
||||
}
|
||||
|
||||
&::after {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, $gray-100, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.now-playing-title {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
animation: marquee 15s linear infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
0% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-100% - 50px)); // Include gap in animation
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
235
src/lib/components/PhotoGrid.svelte
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<script lang="ts">
|
||||
import PhotoItem from './PhotoItem.svelte'
|
||||
import Masonry from 'svelte-bricks'
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
interface Props {
|
||||
photos: Photo[]
|
||||
columns?: 1 | 2 | 3 | 'auto'
|
||||
gap?: 'small' | 'medium' | 'large'
|
||||
showCaptions?: boolean
|
||||
masonry?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
let {
|
||||
photos = [],
|
||||
columns = 'auto',
|
||||
gap = 'medium',
|
||||
showCaptions = false,
|
||||
masonry = false,
|
||||
class: className = ''
|
||||
}: Props = $props()
|
||||
|
||||
// Split photos into columns for column-based layouts
|
||||
function splitIntoColumns(photos: Photo[], numColumns: number): Photo[][] {
|
||||
const columns: Photo[][] = Array.from({ length: numColumns }, () => [])
|
||||
|
||||
photos.forEach((photo, index) => {
|
||||
columns[index % numColumns].push(photo)
|
||||
})
|
||||
|
||||
return columns
|
||||
}
|
||||
|
||||
const columnPhotos = $derived(
|
||||
(columns === 1 || columns === 2 || columns === 3) && !masonry
|
||||
? splitIntoColumns(photos, columns)
|
||||
: []
|
||||
)
|
||||
|
||||
// Window width for responsive masonry
|
||||
let windowWidth = $state(0)
|
||||
|
||||
// Calculate masonry column widths based on columns prop
|
||||
const masonryConfig = $derived(() => {
|
||||
if (!masonry) return null
|
||||
|
||||
const gapSize = gap === 'small' ? 8 : gap === 'large' ? 32 : 16
|
||||
|
||||
if (columns === 1) {
|
||||
const width = windowWidth - 64 // Account for padding
|
||||
return { minColWidth: width, maxColWidth: width, gap: gapSize }
|
||||
} else if (columns === 2) {
|
||||
const width = Math.floor((windowWidth - 64 - gapSize) / 2)
|
||||
return { minColWidth: width - 10, maxColWidth: width + 10, gap: gapSize }
|
||||
} else if (columns === 3) {
|
||||
const width = Math.floor((windowWidth - 64 - gapSize * 2) / 3)
|
||||
return { minColWidth: width - 10, maxColWidth: width + 10, gap: gapSize }
|
||||
} else {
|
||||
// Auto columns
|
||||
return { minColWidth: 200, maxColWidth: 400, gap: gapSize }
|
||||
}
|
||||
})
|
||||
|
||||
// Ensure unique IDs for svelte-bricks
|
||||
const getId = (photo: Photo) => photo.id
|
||||
|
||||
// CSS classes based on props
|
||||
const gridClass = $derived(
|
||||
`photo-grid photo-grid--${columns === 'auto' ? 'auto' : `${columns}-column`} photo-grid--gap-${gap} ${masonry ? 'photo-grid--masonry' : 'photo-grid--square'} ${className}`
|
||||
)
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerWidth={windowWidth} />
|
||||
|
||||
<div class={gridClass}>
|
||||
{#if masonry && masonryConfig()}
|
||||
{@const config = masonryConfig()}
|
||||
<!-- Masonry layout using svelte-bricks -->
|
||||
<Masonry
|
||||
items={photos}
|
||||
minColWidth={config.minColWidth}
|
||||
maxColWidth={config.maxColWidth}
|
||||
gap={config.gap}
|
||||
{getId}
|
||||
animate={false}
|
||||
duration={0}
|
||||
class="photo-masonry"
|
||||
>
|
||||
{#snippet children({ item })}
|
||||
<div class="photo-grid__item">
|
||||
<PhotoItem {item} />
|
||||
{#if showCaptions}
|
||||
<p class="photo-caption">{item.caption || ''}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
</Masonry>
|
||||
{:else if (columns === 1 || columns === 2 || columns === 3) && !masonry}
|
||||
<!-- Column-based layout for square thumbnails -->
|
||||
{#each columnPhotos as column, colIndex}
|
||||
<div class="photo-grid__column">
|
||||
{#each column as photo}
|
||||
<div class="photo-grid__item">
|
||||
<PhotoItem item={photo} />
|
||||
{#if showCaptions}
|
||||
<p class="photo-caption">{photo.caption || ''}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
{:else}
|
||||
<!-- Auto grid layout -->
|
||||
<div class="photo-grid__auto">
|
||||
{#each photos as photo}
|
||||
<div class="photo-grid__item">
|
||||
<PhotoItem item={photo} />
|
||||
{#if showCaptions}
|
||||
<p class="photo-caption">{photo.caption || ''}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.photo-grid {
|
||||
width: 100%;
|
||||
|
||||
// Gap variations
|
||||
&--gap-small {
|
||||
--grid-gap: 8px;
|
||||
}
|
||||
&--gap-medium {
|
||||
--grid-gap: 16px;
|
||||
}
|
||||
&--gap-large {
|
||||
--grid-gap: 32px;
|
||||
}
|
||||
|
||||
// Column-based layouts
|
||||
&--1-column,
|
||||
&--2-column,
|
||||
&--3-column {
|
||||
display: flex;
|
||||
gap: var(--grid-gap);
|
||||
|
||||
@include breakpoint('mobile') {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&__column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--grid-gap);
|
||||
}
|
||||
|
||||
// Square thumbnail mode (non-masonry, except for single column)
|
||||
&--square {
|
||||
// Only apply square thumbnails for multi-column and auto layouts
|
||||
&.photo-grid--2-column,
|
||||
&.photo-grid--3-column,
|
||||
&.photo-grid--auto {
|
||||
.photo-grid__item {
|
||||
:global(.photo-item) {
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:global(.photo-button) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.single-photo) {
|
||||
height: 100%;
|
||||
aspect-ratio: 1 !important;
|
||||
}
|
||||
|
||||
:global(.single-photo img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Masonry mode using svelte-bricks
|
||||
&--masonry {
|
||||
:global(.photo-masonry) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto grid
|
||||
&--auto &__auto {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: var(--grid-gap);
|
||||
|
||||
@include breakpoint('tablet') {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
@include breakpoint('mobile') {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
&__item {
|
||||
break-inside: avoid;
|
||||
margin-bottom: 0; // Override PhotoItem default
|
||||
}
|
||||
}
|
||||
|
||||
.photo-caption {
|
||||
margin-top: $unit;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-40;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
// Responsive adjustments
|
||||
@include breakpoint('mobile') {
|
||||
.photo-grid {
|
||||
&--2-column &__column {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -4,11 +4,9 @@
|
|||
import { goto } from '$app/navigation'
|
||||
|
||||
const {
|
||||
item,
|
||||
albumSlug // For when this is used within an album context
|
||||
item
|
||||
}: {
|
||||
item: PhotoItem
|
||||
albumSlug?: string
|
||||
} = $props()
|
||||
|
||||
let imageLoaded = $state(false)
|
||||
|
|
@ -16,18 +14,11 @@
|
|||
function handleClick() {
|
||||
if (isAlbum(item)) {
|
||||
// Navigate to album page using the slug
|
||||
goto(`/photos/${item.slug}`)
|
||||
goto(`/albums/${item.slug}`)
|
||||
} else {
|
||||
// For individual photos, check if we have album context
|
||||
if (albumSlug) {
|
||||
// Navigate to photo within album
|
||||
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
||||
goto(`/photos/${albumSlug}/${mediaId}`)
|
||||
} else {
|
||||
// Navigate to individual photo page using the media ID
|
||||
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
||||
goto(`/photos/p/${mediaId}`)
|
||||
}
|
||||
// Navigate to individual photo page using the media ID
|
||||
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
||||
goto(`/photos/${mediaId}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,16 +28,8 @@
|
|||
|
||||
const photo = $derived(isAlbum(item) ? item.coverPhoto : item)
|
||||
const isAlbumItem = $derived(isAlbum(item))
|
||||
const placeholderStyle = $derived(
|
||||
photo.dominantColor
|
||||
? `background: ${photo.dominantColor}`
|
||||
: ''
|
||||
)
|
||||
const aspectRatioStyle = $derived(
|
||||
photo.aspectRatio
|
||||
? `aspect-ratio: ${photo.aspectRatio}`
|
||||
: ''
|
||||
)
|
||||
const placeholderStyle = $derived(photo.dominantColor ? `background: ${photo.dominantColor}` : '')
|
||||
const aspectRatioStyle = $derived(photo.aspectRatio ? `aspect-ratio: ${photo.aspectRatio}` : '')
|
||||
</script>
|
||||
|
||||
<div class="photo-item" class:is-album={isAlbumItem}>
|
||||
|
|
@ -116,7 +99,7 @@
|
|||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 8px 25px $shadow-medium;
|
||||
}
|
||||
|
||||
&:active {
|
||||
|
|
@ -136,7 +119,7 @@
|
|||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: $z-index-above;
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
|
|
@ -158,8 +141,8 @@
|
|||
left: 6px;
|
||||
right: -6px;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
z-index: 1;
|
||||
background: $shadow-light;
|
||||
z-index: $z-index-base;
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
|
|
@ -169,14 +152,14 @@
|
|||
left: 3px;
|
||||
right: -3px;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
z-index: 2;
|
||||
background: $shadow-dark;
|
||||
z-index: $z-index-above;
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
|
||||
&.stack-front {
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
z-index: $z-index-hover;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
|
|
@ -186,7 +169,7 @@
|
|||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: $z-index-above;
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
|
|
@ -203,7 +186,7 @@
|
|||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
||||
color: white;
|
||||
padding: $unit-2x;
|
||||
z-index: 4;
|
||||
z-index: calc($z-index-hover + 1);
|
||||
border-radius: 0 0 $corner-radius $corner-radius;
|
||||
}
|
||||
|
||||
|
|
@ -246,14 +229,12 @@
|
|||
border-radius: $corner-radius;
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s ease;
|
||||
z-index: 1;
|
||||
z-index: $z-index-base;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
&.loaded {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,376 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
const {
|
||||
photo,
|
||||
albumPhotos = [],
|
||||
currentIndex = 0,
|
||||
onClose,
|
||||
onNavigate
|
||||
}: {
|
||||
photo: Photo
|
||||
albumPhotos?: Photo[]
|
||||
currentIndex?: number
|
||||
onClose: () => void
|
||||
onNavigate: (direction: 'prev' | 'next') => void
|
||||
} = $props()
|
||||
|
||||
let imageLoaded = $state(false)
|
||||
let currentPhotoId = $state(photo.id)
|
||||
|
||||
const hasNavigation = $derived(albumPhotos.length > 1)
|
||||
const hasExifData = $derived(photo.exif && Object.keys(photo.exif).length > 0)
|
||||
|
||||
// Reset loading state when photo changes
|
||||
$effect(() => {
|
||||
if (photo.id !== currentPhotoId) {
|
||||
imageLoaded = false
|
||||
currentPhotoId = photo.id
|
||||
}
|
||||
})
|
||||
|
||||
function handleImageLoad() {
|
||||
imageLoaded = true
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
switch (event.key) {
|
||||
case 'Escape':
|
||||
onClose()
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (hasNavigation) onNavigate('prev')
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (hasNavigation) onNavigate('next')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackdropClick(event: MouseEvent) {
|
||||
if (event.target === event.currentTarget) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
|
||||
function formatExifValue(key: string, value: string): string {
|
||||
switch (key) {
|
||||
case 'dateTaken':
|
||||
return new Date(value).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
function getExifLabel(key: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
camera: 'Camera',
|
||||
lens: 'Lens',
|
||||
focalLength: 'Focal Length',
|
||||
aperture: 'Aperture',
|
||||
shutterSpeed: 'Shutter Speed',
|
||||
iso: 'ISO',
|
||||
dateTaken: 'Date Taken',
|
||||
location: 'Location'
|
||||
}
|
||||
return labels[key] || key
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="lightbox-backdrop" onclick={handleBackdropClick}>
|
||||
<div class="lightbox-container">
|
||||
<!-- Close button -->
|
||||
<button class="close-button" onclick={onClose} type="button">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M6 6l12 12M18 6l-12 12"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Navigation buttons -->
|
||||
{#if hasNavigation}
|
||||
<button class="nav-button nav-prev" onclick={() => onNavigate('prev')} type="button">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M15 18l-6-6 6-6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="nav-button nav-next" onclick={() => onNavigate('next')} type="button">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path
|
||||
d="M9 18l6-6-6-6"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<!-- Photo -->
|
||||
<div class="photo-container">
|
||||
<img src={photo.src} alt={photo.alt} onload={handleImageLoad} class:loaded={imageLoaded} />
|
||||
{#if !imageLoaded}
|
||||
<div class="loading-indicator">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Photo info -->
|
||||
<div class="photo-info" class:loaded={imageLoaded}>
|
||||
{#if photo.caption}
|
||||
<h3 class="photo-caption">{photo.caption}</h3>
|
||||
{/if}
|
||||
|
||||
{#if hasExifData}
|
||||
<div class="exif-data">
|
||||
<h4>Camera Settings</h4>
|
||||
<dl class="exif-list">
|
||||
{#each Object.entries(photo.exif) as [key, value]}
|
||||
<div class="exif-item">
|
||||
<dt>{getExifLabel(key)}</dt>
|
||||
<dd>{formatExifValue(key, value)}</dd>
|
||||
</div>
|
||||
{/each}
|
||||
</dl>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if hasNavigation}
|
||||
<div class="navigation-info">
|
||||
{currentIndex + 1} of {albumPhotos.length}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.lightbox-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1400;
|
||||
padding: $unit-2x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
padding: $unit;
|
||||
}
|
||||
}
|
||||
|
||||
.lightbox-container {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
gap: $unit-3x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
flex-direction: column;
|
||||
max-width: 95vw;
|
||||
max-height: 95vh;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: -$unit-6x;
|
||||
right: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: $unit;
|
||||
border-radius: $corner-radius;
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
top: -$unit-4x;
|
||||
right: -$unit;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-button {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: none;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
padding: $unit-2x;
|
||||
border-radius: $corner-radius;
|
||||
transition: background-color 0.2s ease;
|
||||
z-index: 10;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
&.nav-prev {
|
||||
left: -$unit-6x;
|
||||
}
|
||||
|
||||
&.nav-next {
|
||||
right: -$unit-6x;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
&.nav-prev {
|
||||
left: $unit;
|
||||
}
|
||||
|
||||
&.nav-next {
|
||||
right: $unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.photo-container {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
max-width: 70vw;
|
||||
max-height: 80vh;
|
||||
height: auto;
|
||||
border-radius: $corner-radius;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
max-width: 95vw;
|
||||
max-height: 60vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: white;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.photo-info {
|
||||
width: 300px;
|
||||
color: white;
|
||||
overflow-y: auto;
|
||||
padding: $unit-2x;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: $corner-radius;
|
||||
backdrop-filter: blur(10px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease 0.1s; // Slight delay to sync with image
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
width: 100%;
|
||||
max-height: 30vh;
|
||||
}
|
||||
}
|
||||
|
||||
.photo-caption {
|
||||
margin: 0 0 $unit-3x 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.exif-data {
|
||||
margin-bottom: $unit-3x;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 $unit-2x 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.exif-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.exif-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
|
||||
dt {
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
margin-right: $unit-2x;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
font-family: 'Monaco', 'Menlo', monospace;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-info {
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -10,6 +10,7 @@
|
|||
backHref?: string
|
||||
backLabel?: string
|
||||
showBackButton?: boolean
|
||||
albums?: Array<{ id: number; title: string; slug: string }>
|
||||
class?: string
|
||||
}
|
||||
|
||||
|
|
@ -22,6 +23,7 @@
|
|||
backHref,
|
||||
backLabel,
|
||||
showBackButton = false,
|
||||
albums = [],
|
||||
class: className = ''
|
||||
}: Props = $props()
|
||||
|
||||
|
|
@ -116,6 +118,19 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if albums && albums.length > 0}
|
||||
<div class="albums-section">
|
||||
<h3 class="albums-title">This photo appears in:</h3>
|
||||
<div class="albums-list">
|
||||
{#each albums as album}
|
||||
<a href="/photos/{album.slug}" class="album-link">
|
||||
{album.title}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showBackButton && backHref && backLabel}
|
||||
<div class="card-footer">
|
||||
<BackButton href={backHref} label={backLabel} />
|
||||
|
|
@ -128,8 +143,8 @@
|
|||
@import '$styles/mixins.scss';
|
||||
|
||||
.photo-metadata {
|
||||
background: $grey-100;
|
||||
border: 1px solid $grey-90;
|
||||
background: $gray-100;
|
||||
border: 1px solid $gray-90;
|
||||
border-radius: $image-corner-radius;
|
||||
padding: $unit-3x;
|
||||
padding-bottom: $unit-2x;
|
||||
|
|
@ -147,7 +162,7 @@
|
|||
.photo-details {
|
||||
margin-bottom: $unit-4x;
|
||||
padding-bottom: $unit-4x;
|
||||
border-bottom: 1px solid $grey-90;
|
||||
border-bottom: 1px solid $gray-90;
|
||||
text-align: center;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
|
|
@ -159,7 +174,7 @@
|
|||
font-size: 1.75rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 $unit-2x;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 1.25rem;
|
||||
|
|
@ -169,7 +184,7 @@
|
|||
|
||||
.photo-description {
|
||||
font-size: 1rem;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
|
||||
|
|
@ -208,17 +223,61 @@
|
|||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
.metadata-value {
|
||||
font-size: 0.875rem;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.albums-section {
|
||||
margin-bottom: $unit-4x;
|
||||
padding-bottom: $unit-4x;
|
||||
border-bottom: 1px solid $gray-90;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
margin-bottom: $unit-3x;
|
||||
padding-bottom: $unit-3x;
|
||||
}
|
||||
|
||||
.albums-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: $gray-40;
|
||||
margin: 0 0 $unit-2x;
|
||||
}
|
||||
|
||||
.albums-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: $unit $unit-2x;
|
||||
}
|
||||
|
||||
.album-link {
|
||||
font-size: 0.875rem;
|
||||
color: $red-60;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $red-50;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: ',';
|
||||
color: $gray-40;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -13,15 +13,7 @@
|
|||
height?: number
|
||||
}
|
||||
|
||||
let {
|
||||
src,
|
||||
alt = '',
|
||||
title,
|
||||
id,
|
||||
class: className = '',
|
||||
width,
|
||||
height
|
||||
}: Props = $props()
|
||||
let { src, alt = '', title, id, class: className = '', width, height }: Props = $props()
|
||||
|
||||
let imageRef = $state<HTMLImageElement>()
|
||||
let isUltrawide = $state(false)
|
||||
|
|
@ -31,14 +23,19 @@
|
|||
function checkIfUltrawide() {
|
||||
if (width && height) {
|
||||
isUltrawide = width / height > 2
|
||||
console.log('Ultrawide check from props:', { width, height, ratio: width / height, isUltrawide })
|
||||
console.log('Ultrawide check from props:', {
|
||||
width,
|
||||
height,
|
||||
ratio: width / height,
|
||||
isUltrawide
|
||||
})
|
||||
} else if (imageRef && imageLoaded) {
|
||||
isUltrawide = imageRef.naturalWidth / imageRef.naturalHeight > 2
|
||||
console.log('Ultrawide check from image:', {
|
||||
naturalWidth: imageRef.naturalWidth,
|
||||
naturalHeight: imageRef.naturalHeight,
|
||||
ratio: imageRef.naturalWidth / imageRef.naturalHeight,
|
||||
isUltrawide
|
||||
console.log('Ultrawide check from image:', {
|
||||
naturalWidth: imageRef.naturalWidth,
|
||||
naturalHeight: imageRef.naturalHeight,
|
||||
ratio: imageRef.naturalWidth / imageRef.naturalHeight,
|
||||
isUltrawide
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -64,7 +61,7 @@
|
|||
// Enhance zoom behavior for ultrawide images
|
||||
function enhanceZoomForUltrawide() {
|
||||
if (!isUltrawide) return
|
||||
|
||||
|
||||
console.log('Setting up ultrawide zoom enhancement')
|
||||
|
||||
// Wait for zoom to be activated
|
||||
|
|
@ -72,53 +69,61 @@
|
|||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'childList') {
|
||||
// Check for zoom overlay - try multiple selectors
|
||||
const zoomOverlay = document.querySelector('[data-smiz-overlay]') ||
|
||||
document.querySelector('.medium-image-zoom-overlay') ||
|
||||
document.querySelector('[data-rmiz-modal-overlay]')
|
||||
const zoomedImage = document.querySelector('[data-smiz-modal] img') ||
|
||||
document.querySelector('.medium-image-zoom-image') ||
|
||||
document.querySelector('[data-rmiz-modal-img]') as HTMLImageElement
|
||||
|
||||
console.log('Checking for zoom elements:', {
|
||||
zoomOverlay: !!zoomOverlay,
|
||||
const zoomOverlay =
|
||||
document.querySelector('[data-smiz-overlay]') ||
|
||||
document.querySelector('.medium-image-zoom-overlay') ||
|
||||
document.querySelector('[data-rmiz-modal-overlay]')
|
||||
const zoomedImage =
|
||||
document.querySelector('[data-smiz-modal] img') ||
|
||||
document.querySelector('.medium-image-zoom-image') ||
|
||||
(document.querySelector('[data-rmiz-modal-img]') as HTMLImageElement)
|
||||
|
||||
console.log('Checking for zoom elements:', {
|
||||
zoomOverlay: !!zoomOverlay,
|
||||
zoomedImage: !!zoomedImage,
|
||||
allDivs: document.querySelectorAll('div').length,
|
||||
bodyChildren: document.body.children.length
|
||||
})
|
||||
|
||||
|
||||
// Also check for any new elements with specific classes
|
||||
const allNewElements = mutation.addedNodes
|
||||
allNewElements.forEach(node => {
|
||||
if (node.nodeType === 1) { // Element node
|
||||
allNewElements.forEach((node) => {
|
||||
if (node.nodeType === 1) {
|
||||
// Element node
|
||||
const element = node as HTMLElement
|
||||
console.log('New element added:', element.tagName, element.className, element.getAttribute('data-rmiz-modal-overlay'))
|
||||
console.log(
|
||||
'New element added:',
|
||||
element.tagName,
|
||||
element.className,
|
||||
element.getAttribute('data-rmiz-modal-overlay')
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (zoomOverlay && zoomedImage) {
|
||||
console.log('Zoom activated, applying ultrawide enhancements')
|
||||
// Add custom class for ultrawide handling
|
||||
zoomOverlay.classList.add('ultrawide-zoom')
|
||||
|
||||
|
||||
// Make the zoomed image scrollable horizontally
|
||||
const modal = zoomedImage.closest('[data-smiz-modal]') as HTMLElement
|
||||
if (modal) {
|
||||
modal.style.overflow = 'auto'
|
||||
modal.style.maxHeight = '90vh'
|
||||
|
||||
|
||||
// Adjust image height to fill more vertical space for ultrawide
|
||||
zoomedImage.style.maxHeight = '85vh'
|
||||
zoomedImage.style.height = 'auto'
|
||||
zoomedImage.style.width = 'auto'
|
||||
zoomedImage.style.maxWidth = 'none'
|
||||
|
||||
|
||||
// Center the scroll position initially
|
||||
setTimeout(() => {
|
||||
const scrollLeft = (modal.scrollWidth - modal.clientWidth) / 2
|
||||
modal.scrollLeft = scrollLeft
|
||||
updateScrollIndicators(modal)
|
||||
}, 50)
|
||||
|
||||
|
||||
// Add scroll listener to update indicators
|
||||
modal.addEventListener('scroll', () => updateScrollIndicators(modal))
|
||||
}
|
||||
|
|
@ -156,10 +161,10 @@
|
|||
<div class="photo-view {className}" class:ultrawide={isUltrawide}>
|
||||
{#key id || src}
|
||||
<Zoom>
|
||||
<img
|
||||
<img
|
||||
bind:this={imageRef}
|
||||
{src}
|
||||
alt={title || alt || 'Photo'}
|
||||
{src}
|
||||
alt={title || alt || 'Photo'}
|
||||
class="photo-image"
|
||||
onload={handleImageLoad}
|
||||
/>
|
||||
|
|
@ -203,7 +208,7 @@
|
|||
:global(.ultrawide-zoom) {
|
||||
:global([data-smiz-modal]) {
|
||||
cursor: grab;
|
||||
|
||||
|
||||
&:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
|
@ -219,7 +224,7 @@
|
|||
height: 100px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
transition: opacity 0.3s ease;
|
||||
transition: opacity $transition-medium ease;
|
||||
}
|
||||
|
||||
&::before {
|
||||
|
|
@ -267,4 +272,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { ComponentType } from 'svelte'
|
||||
|
||||
let {
|
||||
icon,
|
||||
text,
|
||||
href,
|
||||
active = false,
|
||||
variant = 'default'
|
||||
}: {
|
||||
icon: ComponentType
|
||||
text: string
|
||||
href?: string
|
||||
active?: boolean
|
||||
variant?: 'work' | 'universe' | 'default'
|
||||
} = $props()
|
||||
</script>
|
||||
|
||||
<a {href} class="pill {variant}" class:active>
|
||||
<icon />
|
||||
<span>{text}</span>
|
||||
</a>
|
||||
|
||||
<style lang="scss">
|
||||
.pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 100px;
|
||||
text-decoration: none;
|
||||
color: $grey-20; // #666
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
:global(svg) {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
// Work variant
|
||||
&.work {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: $work-bg;
|
||||
color: $work-color;
|
||||
|
||||
:global(svg) {
|
||||
fill: $work-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Universe variant
|
||||
&.universe {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: $universe-bg;
|
||||
color: $universe-color;
|
||||
|
||||
:global(svg) {
|
||||
fill: $universe-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Default variant (Labs)
|
||||
&.default {
|
||||
&:hover,
|
||||
&.active {
|
||||
background: $labs-bg;
|
||||
color: $labs-color;
|
||||
|
||||
:global(svg) {
|
||||
fill: $labs-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
margin: 0 0 $unit-2x;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
|
|
@ -155,13 +155,13 @@
|
|||
:global(blockquote) {
|
||||
margin: $unit-3x 0;
|
||||
padding-left: $unit-3x;
|
||||
border-left: 3px solid $grey-80;
|
||||
border-left: 3px solid $gray-80;
|
||||
color: $text-color;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(code) {
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'SF Mono', Monaco, monospace;
|
||||
|
|
@ -169,7 +169,7 @@
|
|||
}
|
||||
|
||||
:global(pre) {
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
padding: $unit-2x;
|
||||
border-radius: $unit;
|
||||
overflow-x: auto;
|
||||
|
|
@ -193,7 +193,7 @@
|
|||
|
||||
:global(hr) {
|
||||
border: none;
|
||||
border-top: 1px solid $grey-80;
|
||||
border-top: 1px solid $gray-80;
|
||||
margin: $unit-4x 0;
|
||||
}
|
||||
|
||||
|
|
@ -206,6 +206,6 @@
|
|||
.post-footer {
|
||||
margin-top: $unit-6x;
|
||||
padding-top: $unit-4x;
|
||||
border-top: 1px solid $grey-80;
|
||||
border-top: 1px solid $gray-80;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@
|
|||
|
||||
.post-content {
|
||||
padding: $unit-3x;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
border-radius: $card-corner-radius;
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
|
||||
.post-excerpt {
|
||||
margin: 0;
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
|
|
@ -132,7 +132,7 @@
|
|||
.post-date {
|
||||
display: block;
|
||||
font-size: 1rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
font-weight: 400;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
|
@ -146,6 +146,6 @@
|
|||
:global(.universe-icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: $grey-40;
|
||||
fill: $gray-40;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
letter-spacing: -0.025em;
|
||||
font-weight: 500;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
font-size: 2rem;
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
.project-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
margin: 0;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@
|
|||
align-items: center;
|
||||
gap: $unit-3x;
|
||||
padding: $unit-3x;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
border-radius: $card-corner-radius;
|
||||
transition:
|
||||
transform 0.15s ease-out,
|
||||
|
|
@ -232,7 +232,7 @@
|
|||
|
||||
&.list-only {
|
||||
opacity: 0.7;
|
||||
background: $grey-97;
|
||||
background: $gray-97;
|
||||
}
|
||||
|
||||
&.password-protected {
|
||||
|
|
@ -287,7 +287,7 @@
|
|||
margin: 0;
|
||||
font-size: 1rem; // 18px
|
||||
line-height: 1.3;
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
|
|
@ -299,7 +299,7 @@
|
|||
font-weight: 500;
|
||||
|
||||
&.list-only {
|
||||
color: $grey-60;
|
||||
color: $gray-60;
|
||||
}
|
||||
|
||||
&.password-protected {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
name={project.title}
|
||||
slug={project.slug}
|
||||
description={project.description || ''}
|
||||
highlightColor={project.highlightColor || '#333'}
|
||||
highlightColor={project.highlightColor || '$gray-00'}
|
||||
status={project.status}
|
||||
{index}
|
||||
/>
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
|
||||
.intro-card {
|
||||
padding: $unit-3x;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
border-radius: $card-corner-radius;
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
margin: 0;
|
||||
font-size: 1rem; // 18px
|
||||
line-height: 1.3;
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $unit-2x;
|
||||
|
|
@ -110,6 +110,6 @@
|
|||
.no-projects {
|
||||
padding: $unit-3x;
|
||||
text-align: center;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@
|
|||
width: 100%;
|
||||
|
||||
.lock-icon {
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
margin-bottom: $unit-3x;
|
||||
|
||||
svg {
|
||||
|
|
@ -152,7 +152,7 @@
|
|||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
margin: 0 0 $unit-2x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
}
|
||||
|
||||
p {
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
font-size: 1rem;
|
||||
|
|
@ -195,7 +195,7 @@
|
|||
.password-input {
|
||||
flex: 1;
|
||||
padding: $unit-2x;
|
||||
border: 1px solid $grey-80;
|
||||
border: 1px solid $gray-80;
|
||||
border-radius: $unit;
|
||||
font-size: 1rem;
|
||||
transition:
|
||||
|
|
@ -214,8 +214,8 @@
|
|||
}
|
||||
|
||||
&:disabled {
|
||||
background: $grey-95;
|
||||
color: $grey-60;
|
||||
background: $gray-95;
|
||||
color: $gray-60;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
|
@ -232,7 +232,7 @@
|
|||
}
|
||||
|
||||
.back-link-wrapper {
|
||||
border-top: 1px solid $grey-90;
|
||||
border-top: 1px solid $gray-90;
|
||||
padding-top: $unit-3x;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { spring } from 'svelte/motion'
|
||||
|
||||
const {
|
||||
SVGComponent,
|
||||
backgroundColor = '#f0f0f0',
|
||||
maxMovement = 20,
|
||||
containerHeight = '300px',
|
||||
stiffness = 0.15,
|
||||
damping = 0.8
|
||||
} = $props()
|
||||
|
||||
let container = $state(null)
|
||||
let svg = $state(null)
|
||||
|
||||
const position = spring(
|
||||
{ x: 0, y: 0 },
|
||||
{
|
||||
stiffness,
|
||||
damping
|
||||
}
|
||||
)
|
||||
|
||||
$effect(() => {
|
||||
if (svg) {
|
||||
const { x, y } = $position
|
||||
svg.style.transform = `translate(calc(-50% + ${x}px), calc(-50% + ${y}px))`
|
||||
}
|
||||
})
|
||||
|
||||
function handleMouseMove(event) {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const x = event.clientX - rect.left
|
||||
const y = event.clientY - rect.top
|
||||
|
||||
position.set({
|
||||
x: (x / rect.width - 0.5) * 2 * maxMovement,
|
||||
y: (y / rect.height - 0.5) * 2 * maxMovement
|
||||
})
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
position.set({ x: 0, y: 0 })
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
svg = container.querySelector('svg')
|
||||
if (svg) {
|
||||
svg.style.position = 'absolute'
|
||||
svg.style.left = '50%'
|
||||
svg.style.top = '50%'
|
||||
svg.style.transform = 'translate(-50%, -50%)'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={container}
|
||||
on:mousemove={handleMouseMove}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
style="position: relative; overflow: hidden; background-color: {backgroundColor}; height: {containerHeight}; display: flex; justify-content: center; align-items: center;"
|
||||
>
|
||||
<div style="position: relative; width: 100%; height: 100%;">
|
||||
<SVGComponent />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
border-radius: $corner-radius;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
class="nav-item"
|
||||
class:active={index === activeIndex}
|
||||
bind:this={itemElements[index]}
|
||||
style="color: {index === activeIndex ? getTextColor(item.variant) : '#666'};"
|
||||
style="color: {index === activeIndex ? getTextColor(item.variant) : '$text-color-subdued'};"
|
||||
onmouseenter={() => (hoveredIndex = index)}
|
||||
onmouseleave={() => (hoveredIndex = null)}
|
||||
>
|
||||
|
|
@ -132,10 +132,10 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
padding: $unit;
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 1px 3px $shadow-light;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -146,8 +146,8 @@
|
|||
left: $unit;
|
||||
height: calc(100% - #{$unit * 2});
|
||||
border-radius: 100px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1;
|
||||
transition: all $transition-medium cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: $z-index-base;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
|
|
@ -160,13 +160,13 @@
|
|||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
z-index: $z-index-above;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
background-color: $border-light;
|
||||
}
|
||||
|
||||
:global(svg.nav-icon) {
|
||||
|
|
|
|||
|
|
@ -1,60 +1,12 @@
|
|||
<script lang="ts">
|
||||
import PhotoItem from './PhotoItem.svelte'
|
||||
import type { PhotoItem as PhotoItemType } from '$lib/types/photos'
|
||||
import { isAlbum } from '$lib/types/photos'
|
||||
import PhotoGrid from './PhotoGrid.svelte'
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
const {
|
||||
photoItems,
|
||||
albumSlug
|
||||
}: {
|
||||
photoItems: PhotoItemType[]
|
||||
albumSlug?: string
|
||||
} = $props()
|
||||
interface Props {
|
||||
photos: Photo[]
|
||||
}
|
||||
|
||||
let { photos = [] }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="single-column-grid">
|
||||
{#each photoItems as item}
|
||||
<div class="photo-container">
|
||||
<PhotoItem {item} {albumSlug} />
|
||||
{#if !isAlbum(item) && item.caption}
|
||||
<div class="photo-details">
|
||||
<p class="photo-caption">{item.caption}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.single-column-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-4x;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.photo-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.photo-details {
|
||||
padding: $unit-2x 0 0;
|
||||
}
|
||||
|
||||
.photo-caption {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
color: $grey-20;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
.single-column-grid {
|
||||
gap: $unit-3x;
|
||||
}
|
||||
|
||||
.photo-details {
|
||||
padding: $unit 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<PhotoGrid {photos} columns={1} gap="large" showCaptions={true} />
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@
|
|||
inset: 0;
|
||||
border-radius: $image-corner-radius;
|
||||
border: 4px solid transparent;
|
||||
z-index: 2;
|
||||
z-index: $z-index-above;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
|
@ -259,7 +259,7 @@
|
|||
inset: 4px;
|
||||
border-radius: calc($image-corner-radius - 4px);
|
||||
border: 4px solid transparent;
|
||||
z-index: 3;
|
||||
z-index: $z-index-hover;
|
||||
pointer-events: none;
|
||||
transition: border-color 0.2s ease;
|
||||
}
|
||||
|
|
@ -276,7 +276,7 @@
|
|||
}
|
||||
|
||||
&::after {
|
||||
border-color: $grey-100;
|
||||
border-color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,12 +286,12 @@
|
|||
}
|
||||
|
||||
&::after {
|
||||
border-color: $grey-100;
|
||||
border-color: $gray-100;
|
||||
}
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
cursor: default;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -320,7 +320,7 @@
|
|||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
border-radius: $image-corner-radius;
|
||||
z-index: 2;
|
||||
z-index: $z-index-above;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
|
@ -347,7 +347,7 @@
|
|||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: $z-index-base;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,83 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let text = 'Hello, Squiggly World!'
|
||||
export let frequency = 0.4
|
||||
export let amplitude = 1.5
|
||||
export let color = '#e33d3d'
|
||||
export let distance = 3
|
||||
export let lineWidth = 1.75
|
||||
|
||||
let textWidth = 0
|
||||
let textElement: HTMLHeadingElement
|
||||
let squigglyHeight: number
|
||||
|
||||
$: path = generatePath(textWidth, frequency, amplitude, distance)
|
||||
$: squigglyHeight = distance + amplitude * 2 + lineWidth
|
||||
|
||||
onMount(() => {
|
||||
updateTextWidth()
|
||||
})
|
||||
|
||||
function updateTextWidth(): void {
|
||||
textWidth = textElement?.getBoundingClientRect().width || 0
|
||||
}
|
||||
|
||||
function generatePath(width: number, freq: number, amp: number, dist: number): string {
|
||||
if (width === 0) return ''
|
||||
const startX = 2
|
||||
const endX = width - 2
|
||||
const startY = amp * Math.sin(startX * freq) + dist
|
||||
|
||||
let pathData = `M${startX},${startY} `
|
||||
|
||||
for (let x = startX; x <= endX; x++) {
|
||||
const y = amp * Math.sin(x * freq) + dist
|
||||
pathData += `L${x},${y} `
|
||||
}
|
||||
return pathData
|
||||
}
|
||||
|
||||
$: {
|
||||
text
|
||||
updateTextWidth()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="squiggly-container" style="padding-bottom: {squigglyHeight}px;">
|
||||
<h2 bind:this={textElement} class="squiggly-header" style="color: {color}">{text}</h2>
|
||||
<svg
|
||||
class="squiggly-underline"
|
||||
width={textWidth}
|
||||
height={squigglyHeight}
|
||||
viewBox="0 0 {textWidth} {squigglyHeight}"
|
||||
>
|
||||
<path
|
||||
d={path}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
stroke-width={lineWidth}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.squiggly-header {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: $unit-fourth;
|
||||
}
|
||||
|
||||
.squiggly-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.squiggly-underline {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
color: white;
|
||||
border-radius: $unit * 2;
|
||||
font-size: $font-size-small;
|
||||
z-index: 1000;
|
||||
z-index: $z-index-overlay;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
|
||||
&.connected {
|
||||
|
|
|
|||
|
|
@ -1,290 +1,12 @@
|
|||
<script lang="ts">
|
||||
import type { PhotoItem as PhotoItemType } from '$lib/types/photos'
|
||||
import { isAlbum } from '$lib/types/photos'
|
||||
import { goto } from '$app/navigation'
|
||||
import PhotoGrid from './PhotoGrid.svelte'
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
const {
|
||||
photoItems,
|
||||
albumSlug
|
||||
}: {
|
||||
photoItems: PhotoItemType[]
|
||||
albumSlug?: string
|
||||
} = $props()
|
||||
|
||||
// Function to determine if an image is ultrawide (aspect ratio > 2:1)
|
||||
function isUltrawide(item: PhotoItemType): boolean {
|
||||
if (isAlbum(item)) {
|
||||
const { width, height } = item.coverPhoto
|
||||
return width / height > 2
|
||||
} else {
|
||||
return item.width / item.height > 2
|
||||
}
|
||||
interface Props {
|
||||
photos: Photo[]
|
||||
}
|
||||
|
||||
// Process items to determine grid placement
|
||||
let gridItems = $state<Array<{ item: PhotoItemType; spanFull: boolean }>>([])
|
||||
|
||||
$effect(() => {
|
||||
// First, separate ultrawide and regular items
|
||||
const ultrawideItems: PhotoItemType[] = []
|
||||
const regularItems: PhotoItemType[] = []
|
||||
|
||||
photoItems.forEach((item) => {
|
||||
if (isUltrawide(item)) {
|
||||
ultrawideItems.push(item)
|
||||
} else {
|
||||
regularItems.push(item)
|
||||
}
|
||||
})
|
||||
|
||||
// Build the grid ensuring we fill rows of 3
|
||||
const processedItems: Array<{ item: PhotoItemType; spanFull: boolean }> = []
|
||||
let regularIndex = 0
|
||||
let ultrawideIndex = 0
|
||||
let rowsSinceLastUltrawide = 1 // Start with 1 to allow ultrawide at beginning
|
||||
|
||||
while (regularIndex < regularItems.length || ultrawideIndex < ultrawideItems.length) {
|
||||
const remainingRegular = regularItems.length - regularIndex
|
||||
const remainingUltrawide = ultrawideItems.length - ultrawideIndex
|
||||
|
||||
// Check if we can/should place an ultrawide
|
||||
if (
|
||||
ultrawideIndex < ultrawideItems.length &&
|
||||
rowsSinceLastUltrawide >= 1 &&
|
||||
(remainingRegular === 0 || remainingRegular >= 3)
|
||||
) {
|
||||
// Place ultrawide
|
||||
processedItems.push({
|
||||
item: ultrawideItems[ultrawideIndex],
|
||||
spanFull: true
|
||||
})
|
||||
ultrawideIndex++
|
||||
rowsSinceLastUltrawide = 0
|
||||
} else if (regularIndex < regularItems.length && remainingRegular >= 3) {
|
||||
// Place a full row of 3 regular photos
|
||||
for (let i = 0; i < 3 && regularIndex < regularItems.length; i++) {
|
||||
processedItems.push({
|
||||
item: regularItems[regularIndex],
|
||||
spanFull: false
|
||||
})
|
||||
regularIndex++
|
||||
}
|
||||
rowsSinceLastUltrawide++
|
||||
} else if (regularIndex < regularItems.length) {
|
||||
// Place remaining regular photos (less than 3)
|
||||
while (regularIndex < regularItems.length) {
|
||||
processedItems.push({
|
||||
item: regularItems[regularIndex],
|
||||
spanFull: false
|
||||
})
|
||||
regularIndex++
|
||||
}
|
||||
rowsSinceLastUltrawide++
|
||||
} else {
|
||||
// Only ultrawides left, place them with spacing
|
||||
if (ultrawideIndex < ultrawideItems.length) {
|
||||
processedItems.push({
|
||||
item: ultrawideItems[ultrawideIndex],
|
||||
spanFull: true
|
||||
})
|
||||
ultrawideIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gridItems = processedItems
|
||||
})
|
||||
|
||||
function handleClick(item: PhotoItemType) {
|
||||
if (isAlbum(item)) {
|
||||
// Navigate to album page using the slug
|
||||
goto(`/photos/${item.slug}`)
|
||||
} else {
|
||||
// For individual photos, check if we have album context
|
||||
if (albumSlug) {
|
||||
// Navigate to photo within album
|
||||
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
||||
goto(`/photos/${albumSlug}/${mediaId}`)
|
||||
} else {
|
||||
// Navigate to individual photo page using the media ID
|
||||
const mediaId = item.id.replace(/^(media|photo)-/, '') // Support both prefixes
|
||||
goto(`/photos/p/${mediaId}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getImageSrc(item: PhotoItemType): string {
|
||||
return isAlbum(item) ? item.coverPhoto.src : item.src
|
||||
}
|
||||
|
||||
function getImageAlt(item: PhotoItemType): string {
|
||||
return isAlbum(item) ? item.coverPhoto.alt : item.alt
|
||||
}
|
||||
let { photos = [] }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="three-column-grid">
|
||||
{#each gridItems as { item, spanFull }}
|
||||
<button
|
||||
class="grid-item"
|
||||
class:span-full={spanFull}
|
||||
class:is-album={isAlbum(item)}
|
||||
onclick={() => handleClick(item)}
|
||||
type="button"
|
||||
>
|
||||
<div class="image-container">
|
||||
<img src={getImageSrc(item)} alt={getImageAlt(item)} loading="lazy" draggable="false" />
|
||||
</div>
|
||||
{#if isAlbum(item)}
|
||||
<div class="album-overlay">
|
||||
<div class="album-info">
|
||||
<span class="album-title">{item.title}</span>
|
||||
<span class="album-count">{item.photos.length} photos</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.three-column-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: $unit-2x;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
grid-column: span 1;
|
||||
position: relative;
|
||||
aspect-ratio: 1; // Square by default
|
||||
overflow: hidden;
|
||||
border-radius: $corner-radius;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&.span-full {
|
||||
grid-column: span 3;
|
||||
aspect-ratio: 3; // Wider aspect ratio for ultrawide images
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.album-overlay {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.8), transparent);
|
||||
color: white;
|
||||
padding: $unit-2x;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.album-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
.album-title {
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.album-count {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
// Stack effect for albums
|
||||
.is-album {
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: $corner-radius;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -3px;
|
||||
left: 3px;
|
||||
right: -3px;
|
||||
bottom: 3px;
|
||||
transform: rotate(-1deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: -6px;
|
||||
left: 6px;
|
||||
right: -6px;
|
||||
bottom: 6px;
|
||||
transform: rotate(2deg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
transform: rotate(-1.5deg) translateY(-0.5px);
|
||||
}
|
||||
|
||||
&::after {
|
||||
transform: rotate(3deg) translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint('tablet') {
|
||||
.three-column-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.grid-item.span-full {
|
||||
grid-column: span 2;
|
||||
aspect-ratio: 2; // Adjust aspect ratio for 2-column layout
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
.three-column-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
aspect-ratio: 4/3; // Slightly wider on mobile
|
||||
}
|
||||
|
||||
.grid-item.span-full {
|
||||
grid-column: span 1;
|
||||
aspect-ratio: 16/9; // Standard widescreen on mobile
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<PhotoGrid {photos} columns={3} gap="medium" />
|
||||
|
|
|
|||
|
|
@ -1,51 +1,12 @@
|
|||
<script lang="ts">
|
||||
import PhotoItem from '$components/PhotoItem.svelte'
|
||||
import type { PhotoItem as PhotoItemType } from '$lib/types/photos'
|
||||
import PhotoGrid from './PhotoGrid.svelte'
|
||||
import type { Photo } from '$lib/types/photos'
|
||||
|
||||
const {
|
||||
photoItems,
|
||||
albumSlug
|
||||
}: {
|
||||
photoItems: PhotoItemType[]
|
||||
albumSlug?: string
|
||||
} = $props()
|
||||
interface Props {
|
||||
photos: Photo[]
|
||||
}
|
||||
|
||||
// Split items into two columns
|
||||
const column1 = $derived(photoItems.filter((_, index) => index % 2 === 0))
|
||||
const column2 = $derived(photoItems.filter((_, index) => index % 2 === 1))
|
||||
let { photos = [] }: Props = $props()
|
||||
</script>
|
||||
|
||||
<div class="two-column-grid">
|
||||
<div class="column">
|
||||
{#each column1 as item}
|
||||
<PhotoItem {item} {albumSlug} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="column">
|
||||
{#each column2 as item}
|
||||
<PhotoItem {item} {albumSlug} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.two-column-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $unit-3x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-3x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<PhotoGrid {photos} columns={2} gap="medium" />
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
showThumbnails={slideshowItems.length > 1}
|
||||
maxThumbnails={6}
|
||||
totalCount={album.photosCount}
|
||||
showMoreLink="/photos/{album.slug}"
|
||||
showMoreLink="/albums/{album.slug}"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
<div class="album-info">
|
||||
<h2 class="card-title">
|
||||
<a
|
||||
href="/photos/{album.slug}"
|
||||
href="/albums/{album.slug}"
|
||||
class="card-title-link"
|
||||
onclick={(e) => e.preventDefault()}
|
||||
tabindex="-1">{album.title}</a
|
||||
|
|
@ -55,6 +55,12 @@
|
|||
{#if album.description}
|
||||
<p class="album-description">{album.description}</p>
|
||||
{/if}
|
||||
|
||||
{#if album.hasContent}
|
||||
<div class="album-story-indicator">
|
||||
<span class="story-badge">📖 Photo Story</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</UniverseCard>
|
||||
|
||||
|
|
@ -83,7 +89,7 @@
|
|||
|
||||
.album-description {
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
|
|
@ -91,4 +97,20 @@
|
|||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.album-story-indicator {
|
||||
margin-top: $unit-2x;
|
||||
}
|
||||
|
||||
.story-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: $unit-half;
|
||||
padding: $unit-half $unit-2x;
|
||||
background: $blue-10;
|
||||
color: $blue-50;
|
||||
border-radius: $corner-radius-sm;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -76,21 +76,21 @@
|
|||
|
||||
.card-content {
|
||||
padding: $unit-3x;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
border-radius: $card-corner-radius;
|
||||
border: 1px solid $grey-95;
|
||||
transition: all 0.2s ease;
|
||||
border: $unit-1px solid $gray-95;
|
||||
transition: all $transition-normal ease;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-85;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
border-color: $gray-85;
|
||||
box-shadow: 0 $unit-2px $unit rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid $red-60;
|
||||
outline-offset: 2px;
|
||||
outline: $unit-2px solid $red-60;
|
||||
outline-offset: $unit-2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,17 +108,17 @@
|
|||
}
|
||||
|
||||
.card-date {
|
||||
color: $grey-40;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-40;
|
||||
font-size: $font-size-small;
|
||||
font-weight: 400;
|
||||
transition: color 0.2s ease;
|
||||
transition: color $transition-normal ease;
|
||||
}
|
||||
|
||||
:global(.card-icon) {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: $grey-40;
|
||||
transition: all 0.2s ease;
|
||||
width: $unit-2x;
|
||||
height: $unit-2x;
|
||||
fill: $gray-40;
|
||||
transition: all $transition-normal ease;
|
||||
}
|
||||
|
||||
.universe-card--post {
|
||||
|
|
@ -138,9 +138,9 @@
|
|||
}
|
||||
|
||||
:global(.card-title-link) {
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
transition: all $transition-normal ease;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,27 +155,27 @@
|
|||
}
|
||||
|
||||
:global(.card-icon rect:nth-child(1)) {
|
||||
transition: all 0.3s ease;
|
||||
height: 6px;
|
||||
y: 2px;
|
||||
transition: all $transition-medium ease;
|
||||
height: $unit-6px;
|
||||
y: $unit-2px;
|
||||
}
|
||||
|
||||
:global(.card-icon rect:nth-child(2)) {
|
||||
transition: all 0.3s ease;
|
||||
height: 10px;
|
||||
y: 2px;
|
||||
transition: all $transition-medium ease;
|
||||
height: $unit-10px;
|
||||
y: $unit-2px;
|
||||
}
|
||||
|
||||
:global(.card-icon rect:nth-child(3)) {
|
||||
transition: all 0.3s ease;
|
||||
height: 8px;
|
||||
y: 10px;
|
||||
transition: all $transition-medium ease;
|
||||
height: $unit;
|
||||
y: $unit-10px;
|
||||
}
|
||||
|
||||
:global(.card-icon rect:nth-child(4)) {
|
||||
transition: all 0.3s ease;
|
||||
height: 4px;
|
||||
y: 14px;
|
||||
transition: all $transition-medium ease;
|
||||
height: $unit-half;
|
||||
y: $unit-14px;
|
||||
}
|
||||
|
||||
:global(.card-title-link) {
|
||||
|
|
@ -189,9 +189,9 @@
|
|||
}
|
||||
|
||||
:global(.card-title-link) {
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
transition: all $transition-normal ease;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -48,12 +48,12 @@
|
|||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: 0 0 $unit-2x;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import UniverseCard from './UniverseCard.svelte'
|
||||
import { getContentExcerpt } from '$lib/utils/content'
|
||||
import { getContentExcerpt, renderEdraContent } from '$lib/utils/content'
|
||||
import { extractEmbeds } from '$lib/utils/extractEmbeds'
|
||||
import type { UniverseItem } from '../../routes/api/universe/+server'
|
||||
|
||||
|
|
@ -38,12 +38,6 @@
|
|||
</h2>
|
||||
{/if}
|
||||
|
||||
{#if post.content}
|
||||
<div class="post-excerpt">
|
||||
<p>{getContentExcerpt(post.content, 150)}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if firstEmbed}
|
||||
<div class="embed-preview">
|
||||
{#if firstEmbed.type === 'youtube' && firstEmbed.videoId}
|
||||
|
|
@ -84,7 +78,17 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
{#if post.postType === 'essay' && isContentTruncated}
|
||||
{#if post.content}
|
||||
<div class="post-excerpt">
|
||||
{#if post.postType === 'essay'}
|
||||
<p>{getContentExcerpt(post.content, 300)}</p>
|
||||
{:else}
|
||||
{@html renderEdraContent(post.content)}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if post.postType === 'essay' && isContentTruncated()}
|
||||
<p>
|
||||
<a href="/universe/{post.slug}" class="read-more" tabindex="-1">Continue reading</a>
|
||||
</p>
|
||||
|
|
@ -113,8 +117,8 @@
|
|||
}
|
||||
|
||||
.link-preview {
|
||||
background: $grey-97;
|
||||
border: 1px solid $grey-90;
|
||||
background: $gray-97;
|
||||
border: 1px solid $gray-90;
|
||||
border-radius: $card-corner-radius;
|
||||
padding: $unit-2x;
|
||||
margin-bottom: $unit-3x;
|
||||
|
|
@ -134,7 +138,7 @@
|
|||
|
||||
.link-description {
|
||||
margin: 0;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
|
@ -143,26 +147,65 @@
|
|||
.post-excerpt {
|
||||
p {
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
// Only apply truncation for essay excerpts
|
||||
p:only-child {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Styles for full content (non-essays)
|
||||
:global(p) {
|
||||
margin: 0 0 $unit-2x;
|
||||
color: $gray-10;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:global(a) {
|
||||
color: $red-60;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
:global(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
// Hide embeds in the rendered content since we show them separately
|
||||
:global(.url-embed-rendered) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.attachments {
|
||||
margin-bottom: $unit-3x;
|
||||
|
||||
.attachment-count {
|
||||
background: $grey-95;
|
||||
border: 1px solid $grey-85;
|
||||
background: $gray-95;
|
||||
border: 1px solid $gray-85;
|
||||
border-radius: $unit;
|
||||
padding: $unit $unit-2x;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
|
@ -187,9 +230,9 @@
|
|||
padding-bottom: 56%; // 16:9 aspect ratio
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
background: $grey-95;
|
||||
background: $gray-95;
|
||||
border-radius: $card-corner-radius;
|
||||
border: 1px solid $grey-85;
|
||||
border: 1px solid $gray-85;
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
|
|
@ -206,16 +249,16 @@
|
|||
.url-embed-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: $grey-97;
|
||||
background: $gray-97;
|
||||
border-radius: $card-corner-radius;
|
||||
overflow: hidden;
|
||||
border: 1px solid $grey-80;
|
||||
border: 1px solid $gray-80;
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-80;
|
||||
border-color: $gray-80;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
|
@ -224,7 +267,7 @@
|
|||
width: 100%;
|
||||
aspect-ratio: 2 / 1;
|
||||
overflow: hidden;
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
|
|
@ -247,7 +290,7 @@
|
|||
align-items: center;
|
||||
gap: $unit-half;
|
||||
font-size: 0.8125rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
.embed-favicon {
|
||||
|
|
@ -267,7 +310,7 @@
|
|||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
line-height: 1.3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
@ -278,7 +321,7 @@
|
|||
.embed-description {
|
||||
margin: 0;
|
||||
font-size: 0.9375rem;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
<style lang="scss">
|
||||
.view-mode-selector {
|
||||
width: 100%;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
border-radius: $corner-radius-lg;
|
||||
box-sizing: border-box;
|
||||
padding: $unit;
|
||||
|
|
@ -119,10 +119,10 @@
|
|||
border-radius: $corner-radius-sm;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: $grey-60;
|
||||
color: $gray-60;
|
||||
|
||||
&:hover {
|
||||
background: $grey-95;
|
||||
background: $gray-95;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
align-items: center;
|
||||
gap: $unit;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.byline-section {
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
}
|
||||
|
||||
.separator {
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
import AvatarSimple from '$lib/components/AvatarSimple.svelte'
|
||||
import WorkIcon from '$icons/work.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)
|
||||
let isScrolled = $state(false)
|
||||
|
|
@ -31,8 +32,8 @@
|
|||
const navItems: NavItem[] = [
|
||||
{ text: 'Projects', href: '/admin/projects', icon: WorkIcon },
|
||||
{ text: 'Universe', href: '/admin/posts', icon: UniverseIcon },
|
||||
{ text: 'Albums', href: '/admin/albums', icon: PhotosIcon },
|
||||
{ text: 'Media', href: '/admin/media', icon: PhotosIcon }
|
||||
{ text: 'Albums', href: '/admin/albums', icon: AlbumIcon },
|
||||
{ text: 'Media', href: '/admin/media', icon: MediaIcon }
|
||||
]
|
||||
|
||||
// Calculate active index based on current path
|
||||
|
|
@ -83,14 +84,14 @@
|
|||
.admin-nav-bar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
z-index: $z-index-admin-nav;
|
||||
width: 100%;
|
||||
background: $bg-color;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-bottom 0.2s ease;
|
||||
|
||||
&.scrolled {
|
||||
border-bottom: 1px solid $grey-60;
|
||||
border-bottom: 1px solid $gray-60;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,13 +144,13 @@
|
|||
align-items: center;
|
||||
gap: $unit;
|
||||
text-decoration: none;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
font-weight: 400;
|
||||
font-size: 0.925rem;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
|
|
@ -202,7 +203,7 @@
|
|||
text-decoration: none;
|
||||
font-size: 0.925rem;
|
||||
font-weight: 500;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
transition: all 0.2s ease;
|
||||
position: relative;
|
||||
|
||||
|
|
@ -211,7 +212,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-70;
|
||||
background-color: $gray-70;
|
||||
}
|
||||
|
||||
&.active {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<style lang="scss">
|
||||
.segmented-control {
|
||||
display: inline-flex;
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
border-radius: 50px;
|
||||
padding: $unit-half;
|
||||
gap: $unit-half;
|
||||
|
|
@ -35,17 +35,17 @@
|
|||
border: none;
|
||||
border-radius: 50px;
|
||||
font-size: 0.925rem;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover:not(.active) {
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: white;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,40 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores'
|
||||
import { goto } from '$app/navigation'
|
||||
import BaseSegmentedController from './BaseSegmentedController.svelte'
|
||||
|
||||
const currentPath = $derived($page.url.pathname)
|
||||
|
||||
interface NavItem {
|
||||
text: string
|
||||
value: string
|
||||
label: string
|
||||
href: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ text: 'Dashboard', href: '/admin', icon: '📊' },
|
||||
{ text: 'Projects', href: '/admin/projects', icon: '💼' },
|
||||
{ text: 'Universe', href: '/admin/posts', icon: '🌟' },
|
||||
{ text: 'Media', href: '/admin/media', icon: '🖼️' }
|
||||
{ value: 'dashboard', label: 'Dashboard', href: '/admin', icon: '📊' },
|
||||
{ value: 'projects', label: 'Projects', href: '/admin/projects', icon: '💼' },
|
||||
{ value: 'universe', label: 'Universe', href: '/admin/posts', icon: '🌟' },
|
||||
{ value: 'media', label: 'Media', href: '/admin/media', icon: '🖼️' }
|
||||
]
|
||||
|
||||
// Track hover state and dropdown state
|
||||
let hoveredIndex = $state<number | null>(null)
|
||||
// Track dropdown state
|
||||
let showDropdown = $state(false)
|
||||
|
||||
// Calculate active index based on current path
|
||||
const activeIndex = $derived(
|
||||
// Calculate active value based on current path
|
||||
const activeValue = $derived(
|
||||
currentPath === '/admin'
|
||||
? 0
|
||||
? 'dashboard'
|
||||
: currentPath.startsWith('/admin/projects')
|
||||
? 1
|
||||
? 'projects'
|
||||
: currentPath.startsWith('/admin/posts')
|
||||
? 2
|
||||
? 'universe'
|
||||
: currentPath.startsWith('/admin/media')
|
||||
? 3
|
||||
: -1
|
||||
? 'media'
|
||||
: ''
|
||||
)
|
||||
|
||||
// Calculate pill position and width
|
||||
let containerElement: HTMLElement
|
||||
let itemElements: HTMLAnchorElement[] = []
|
||||
let pillStyle = $state('')
|
||||
|
||||
function updatePillPosition() {
|
||||
if (activeIndex >= 0 && itemElements[activeIndex] && containerElement) {
|
||||
const activeElement = itemElements[activeIndex]
|
||||
const containerRect = containerElement.getBoundingClientRect()
|
||||
const activeRect = activeElement.getBoundingClientRect()
|
||||
|
||||
// Subtract the container padding (8px) from the left position
|
||||
const left = activeRect.left - containerRect.left - 8
|
||||
const width = activeRect.width
|
||||
|
||||
pillStyle = `transform: translateX(${left}px); width: ${width}px;`
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
updatePillPosition()
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => updatePillPosition()
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
})
|
||||
|
||||
function logout() {
|
||||
localStorage.removeItem('admin_auth')
|
||||
goto('/admin/login')
|
||||
|
|
@ -86,26 +56,21 @@
|
|||
})
|
||||
</script>
|
||||
|
||||
<nav class="admin-segmented-controller" bind:this={containerElement}>
|
||||
<div class="pills-container">
|
||||
{#if activeIndex >= 0}
|
||||
<div class="active-pill" style={pillStyle}></div>
|
||||
{/if}
|
||||
|
||||
{#each navItems as item, index}
|
||||
<a
|
||||
href={item.href}
|
||||
class="nav-item"
|
||||
class:active={index === activeIndex}
|
||||
bind:this={itemElements[index]}
|
||||
onmouseenter={() => (hoveredIndex = index)}
|
||||
onmouseleave={() => (hoveredIndex = null)}
|
||||
>
|
||||
<span class="icon">{item.icon}</span>
|
||||
<span>{item.text}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<nav class="admin-segmented-controller">
|
||||
<BaseSegmentedController
|
||||
items={navItems}
|
||||
value={activeValue}
|
||||
variant="navigation"
|
||||
pillColor="#e5e5e5"
|
||||
gap={4}
|
||||
containerPadding={0}
|
||||
class="admin-nav-pills"
|
||||
>
|
||||
{#snippet children({ item, isActive })}
|
||||
<span class="icon">{item.icon}</span>
|
||||
<span>{item.label}</span>
|
||||
{/snippet}
|
||||
</BaseSegmentedController>
|
||||
|
||||
<div class="dropdown-container">
|
||||
<button
|
||||
|
|
@ -139,7 +104,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
background: $grey-100;
|
||||
background: $gray-100;
|
||||
padding: $unit;
|
||||
border-radius: 100px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
|
@ -147,55 +112,33 @@
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
.pills-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
:global(.admin-nav-pills) {
|
||||
flex: 1;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: none !important;
|
||||
|
||||
:global(.segmented-pill) {
|
||||
background-color: $gray-85 !important;
|
||||
}
|
||||
|
||||
:global(.segmented-item) {
|
||||
gap: 6px;
|
||||
padding: 10px 16px;
|
||||
font-size: 1rem;
|
||||
color: $gray-20;
|
||||
|
||||
&:global(.active) {
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active-pill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background-color: $grey-85;
|
||||
border-radius: 100px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 16px;
|
||||
border-radius: 100px;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: $grey-20;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
|
||||
&:hover:not(.active) {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $grey-10;
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.icon {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1;
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropdown-container {
|
||||
|
|
@ -211,13 +154,13 @@
|
|||
border-radius: 50%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
@ -237,7 +180,7 @@
|
|||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
min-width: 150px;
|
||||
z-index: 1050;
|
||||
z-index: $z-index-modal;
|
||||
overflow: hidden;
|
||||
animation: slideDown 0.2s ease;
|
||||
}
|
||||
|
|
@ -250,12 +193,12 @@
|
|||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.925rem;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,197 +1,258 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { z } from 'zod'
|
||||
import AdminPage from './AdminPage.svelte'
|
||||
import Button from './Button.svelte'
|
||||
import AdminSegmentedControl from './AdminSegmentedControl.svelte'
|
||||
import Input from './Input.svelte'
|
||||
import GalleryUploader from './GalleryUploader.svelte'
|
||||
import Editor from './Editor.svelte'
|
||||
import Button from './Button.svelte'
|
||||
import StatusDropdown from './StatusDropdown.svelte'
|
||||
import UnifiedMediaModal from './UnifiedMediaModal.svelte'
|
||||
import SmartImage from '../SmartImage.svelte'
|
||||
import Composer from './composer'
|
||||
import { authenticatedFetch } from '$lib/admin-auth'
|
||||
import { toast } from '$lib/stores/toast'
|
||||
import type { Album, Media } from '@prisma/client'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
import type { Media } from '@prisma/client'
|
||||
|
||||
interface Props {
|
||||
postId?: number
|
||||
initialData?: {
|
||||
title?: string
|
||||
slug?: string
|
||||
content?: JSONContent
|
||||
gallery?: Media[]
|
||||
status: 'draft' | 'published'
|
||||
tags?: string[]
|
||||
}
|
||||
album?: Album | null
|
||||
mode: 'create' | 'edit'
|
||||
}
|
||||
|
||||
let { postId, initialData, mode }: Props = $props()
|
||||
let { album = null, mode }: Props = $props()
|
||||
|
||||
// Album schema for validation
|
||||
const albumSchema = z.object({
|
||||
title: z.string().min(1, 'Title is required'),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, 'Slug is required')
|
||||
.regex(/^[a-z0-9-]+$/, 'Slug must be lowercase letters, numbers, and hyphens only'),
|
||||
location: z.string().optional(),
|
||||
year: z.string().optional()
|
||||
})
|
||||
|
||||
// State
|
||||
let isLoading = $state(mode === 'edit')
|
||||
let isSaving = $state(false)
|
||||
let error = $state('')
|
||||
let status = $state<'draft' | 'published'>(initialData?.status || 'draft')
|
||||
let validationErrors = $state<Record<string, string>>({})
|
||||
let showBulkAlbumModal = $state(false)
|
||||
let albumMedia = $state<any[]>([])
|
||||
let editorInstance = $state<any>()
|
||||
let activeTab = $state('metadata')
|
||||
let pendingMediaIds = $state<number[]>([]) // Photos to add after album creation
|
||||
|
||||
const tabOptions = [
|
||||
{ value: 'metadata', label: 'Metadata' },
|
||||
{ value: 'content', label: 'Content' }
|
||||
]
|
||||
|
||||
// Form data
|
||||
let title = $state(initialData?.title || '')
|
||||
let slug = $state(initialData?.slug || '')
|
||||
let content = $state<JSONContent>({ type: 'doc', content: [] })
|
||||
let gallery = $state<Media[]>([])
|
||||
let tags = $state(initialData?.tags?.join(', ') || '')
|
||||
let formData = $state({
|
||||
title: '',
|
||||
slug: '',
|
||||
year: '',
|
||||
location: '',
|
||||
showInUniverse: false,
|
||||
status: 'draft' as 'draft' | 'published',
|
||||
content: { type: 'doc', content: [{ type: 'paragraph' }] } as JSONContent
|
||||
})
|
||||
|
||||
// Editor ref
|
||||
let editorRef: any
|
||||
// Derived state for existing media IDs
|
||||
const existingMediaIds = $derived(albumMedia.map((item) => item.media.id))
|
||||
|
||||
// Auto-generate slug from title
|
||||
// Watch for album changes and populate form data
|
||||
$effect(() => {
|
||||
if (title && !slug) {
|
||||
slug = title
|
||||
if (album && mode === 'edit') {
|
||||
populateFormData(album)
|
||||
loadAlbumMedia()
|
||||
} else if (mode === 'create') {
|
||||
isLoading = false
|
||||
}
|
||||
})
|
||||
|
||||
// Watch for title changes and update slug
|
||||
$effect(() => {
|
||||
if (formData.title && mode === 'create') {
|
||||
formData.slug = formData.title
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize data for edit mode
|
||||
$effect(() => {
|
||||
if (initialData && mode === 'edit') {
|
||||
// Parse album content structure
|
||||
if (
|
||||
initialData.content &&
|
||||
typeof initialData.content === 'object' &&
|
||||
'type' in initialData.content
|
||||
) {
|
||||
const albumContent = initialData.content as any
|
||||
if (albumContent.type === 'album') {
|
||||
// Album content structure: { type: 'album', gallery: [mediaIds], description: JSONContent }
|
||||
if (albumContent.gallery) {
|
||||
// Load media objects from IDs (we'll need to fetch these)
|
||||
loadGalleryMedia(albumContent.gallery)
|
||||
}
|
||||
if (albumContent.description) {
|
||||
content = albumContent.description
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to regular content
|
||||
content = initialData.content || { type: 'doc', content: [] }
|
||||
}
|
||||
|
||||
// Load gallery from initialData if provided directly
|
||||
if (initialData.gallery) {
|
||||
gallery = initialData.gallery
|
||||
}
|
||||
function populateFormData(data: Album) {
|
||||
formData = {
|
||||
title: data.title || '',
|
||||
slug: data.slug || '',
|
||||
year: data.date ? new Date(data.date).getFullYear().toString() : '',
|
||||
location: data.location || '',
|
||||
showInUniverse: data.showInUniverse || false,
|
||||
status: (data.status as 'draft' | 'published') || 'draft',
|
||||
content: (data.content as JSONContent) || { type: 'doc', content: [{ type: 'paragraph' }] }
|
||||
}
|
||||
})
|
||||
|
||||
async function loadGalleryMedia(mediaIds: number[]) {
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
async function loadAlbumMedia() {
|
||||
if (!album) return
|
||||
|
||||
try {
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) return
|
||||
const response = await authenticatedFetch(`/api/albums/${album.id}`)
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
albumMedia = data.media || []
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load album media:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const mediaPromises = mediaIds.map(async (id) => {
|
||||
const response = await fetch(`/api/media/${id}`, {
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
})
|
||||
if (response.ok) {
|
||||
return await response.json()
|
||||
}
|
||||
return null
|
||||
function validateForm() {
|
||||
try {
|
||||
albumSchema.parse({
|
||||
title: formData.title,
|
||||
slug: formData.slug,
|
||||
location: formData.location || undefined,
|
||||
year: formData.year || undefined
|
||||
})
|
||||
|
||||
const mediaResults = await Promise.all(mediaPromises)
|
||||
gallery = mediaResults.filter((media) => media !== null)
|
||||
} catch (error) {
|
||||
console.error('Failed to load gallery media:', error)
|
||||
validationErrors = {}
|
||||
return true
|
||||
} catch (err) {
|
||||
if (err instanceof z.ZodError) {
|
||||
const errors: Record<string, string> = {}
|
||||
err.errors.forEach((e) => {
|
||||
if (e.path[0]) {
|
||||
errors[e.path[0].toString()] = e.message
|
||||
}
|
||||
})
|
||||
validationErrors = errors
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Validation
|
||||
let isValid = $derived(title.trim().length > 0 && gallery.length > 0)
|
||||
async function handleSave() {
|
||||
if (!validateForm()) {
|
||||
toast.error('Please fix the validation errors')
|
||||
return
|
||||
}
|
||||
|
||||
function handleGalleryUpload(newMedia: Media[]) {
|
||||
gallery = [...gallery, ...newMedia]
|
||||
}
|
||||
|
||||
function handleGalleryReorder(reorderedMedia: Media[]) {
|
||||
gallery = reorderedMedia
|
||||
}
|
||||
|
||||
function handleEditorChange(newContent: JSONContent) {
|
||||
content = newContent
|
||||
}
|
||||
|
||||
async function handleSave(newStatus: 'draft' | 'published' = status) {
|
||||
if (!isValid) return
|
||||
|
||||
isSaving = true
|
||||
error = ''
|
||||
const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} album...`)
|
||||
|
||||
try {
|
||||
const postData = {
|
||||
title: title.trim(),
|
||||
slug: slug,
|
||||
postType: 'album',
|
||||
status: newStatus,
|
||||
content,
|
||||
gallery: gallery.map((media) => media.id),
|
||||
featuredImage: gallery.length > 0 ? gallery[0].id : undefined,
|
||||
tags: tags.trim() ? tags.split(',').map((tag) => tag.trim()) : []
|
||||
isSaving = true
|
||||
|
||||
const payload = {
|
||||
title: formData.title,
|
||||
slug: formData.slug,
|
||||
description: null,
|
||||
date: formData.year || null,
|
||||
location: formData.location || null,
|
||||
showInUniverse: formData.showInUniverse,
|
||||
status: formData.status,
|
||||
content: formData.content
|
||||
}
|
||||
|
||||
const url = mode === 'edit' ? `/api/posts/${postId}` : '/api/posts'
|
||||
const url = mode === 'edit' ? `/api/albums/${album?.id}` : '/api/albums'
|
||||
const method = mode === 'edit' ? 'PUT' : 'POST'
|
||||
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
goto('/admin/login')
|
||||
return
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
const response = await authenticatedFetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Basic ${auth}`
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(postData)
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.text()
|
||||
throw new Error(`Failed to save album: ${errorData}`)
|
||||
const errorData = await response.json()
|
||||
throw new Error(
|
||||
errorData.message || `Failed to ${mode === 'edit' ? 'save' : 'create'} album`
|
||||
)
|
||||
}
|
||||
|
||||
status = newStatus
|
||||
goto('/admin/posts')
|
||||
const savedAlbum = await response.json()
|
||||
|
||||
toast.dismiss(loadingToastId)
|
||||
|
||||
// Add pending photos to newly created album
|
||||
if (mode === 'create' && pendingMediaIds.length > 0) {
|
||||
const photoToastId = toast.loading('Adding selected photos to album...')
|
||||
try {
|
||||
const photoResponse = await authenticatedFetch(`/api/albums/${savedAlbum.id}/media`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ mediaIds: pendingMediaIds })
|
||||
})
|
||||
|
||||
if (!photoResponse.ok) {
|
||||
throw new Error('Failed to add photos to album')
|
||||
}
|
||||
|
||||
toast.dismiss(photoToastId)
|
||||
toast.success(
|
||||
`Album created with ${pendingMediaIds.length} photo${pendingMediaIds.length !== 1 ? 's' : ''}!`
|
||||
)
|
||||
} catch (err) {
|
||||
toast.dismiss(photoToastId)
|
||||
toast.error(
|
||||
'Album created but failed to add photos. You can add them by editing the album.'
|
||||
)
|
||||
console.error('Failed to add photos:', err)
|
||||
}
|
||||
} else {
|
||||
toast.success(`Album ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
|
||||
}
|
||||
|
||||
if (mode === 'create') {
|
||||
goto(`/admin/albums/${savedAlbum.id}/edit`)
|
||||
} else if (mode === 'edit' && album) {
|
||||
// Update the album object to reflect saved changes
|
||||
album = savedAlbum
|
||||
populateFormData(savedAlbum)
|
||||
}
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to save album'
|
||||
toast.dismiss(loadingToastId)
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: `Failed to ${mode === 'edit' ? 'save' : 'create'} album`
|
||||
)
|
||||
console.error(err)
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
if (hasChanges() && !confirm('Are you sure you want to cancel? Your changes will be lost.')) {
|
||||
return
|
||||
}
|
||||
goto('/admin/posts')
|
||||
async function handleStatusChange(newStatus: string) {
|
||||
formData.status = newStatus as any
|
||||
await handleSave()
|
||||
}
|
||||
|
||||
function hasChanges(): boolean {
|
||||
if (mode === 'create') {
|
||||
return title.trim().length > 0 || gallery.length > 0 || tags.trim().length > 0
|
||||
async function handleBulkAlbumSave() {
|
||||
// Reload album to get updated photo count
|
||||
if (album && mode === 'edit') {
|
||||
await loadAlbumMedia()
|
||||
}
|
||||
}
|
||||
|
||||
// For edit mode, compare with initial data
|
||||
return (
|
||||
title !== (initialData?.title || '') ||
|
||||
gallery !== (initialData?.gallery || []) ||
|
||||
tags !== (initialData?.tags?.join(', ') || '')
|
||||
)
|
||||
function handleContentUpdate(content: JSONContent) {
|
||||
formData.content = content
|
||||
}
|
||||
|
||||
function handlePhotoSelection(media: Media[]) {
|
||||
pendingMediaIds = media.map((m) => m.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<AdminPage>
|
||||
<header slot="header">
|
||||
<div class="header-left">
|
||||
<button class="btn-icon" onclick={handleCancel}>
|
||||
<button class="btn-icon" onclick={() => goto('/admin/albums')} aria-label="Back to albums">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<path
|
||||
d="M12.5 15L7.5 10L12.5 5"
|
||||
|
|
@ -202,121 +263,193 @@
|
|||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<h1>📸 {mode === 'create' ? 'New Album' : 'Edit Album'}</h1>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<AdminSegmentedControl
|
||||
options={tabOptions}
|
||||
value={activeTab}
|
||||
onChange={(value) => (activeTab = value)}
|
||||
/>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
{#if mode === 'create'}
|
||||
<Button variant="secondary" onclick={handleCancel} disabled={isSaving}>Cancel</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onclick={() => handleSave('draft')}
|
||||
disabled={!isValid || isSaving}
|
||||
>
|
||||
{isSaving ? 'Saving...' : 'Save Draft'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onclick={() => handleSave('published')}
|
||||
disabled={!isValid || isSaving}
|
||||
>
|
||||
{isSaving ? 'Publishing...' : 'Publish Album'}
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="secondary" onclick={handleCancel} disabled={isSaving}>Cancel</Button>
|
||||
<Button variant="primary" onclick={() => handleSave()} disabled={!isValid || isSaving}>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
{#if !isLoading}
|
||||
<StatusDropdown
|
||||
currentStatus={formData.status}
|
||||
onStatusChange={handleStatusChange}
|
||||
disabled={isSaving || (mode === 'create' && (!formData.title || !formData.slug))}
|
||||
isLoading={isSaving}
|
||||
primaryAction={formData.status === 'published'
|
||||
? { label: 'Save', status: 'published' }
|
||||
: { label: 'Publish', status: 'published' }}
|
||||
dropdownActions={[
|
||||
{ label: 'Save as Draft', status: 'draft', show: formData.status !== 'draft' }
|
||||
]}
|
||||
viewUrl={album?.slug ? `/albums/${album.slug}` : undefined}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="album-form">
|
||||
{#if error}
|
||||
<div class="error-message">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="admin-container">
|
||||
{#if isLoading}
|
||||
<div class="loading">Loading album...</div>
|
||||
{:else}
|
||||
<div class="tab-panels">
|
||||
<!-- Metadata Panel -->
|
||||
<div class="panel content-wrapper" class:active={activeTab === 'metadata'}>
|
||||
<!-- Album Details -->
|
||||
<div class="form-section">
|
||||
<Input
|
||||
label="Title"
|
||||
size="jumbo"
|
||||
bind:value={formData.title}
|
||||
placeholder="Album title"
|
||||
required
|
||||
error={validationErrors.title}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
|
||||
<div class="form-content">
|
||||
<div class="form-section">
|
||||
<Input
|
||||
label="Album Title"
|
||||
size="jumbo"
|
||||
bind:value={title}
|
||||
placeholder="Enter album title"
|
||||
required={true}
|
||||
error={title.trim().length === 0 ? 'Title is required' : undefined}
|
||||
/>
|
||||
<Input
|
||||
label="Slug"
|
||||
bind:value={formData.slug}
|
||||
placeholder="url-friendly-name"
|
||||
required
|
||||
error={validationErrors.slug}
|
||||
disabled={isSaving || mode === 'edit'}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Slug"
|
||||
bind:value={slug}
|
||||
placeholder="album-url-slug"
|
||||
helpText="URL-friendly version of the title"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-grid">
|
||||
<Input
|
||||
label="Location"
|
||||
bind:value={formData.location}
|
||||
placeholder="e.g. Tokyo, Japan"
|
||||
error={validationErrors.location}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
<Input
|
||||
label="Year"
|
||||
type="text"
|
||||
bind:value={formData.year}
|
||||
placeholder="e.g. 2023 or 2023-2025"
|
||||
error={validationErrors.year}
|
||||
disabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<GalleryUploader
|
||||
label="Album Photos"
|
||||
bind:value={gallery}
|
||||
onUpload={handleGalleryUpload}
|
||||
onReorder={handleGalleryReorder}
|
||||
required={true}
|
||||
showBrowseLibrary={true}
|
||||
maxItems={50}
|
||||
placeholder="Add photos to your album"
|
||||
helpText="First photo will be used as the album cover"
|
||||
error={gallery.length === 0 ? 'At least one photo is required' : undefined}
|
||||
/>
|
||||
</div>
|
||||
<!-- Display Settings -->
|
||||
<div class="form-section">
|
||||
<label class="toggle-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={formData.showInUniverse}
|
||||
disabled={isSaving}
|
||||
class="toggle-input"
|
||||
/>
|
||||
<div class="toggle-content">
|
||||
<span class="toggle-title">Show in Universe</span>
|
||||
<span class="toggle-description">Display this album in the Universe feed</span>
|
||||
</div>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="editor-wrapper">
|
||||
<label class="form-label">Description</label>
|
||||
<Editor
|
||||
bind:this={editorRef}
|
||||
bind:data={content}
|
||||
onChange={handleEditorChange}
|
||||
placeholder="Write a description for your album..."
|
||||
simpleMode={false}
|
||||
minHeight={200}
|
||||
<!-- Photos Grid -->
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<h3 class="section-title">
|
||||
Photos {albumMedia.length > 0 || pendingMediaIds.length > 0
|
||||
? `(${mode === 'edit' ? albumMedia.length : pendingMediaIds.length})`
|
||||
: ''}
|
||||
</h3>
|
||||
<button class="btn-secondary" onclick={() => (showBulkAlbumModal = true)}>
|
||||
{mode === 'create' ? 'Select Photos' : 'Manage Photos'}
|
||||
</button>
|
||||
</div>
|
||||
{#if mode === 'edit' && albumMedia.length > 0}
|
||||
<div class="photos-grid">
|
||||
{#each albumMedia as item}
|
||||
<div class="photo-item">
|
||||
<SmartImage
|
||||
media={item.media}
|
||||
alt={item.media.description || item.media.filename}
|
||||
sizes="(max-width: 768px) 50vw, 25vw"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if mode === 'create' && pendingMediaIds.length > 0}
|
||||
<p class="selected-count">
|
||||
{pendingMediaIds.length} photo{pendingMediaIds.length !== 1 ? 's' : ''} selected. They
|
||||
will be added when you save the album.
|
||||
</p>
|
||||
{:else}
|
||||
<p class="empty-state">
|
||||
No photos {mode === 'create' ? 'selected' : 'added'} yet. Click "{mode === 'create'
|
||||
? 'Select Photos'
|
||||
: 'Manage Photos'}" to {mode === 'create' ? 'select' : 'add'} photos.
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content Panel -->
|
||||
<div class="panel panel-content" class:active={activeTab === 'content'}>
|
||||
<Composer
|
||||
bind:this={editorInstance}
|
||||
bind:data={formData.content}
|
||||
placeholder="Add album content..."
|
||||
onChange={handleContentUpdate}
|
||||
editable={!isSaving}
|
||||
albumId={album?.id}
|
||||
variant="full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<Input
|
||||
label="Tags"
|
||||
bind:value={tags}
|
||||
placeholder="travel, photography, nature"
|
||||
helpText="Separate tags with commas"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</AdminPage>
|
||||
|
||||
<!-- Media Modal -->
|
||||
<UnifiedMediaModal
|
||||
bind:isOpen={showBulkAlbumModal}
|
||||
albumId={album?.id}
|
||||
selectedIds={mode === 'edit' ? existingMediaIds : pendingMediaIds}
|
||||
showInAlbumMode={mode === 'edit'}
|
||||
onSave={mode === 'edit' ? handleBulkAlbumSave : undefined}
|
||||
onSelect={mode === 'create' ? handlePhotoSelection : undefined}
|
||||
mode="multiple"
|
||||
title={mode === 'create' ? 'Select Photos for Album' : 'Manage Album Photos'}
|
||||
confirmText={mode === 'create' ? 'Select Photos' : 'Update Photos'}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$styles/variables.scss';
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
header {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 250px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: $unit-2x;
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: $grey-10;
|
||||
.header-left {
|
||||
width: 250px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-2x;
|
||||
.header-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 250px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
|
|
@ -324,7 +457,7 @@
|
|||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -333,56 +466,230 @@
|
|||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: $grey-90;
|
||||
color: $grey-10;
|
||||
background: $gray-90;
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
.album-form {
|
||||
max-width: 800px;
|
||||
.admin-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: $unit-3x;
|
||||
padding: 0 $unit-2x $unit-4x;
|
||||
box-sizing: border-box;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
padding: 0 $unit-2x $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
padding: $unit-2x;
|
||||
margin-bottom: $unit-3x;
|
||||
color: #dc2626;
|
||||
font-size: 0.875rem;
|
||||
.tab-panels {
|
||||
position: relative;
|
||||
|
||||
.panel {
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.active {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-4x;
|
||||
.content-wrapper {
|
||||
background: white;
|
||||
border-radius: $unit-2x;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
padding: $unit-3x;
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: $unit-6x;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-4x;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: $unit-6x;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
.form-label {
|
||||
display: block;
|
||||
.section-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: $gray-10;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: $unit-3x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.photos-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: $unit-2x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.photo-item {
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: $unit;
|
||||
background: $gray-95;
|
||||
|
||||
:global(img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
background: white;
|
||||
padding: 0;
|
||||
min-height: 80vh;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
min-height: 600px;
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle styles
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $unit-3x;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
|
||||
&:checked + .toggle-content + .toggle-slider {
|
||||
background-color: $blue-60;
|
||||
|
||||
&::before {
|
||||
transform: translateX(20px);
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled + .toggle-content + .toggle-slider {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
background-color: $gray-80;
|
||||
border-radius: 12px;
|
||||
transition: background-color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s ease;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-half;
|
||||
|
||||
.toggle-title {
|
||||
font-weight: 500;
|
||||
color: $gray-10;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: $grey-20;
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
||||
.toggle-description {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-50;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
.album-form {
|
||||
padding: $unit-2x;
|
||||
// Button styles
|
||||
.btn-secondary {
|
||||
padding: $unit $unit-2x;
|
||||
border: 1px solid $gray-80;
|
||||
background: white;
|
||||
color: $gray-20;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: $gray-95;
|
||||
border-color: $gray-70;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-wrap: wrap;
|
||||
gap: $unit;
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: $gray-50;
|
||||
font-size: 0.875rem;
|
||||
text-align: center;
|
||||
padding: $unit-4x;
|
||||
background: $gray-95;
|
||||
border-radius: $unit;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
color: $gray-30;
|
||||
font-size: 0.875rem;
|
||||
padding: $unit-2x;
|
||||
margin: 0;
|
||||
background: $gray-95;
|
||||
border-radius: $unit;
|
||||
border: 1px solid $gray-90;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,15 +18,15 @@
|
|||
date: string | null
|
||||
location: string | null
|
||||
coverPhotoId: number | null
|
||||
isPhotography: boolean
|
||||
status: string
|
||||
showInUniverse: boolean
|
||||
publishedAt: string | null
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
photos: Photo[]
|
||||
content?: any
|
||||
_count: {
|
||||
photos: number
|
||||
media: number
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
}
|
||||
|
||||
function getPhotoCount(): number {
|
||||
return album._count?.photos || 0
|
||||
return album._count?.media || 0
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -135,9 +135,10 @@
|
|||
<h3 class="album-title">{album.title}</h3>
|
||||
<AdminByline
|
||||
sections={[
|
||||
album.isPhotography ? 'Photography' : 'Album',
|
||||
'Album',
|
||||
album.status === 'published' ? 'Published' : 'Draft',
|
||||
`${getPhotoCount()} ${getPhotoCount() === 1 ? 'photo' : 'photos'}`,
|
||||
...(album.content ? ['📖 Story'] : []),
|
||||
album.status === 'published' && album.publishedAt
|
||||
? `Published ${formatRelativeTime(album.publishedAt)}`
|
||||
: `Created ${formatRelativeTime(album.createdAt)}`
|
||||
|
|
@ -188,7 +189,7 @@
|
|||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +202,7 @@
|
|||
justify-content: center;
|
||||
border-radius: $unit;
|
||||
overflow: hidden;
|
||||
background-color: $grey-90;
|
||||
background-color: $gray-90;
|
||||
|
||||
.thumbnail-image {
|
||||
width: 100%;
|
||||
|
|
@ -215,7 +216,7 @@
|
|||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: $grey-50;
|
||||
color: $gray-50;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +231,7 @@
|
|||
.album-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: $grey-10;
|
||||
color: $gray-10;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
|
@ -253,7 +254,7 @@
|
|||
border: none;
|
||||
border-radius: $unit;
|
||||
cursor: pointer;
|
||||
color: $grey-30;
|
||||
color: $gray-30;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
|
|
@ -267,7 +268,7 @@
|
|||
right: 0;
|
||||
margin-top: $unit-half;
|
||||
background: white;
|
||||
border: 1px solid $grey-85;
|
||||
border: 1px solid $gray-85;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
|
|
@ -282,12 +283,12 @@
|
|||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
}
|
||||
|
||||
&.delete {
|
||||
|
|
@ -297,7 +298,7 @@
|
|||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-80;
|
||||
background-color: $gray-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
441
src/lib/components/admin/AlbumSelector.svelte
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import Button from './Button.svelte'
|
||||
import Input from './Input.svelte'
|
||||
import LoadingSpinner from './LoadingSpinner.svelte'
|
||||
|
||||
interface Album {
|
||||
id: number
|
||||
title: string
|
||||
slug: string
|
||||
_count?: {
|
||||
media: number
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
mediaId: number
|
||||
currentAlbums: Album[]
|
||||
onUpdate: (albums: Album[]) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
let { mediaId, currentAlbums = [], onUpdate, onClose }: Props = $props()
|
||||
|
||||
// State
|
||||
let albums = $state<Album[]>([])
|
||||
let filteredAlbums = $state<Album[]>([])
|
||||
let selectedAlbumIds = $state<Set<number>>(new Set(currentAlbums.map((a) => a.id)))
|
||||
let isLoading = $state(true)
|
||||
let isSaving = $state(false)
|
||||
let error = $state('')
|
||||
let searchQuery = $state('')
|
||||
let showCreateNew = $state(false)
|
||||
let newAlbumTitle = $state('')
|
||||
let newAlbumSlug = $state('')
|
||||
|
||||
onMount(() => {
|
||||
loadAlbums()
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (searchQuery) {
|
||||
filteredAlbums = albums.filter((album) =>
|
||||
album.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
)
|
||||
} else {
|
||||
filteredAlbums = albums
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
if (newAlbumTitle) {
|
||||
// Auto-generate slug from title
|
||||
newAlbumSlug = newAlbumTitle
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '')
|
||||
}
|
||||
})
|
||||
|
||||
async function loadAlbums() {
|
||||
try {
|
||||
isLoading = true
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) return
|
||||
|
||||
const response = await fetch('/api/albums', {
|
||||
headers: { Authorization: `Basic ${auth}` }
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load albums')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
albums = data.albums || []
|
||||
filteredAlbums = albums
|
||||
} catch (err) {
|
||||
console.error('Failed to load albums:', err)
|
||||
error = 'Failed to load albums'
|
||||
} finally {
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAlbum(albumId: number) {
|
||||
if (selectedAlbumIds.has(albumId)) {
|
||||
selectedAlbumIds.delete(albumId)
|
||||
} else {
|
||||
selectedAlbumIds.add(albumId)
|
||||
}
|
||||
selectedAlbumIds = new Set(selectedAlbumIds)
|
||||
}
|
||||
|
||||
async function createNewAlbum() {
|
||||
if (!newAlbumTitle.trim() || !newAlbumSlug.trim()) return
|
||||
|
||||
try {
|
||||
isSaving = true
|
||||
error = ''
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) return
|
||||
|
||||
const response = await fetch('/api/albums', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title: newAlbumTitle.trim(),
|
||||
slug: newAlbumSlug.trim(),
|
||||
isPhotography: true,
|
||||
status: 'draft'
|
||||
})
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.message || 'Failed to create album')
|
||||
}
|
||||
|
||||
const newAlbum = await response.json()
|
||||
|
||||
// Add to albums list and select it
|
||||
albums = [newAlbum, ...albums]
|
||||
selectedAlbumIds.add(newAlbum.id)
|
||||
selectedAlbumIds = new Set(selectedAlbumIds)
|
||||
|
||||
// Reset form
|
||||
showCreateNew = false
|
||||
newAlbumTitle = ''
|
||||
newAlbumSlug = ''
|
||||
searchQuery = ''
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to create album'
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
isSaving = true
|
||||
error = ''
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) return
|
||||
|
||||
// Get the list of albums to add/remove
|
||||
const currentAlbumIds = new Set(currentAlbums.map((a) => a.id))
|
||||
const albumsToAdd = Array.from(selectedAlbumIds).filter((id) => !currentAlbumIds.has(id))
|
||||
const albumsToRemove = currentAlbums
|
||||
.filter((a) => !selectedAlbumIds.has(a.id))
|
||||
.map((a) => a.id)
|
||||
|
||||
// Add to new albums
|
||||
for (const albumId of albumsToAdd) {
|
||||
const response = await fetch(`/api/albums/${albumId}/media`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ mediaIds: [mediaId] })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add to album')
|
||||
}
|
||||
}
|
||||
|
||||
// Remove from albums
|
||||
for (const albumId of albumsToRemove) {
|
||||
const response = await fetch(`/api/albums/${albumId}/media`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ mediaIds: [mediaId] })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to remove from album')
|
||||
}
|
||||
}
|
||||
|
||||
// Get updated album list
|
||||
const updatedAlbums = albums.filter((a) => selectedAlbumIds.has(a.id))
|
||||
onUpdate(updatedAlbums)
|
||||
onClose()
|
||||
} catch (err) {
|
||||
console.error('Failed to update albums:', err)
|
||||
error = 'Failed to update albums'
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
// Computed
|
||||
const hasChanges = $derived(() => {
|
||||
const currentIds = new Set(currentAlbums.map((a) => a.id))
|
||||
if (currentIds.size !== selectedAlbumIds.size) return true
|
||||
for (const id of selectedAlbumIds) {
|
||||
if (!currentIds.has(id)) return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="album-selector">
|
||||
<div class="selector-header">
|
||||
<h3>Manage Albums</h3>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error-message">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="selector-content">
|
||||
{#if !showCreateNew}
|
||||
<div class="search-section">
|
||||
<Input type="search" bind:value={searchQuery} placeholder="Search albums..." fullWidth />
|
||||
<Button variant="ghost" onclick={() => (showCreateNew = true)} buttonSize="small">
|
||||
<svg slot="icon" width="16" height="16" viewBox="0 0 16 16" fill="none">
|
||||
<path
|
||||
d="M8 3v10M3 8h10"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
New Album
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<div class="loading-state">
|
||||
<LoadingSpinner />
|
||||
<p>Loading albums...</p>
|
||||
</div>
|
||||
{:else if filteredAlbums.length === 0}
|
||||
<div class="empty-state">
|
||||
<p>{searchQuery ? 'No albums found' : 'No albums available'}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="album-grid">
|
||||
{#each filteredAlbums as album}
|
||||
<label class="album-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAlbumIds.has(album.id)}
|
||||
onchange={() => toggleAlbum(album.id)}
|
||||
/>
|
||||
<div class="album-info">
|
||||
<span class="album-title">{album.title}</span>
|
||||
<span class="album-meta">
|
||||
{album._count?.media || 0} photos
|
||||
</span>
|
||||
</div>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="create-new-form">
|
||||
<h4>Create New Album</h4>
|
||||
<Input
|
||||
label="Album Title"
|
||||
bind:value={newAlbumTitle}
|
||||
placeholder="My New Album"
|
||||
fullWidth
|
||||
/>
|
||||
<Input label="URL Slug" bind:value={newAlbumSlug} placeholder="my-new-album" fullWidth />
|
||||
<div class="form-actions">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={() => {
|
||||
showCreateNew = false
|
||||
newAlbumTitle = ''
|
||||
newAlbumSlug = ''
|
||||
}}
|
||||
disabled={isSaving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onclick={createNewAlbum}
|
||||
disabled={!newAlbumTitle.trim() || !newAlbumSlug.trim() || isSaving}
|
||||
>
|
||||
{isSaving ? 'Creating...' : 'Create Album'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if !showCreateNew}
|
||||
<div class="selector-footer">
|
||||
<Button variant="ghost" onclick={onClose}>Cancel</Button>
|
||||
<Button variant="primary" onclick={handleSave} disabled={!hasChanges() || isSaving}>
|
||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.album-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: white;
|
||||
border-radius: $unit-2x;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.selector-header {
|
||||
padding: $unit-3x;
|
||||
border-bottom: 1px solid $gray-85;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
margin: $unit-2x $unit-3x 0;
|
||||
padding: $unit-2x;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
border-radius: $unit;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.selector-content {
|
||||
flex: 1;
|
||||
padding: $unit-3x;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
margin-bottom: $unit-3x;
|
||||
}
|
||||
|
||||
.loading-state,
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: $unit-6x;
|
||||
text-align: center;
|
||||
color: $gray-40;
|
||||
|
||||
p {
|
||||
margin: $unit-2x 0 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.album-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
.album-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit-2x;
|
||||
padding: $unit-2x;
|
||||
background: $gray-95;
|
||||
border-radius: $unit;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: $gray-90;
|
||||
}
|
||||
|
||||
input[type='checkbox'] {
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.album-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.album-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: $gray-10;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.album-meta {
|
||||
font-size: 0.75rem;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
.create-new-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-3x;
|
||||
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.selector-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: $unit-2x;
|
||||
padding: $unit-3x;
|
||||
border-top: 1px solid $gray-85;
|
||||
}
|
||||
</style>
|
||||
229
src/lib/components/admin/AlbumSelectorModal.svelte
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
<script lang="ts">
|
||||
import Modal from './Modal.svelte'
|
||||
import AlbumSelector from './AlbumSelector.svelte'
|
||||
import Button from './Button.svelte'
|
||||
import CloseButton from '../icons/CloseButton.svelte'
|
||||
import LoadingSpinner from './LoadingSpinner.svelte'
|
||||
import type { Album } from '@prisma/client'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
selectedMediaIds: number[]
|
||||
onClose?: () => void
|
||||
onSave?: () => void
|
||||
}
|
||||
|
||||
let { isOpen = $bindable(), selectedMediaIds = [], onClose, onSave }: Props = $props()
|
||||
|
||||
// State
|
||||
let selectedAlbumId = $state<number | null>(null)
|
||||
let isSaving = $state(false)
|
||||
let error = $state('')
|
||||
|
||||
// Reset state when modal opens
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
selectedAlbumId = null
|
||||
error = ''
|
||||
}
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
if (!selectedAlbumId || selectedMediaIds.length === 0) return
|
||||
|
||||
try {
|
||||
isSaving = true
|
||||
error = ''
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) return
|
||||
|
||||
const response = await fetch(`/api/albums/${selectedAlbumId}/media`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Basic ${auth}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ mediaIds: selectedMediaIds })
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to add media to album')
|
||||
}
|
||||
|
||||
handleClose()
|
||||
onSave?.()
|
||||
} catch (err) {
|
||||
console.error('Failed to update album:', err)
|
||||
error = err instanceof Error ? err.message : 'Failed to update album'
|
||||
} finally {
|
||||
isSaving = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
selectedAlbumId = null
|
||||
error = ''
|
||||
isOpen = false
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
function handleAlbumSelect(albumId: number | null) {
|
||||
selectedAlbumId = albumId
|
||||
}
|
||||
|
||||
// Computed
|
||||
const canSave = $derived(selectedAlbumId !== null && selectedMediaIds.length > 0)
|
||||
const mediaCount = $derived(selectedMediaIds.length)
|
||||
</script>
|
||||
|
||||
<Modal bind:isOpen onClose={handleClose} size="medium" showCloseButton={false}>
|
||||
<div class="album-selector-modal">
|
||||
<!-- Header -->
|
||||
<div class="modal-header">
|
||||
<div class="header-top">
|
||||
<h2>Add to Album</h2>
|
||||
<button class="close-button" onclick={handleClose} aria-label="Close modal">
|
||||
<CloseButton size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<p class="modal-subtitle">
|
||||
Select an album to add {mediaCount}
|
||||
{mediaCount === 1 ? 'item' : 'items'} to
|
||||
</p>
|
||||
|
||||
{#if error}
|
||||
<div class="error-message">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Album Selector -->
|
||||
<div class="modal-body">
|
||||
<AlbumSelector {selectedAlbumId} onSelect={handleAlbumSelect} placeholder="Choose an album" />
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-footer">
|
||||
<div class="action-summary">
|
||||
{#if selectedAlbumId}
|
||||
<span>Ready to add {mediaCount} {mediaCount === 1 ? 'item' : 'items'}</span>
|
||||
{:else}
|
||||
<span>Select an album to continue</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<Button variant="ghost" onclick={handleCancel}>Cancel</Button>
|
||||
<Button variant="primary" onclick={handleSave} disabled={!canSave || isSaving}>
|
||||
{#if isSaving}
|
||||
<LoadingSpinner buttonSize="small" />
|
||||
Adding...
|
||||
{:else}
|
||||
Add to Album
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.album-selector-modal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 300px;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: $unit-3x;
|
||||
border-bottom: 1px solid $gray-90;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
color: $gray-10;
|
||||
}
|
||||
}
|
||||
|
||||
.header-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
margin: 0;
|
||||
font-size: 0.875rem;
|
||||
color: $gray-40;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: $gray-40;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
background: $gray-90;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
color: #dc2626;
|
||||
padding: $unit-2x;
|
||||
border-radius: $unit;
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
margin-top: $unit-2x;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
flex: 1;
|
||||
padding: $unit-3x;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: $unit-3x;
|
||||
padding: $unit-3x;
|
||||
border-top: 1px solid $gray-90;
|
||||
background: $gray-95;
|
||||
}
|
||||
|
||||
.action-summary {
|
||||
font-size: 0.875rem;
|
||||
color: $gray-30;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
</style>
|
||||
103
src/lib/components/admin/BaseDropdown.svelte
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte'
|
||||
import Button from './Button.svelte'
|
||||
import DropdownMenuContainer from './DropdownMenuContainer.svelte'
|
||||
|
||||
interface Props {
|
||||
isOpen?: boolean
|
||||
disabled?: boolean
|
||||
isLoading?: boolean
|
||||
dropdownTriggerSize?: 'small' | 'medium' | 'large'
|
||||
class?: string
|
||||
onToggle?: (isOpen: boolean) => void
|
||||
trigger: Snippet
|
||||
dropdown?: Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen = $bindable(false),
|
||||
disabled = false,
|
||||
isLoading = false,
|
||||
dropdownTriggerSize = 'large',
|
||||
class: className = '',
|
||||
onToggle,
|
||||
trigger,
|
||||
dropdown
|
||||
}: Props = $props()
|
||||
|
||||
function handleDropdownToggle(e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
isOpen = !isOpen
|
||||
onToggle?.(isOpen)
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
const target = event.target as HTMLElement
|
||||
if (!target.closest(`.${className}`) && !target.closest('.dropdown-container')) {
|
||||
isOpen = false
|
||||
onToggle?.(false)
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Use setTimeout to avoid immediate closing when clicking the trigger
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}, 0)
|
||||
return () => document.removeEventListener('click', handleClickOutside)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="dropdown-container {className}">
|
||||
<div class="dropdown-trigger">
|
||||
{@render trigger()}
|
||||
|
||||
{#if dropdown}
|
||||
<Button
|
||||
variant="ghost"
|
||||
iconOnly
|
||||
buttonSize={dropdownTriggerSize}
|
||||
onclick={handleDropdownToggle}
|
||||
{disabled}
|
||||
{isLoading}
|
||||
class="dropdown-toggle"
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M3 4.5L6 7.5L9 4.5"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{/snippet}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if isOpen && dropdown}
|
||||
<DropdownMenuContainer>
|
||||
{@render dropdown()}
|
||||
</DropdownMenuContainer>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.dropdown-trigger {
|
||||
display: flex;
|
||||
gap: $unit-half;
|
||||
}
|
||||
|
||||
:global(.dropdown-toggle) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
155
src/lib/components/admin/BaseModal.svelte
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
|
||||
// Convert CSS transition durations to milliseconds for Svelte transitions
|
||||
const TRANSITION_FAST_MS = 150 // $transition-fast: 0.15s
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
size?: 'small' | 'medium' | 'large' | 'jumbo' | 'full' | 'auto'
|
||||
closeOnBackdrop?: boolean
|
||||
closeOnEscape?: boolean
|
||||
onClose?: () => void
|
||||
class?: string
|
||||
}
|
||||
|
||||
let {
|
||||
isOpen = $bindable(),
|
||||
size = 'medium',
|
||||
closeOnBackdrop = true,
|
||||
closeOnEscape = true,
|
||||
onClose,
|
||||
class: className = ''
|
||||
}: Props = $props()
|
||||
|
||||
function handleClose() {
|
||||
isOpen = false
|
||||
onClose?.()
|
||||
}
|
||||
|
||||
function handleBackdropClick() {
|
||||
if (closeOnBackdrop) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && closeOnEscape && isOpen) {
|
||||
handleClose()
|
||||
}
|
||||
}
|
||||
|
||||
// Effect to handle body scroll locking
|
||||
$effect(() => {
|
||||
if (isOpen) {
|
||||
// Save current scroll position
|
||||
const scrollY = window.scrollY
|
||||
|
||||
// Lock body scroll
|
||||
document.body.style.position = 'fixed'
|
||||
document.body.style.top = `-${scrollY}px`
|
||||
document.body.style.width = '100%'
|
||||
document.body.style.overflow = 'hidden'
|
||||
|
||||
return () => {
|
||||
// Restore body scroll
|
||||
const scrollY = document.body.style.top
|
||||
document.body.style.position = ''
|
||||
document.body.style.top = ''
|
||||
document.body.style.width = ''
|
||||
document.body.style.overflow = ''
|
||||
|
||||
// Restore scroll position
|
||||
if (scrollY) {
|
||||
window.scrollTo(0, parseInt(scrollY || '0') * -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
}
|
||||
})
|
||||
|
||||
let modalClass = $derived(`modal modal-${size} ${className}`)
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="modal-backdrop"
|
||||
on:click={handleBackdropClick}
|
||||
transition:fade={{ duration: TRANSITION_FAST_MS }}
|
||||
>
|
||||
<div
|
||||
class={modalClass}
|
||||
on:click|stopPropagation
|
||||
transition:fade={{ duration: TRANSITION_FAST_MS }}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: $overlay-medium;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: $z-index-modal-backdrop;
|
||||
padding: $unit-2x;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: $white;
|
||||
border-radius: $card-corner-radius;
|
||||
box-shadow: 0 4px 12px $shadow-medium;
|
||||
position: relative;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
&.modal-auto {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
&.modal-small {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
&.modal-medium {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
&.modal-large {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
&.modal-jumbo {
|
||||
width: 90vw;
|
||||
max-width: 1400px;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
&.modal-full {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: 90vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
340
src/lib/components/admin/BaseSegmentedController.svelte
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
interface BaseItem {
|
||||
value: string | number
|
||||
label: string
|
||||
href?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface Props<T extends BaseItem = BaseItem> {
|
||||
items: T[]
|
||||
value?: string | number
|
||||
defaultValue?: string | number
|
||||
onChange?: (value: string | number, item: T) => void
|
||||
variant?: 'navigation' | 'selection'
|
||||
size?: 'small' | 'medium' | 'large'
|
||||
fullWidth?: boolean
|
||||
pillColor?: string | ((item: T) => string)
|
||||
showPill?: boolean
|
||||
gap?: number
|
||||
containerPadding?: number
|
||||
class?: string
|
||||
children?: Snippet<[{ item: T; index: number; isActive: boolean; isHovered: boolean }]>
|
||||
}
|
||||
|
||||
let {
|
||||
items = [],
|
||||
value = $bindable(),
|
||||
defaultValue,
|
||||
onChange,
|
||||
variant = 'selection',
|
||||
size = 'medium',
|
||||
fullWidth = false,
|
||||
pillColor = 'white',
|
||||
showPill = true,
|
||||
gap = 4,
|
||||
containerPadding = 4,
|
||||
class: className = '',
|
||||
children
|
||||
}: Props = $props()
|
||||
|
||||
// State
|
||||
let containerElement: HTMLElement
|
||||
let itemElements: HTMLElement[] = []
|
||||
let pillStyle = ''
|
||||
let hoveredIndex = $state(-1)
|
||||
let internalValue = $state(defaultValue ?? value ?? items[0]?.value ?? '')
|
||||
|
||||
// Derived state
|
||||
const currentValue = $derived(value ?? internalValue)
|
||||
const activeIndex = $derived(items.findIndex((item) => item.value === currentValue))
|
||||
|
||||
// Effects
|
||||
$effect(() => {
|
||||
if (value !== undefined) {
|
||||
internalValue = value
|
||||
}
|
||||
})
|
||||
|
||||
$effect(() => {
|
||||
updatePillPosition()
|
||||
})
|
||||
|
||||
// Functions
|
||||
function updatePillPosition() {
|
||||
if (!showPill) return
|
||||
|
||||
if (activeIndex >= 0 && itemElements[activeIndex] && containerElement) {
|
||||
const activeElement = itemElements[activeIndex]
|
||||
const containerRect = containerElement.getBoundingClientRect()
|
||||
const activeRect = activeElement.getBoundingClientRect()
|
||||
|
||||
const left = activeRect.left - containerRect.left - containerPadding
|
||||
const width = activeRect.width
|
||||
|
||||
pillStyle = `transform: translateX(${left}px); width: ${width}px;`
|
||||
} else {
|
||||
pillStyle = 'opacity: 0;'
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemClick(item: BaseItem, index: number) {
|
||||
if (variant === 'selection') {
|
||||
const newValue = item.value
|
||||
internalValue = newValue
|
||||
if (value === undefined) {
|
||||
// Uncontrolled mode
|
||||
value = newValue
|
||||
}
|
||||
onChange?.(newValue, item)
|
||||
}
|
||||
// Navigation variant handles clicks via href
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent) {
|
||||
const currentIndex = activeIndex >= 0 ? activeIndex : 0
|
||||
let newIndex = currentIndex
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowUp':
|
||||
event.preventDefault()
|
||||
newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1
|
||||
break
|
||||
case 'ArrowRight':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault()
|
||||
newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0
|
||||
break
|
||||
case 'Home':
|
||||
event.preventDefault()
|
||||
newIndex = 0
|
||||
break
|
||||
case 'End':
|
||||
event.preventDefault()
|
||||
newIndex = items.length - 1
|
||||
break
|
||||
case 'Enter':
|
||||
case ' ':
|
||||
if (variant === 'navigation' && items[currentIndex]?.href) {
|
||||
// Let the link handle navigation
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
if (items[currentIndex]) {
|
||||
handleItemClick(items[currentIndex], currentIndex)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (newIndex !== currentIndex && items[newIndex]) {
|
||||
if (variant === 'navigation' && items[newIndex].href) {
|
||||
// Focus the link
|
||||
itemElements[newIndex]?.focus()
|
||||
} else {
|
||||
handleItemClick(items[newIndex], newIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getPillColor(item: BaseItem): string {
|
||||
if (typeof pillColor === 'function') {
|
||||
return pillColor(item)
|
||||
}
|
||||
return pillColor
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
onMount(() => {
|
||||
const handleResize = () => updatePillPosition()
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
}
|
||||
})
|
||||
|
||||
// Size classes
|
||||
const sizeClasses = {
|
||||
small: 'segmented-controller-small',
|
||||
medium: 'segmented-controller-medium',
|
||||
large: 'segmented-controller-large'
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={containerElement}
|
||||
class="base-segmented-controller {sizeClasses[size]} {className}"
|
||||
class:full-width={fullWidth}
|
||||
role="tablist"
|
||||
style="--gap: {gap}px; --container-padding: {containerPadding}px;"
|
||||
onkeydown={handleKeyDown}
|
||||
>
|
||||
{#if showPill && activeIndex >= 0}
|
||||
<div
|
||||
class="segmented-pill"
|
||||
style="{pillStyle}; background-color: {getPillColor(items[activeIndex])};"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
{#each items as item, index}
|
||||
{@const isActive = index === activeIndex}
|
||||
{@const isHovered = index === hoveredIndex}
|
||||
|
||||
{#if variant === 'navigation' && item.href}
|
||||
<a
|
||||
bind:this={itemElements[index]}
|
||||
href={item.href}
|
||||
class="segmented-item"
|
||||
class:active={isActive}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
tabindex={isActive ? 0 : -1}
|
||||
onmouseenter={() => (hoveredIndex = index)}
|
||||
onmouseleave={() => (hoveredIndex = -1)}
|
||||
>
|
||||
{#if children}
|
||||
{@render children({ item, index, isActive, isHovered })}
|
||||
{:else}
|
||||
<span class="item-label">{item.label}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
bind:this={itemElements[index]}
|
||||
type="button"
|
||||
class="segmented-item"
|
||||
class:active={isActive}
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
tabindex={isActive ? 0 : -1}
|
||||
onclick={() => handleItemClick(item, index)}
|
||||
onmouseenter={() => (hoveredIndex = index)}
|
||||
onmouseleave={() => (hoveredIndex = -1)}
|
||||
>
|
||||
{#if children}
|
||||
{@render children({ item, index, isActive, isHovered })}
|
||||
{:else}
|
||||
<span class="item-label">{item.label}</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.base-segmented-controller {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--gap);
|
||||
padding: var(--container-padding);
|
||||
background-color: $gray-90;
|
||||
border-radius: $corner-radius-xl;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&.full-width {
|
||||
width: 100%;
|
||||
|
||||
.segmented-item {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.segmented-pill {
|
||||
position: absolute;
|
||||
top: var(--container-padding);
|
||||
bottom: var(--container-padding);
|
||||
background-color: white;
|
||||
border-radius: $corner-radius-lg;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: $shadow-sm;
|
||||
z-index: $z-index-base;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.segmented-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border-radius: $corner-radius-lg;
|
||||
transition: all 0.2s ease;
|
||||
z-index: $z-index-above;
|
||||
font-family: inherit;
|
||||
outline: none;
|
||||
|
||||
&:not(.active):hover {
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0 0 2px $blue-50;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $gray-10;
|
||||
|
||||
.item-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active) {
|
||||
color: $gray-50;
|
||||
|
||||
&:hover {
|
||||
color: $gray-30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-label {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
// Size variants
|
||||
.segmented-controller-small {
|
||||
.segmented-item {
|
||||
padding: $unit $unit-2x;
|
||||
font-size: 0.875rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.segmented-controller-medium {
|
||||
.segmented-item {
|
||||
padding: calc($unit + $unit-half) $unit-3x;
|
||||
font-size: 0.9375rem;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.segmented-controller-large {
|
||||
.segmented-item {
|
||||
padding: $unit-2x $unit-4x;
|
||||
font-size: 1rem;
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
// Animation states
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.segmented-pill,
|
||||
.segmented-item {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||
import type { Snippet } from 'svelte'
|
||||
|
||||
interface Props extends HTMLButtonAttributes {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'ghost' | 'text' | 'overlay' | 'danger-text'
|
||||
|
|
@ -12,6 +13,8 @@
|
|||
active?: boolean
|
||||
href?: string
|
||||
class?: string
|
||||
icon?: Snippet
|
||||
children?: Snippet
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -27,6 +30,7 @@
|
|||
type = 'button',
|
||||
href,
|
||||
class: className = '',
|
||||
icon,
|
||||
children,
|
||||
onclick,
|
||||
...restProps
|
||||
|
|
@ -60,8 +64,8 @@
|
|||
})
|
||||
|
||||
// Handle icon slot positioning
|
||||
const hasIcon = $derived(!!$$slots.icon)
|
||||
const hasDefaultSlot = $derived(!!$$slots.default)
|
||||
const hasIcon = $derived(!!icon)
|
||||
const hasDefaultSlot = $derived(!!children)
|
||||
const showSpinner = $derived(loading && !iconOnly)
|
||||
</script>
|
||||
|
||||
|
|
@ -94,21 +98,21 @@
|
|||
|
||||
{#if hasIcon && iconPosition === 'left' && !iconOnly}
|
||||
<span class="btn-icon-wrapper">
|
||||
<slot name="icon" />
|
||||
{@render icon()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if hasDefaultSlot && !iconOnly}
|
||||
<span class="btn-label">
|
||||
<slot />
|
||||
{@render children()}
|
||||
</span>
|
||||
{:else if iconOnly && hasIcon}
|
||||
<slot name="icon" />
|
||||
{@render icon()}
|
||||
{/if}
|
||||
|
||||
{#if hasIcon && iconPosition === 'right' && !iconOnly}
|
||||
<span class="btn-icon-wrapper">
|
||||
<slot name="icon" />
|
||||
{@render icon()}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
|
|
@ -141,21 +145,21 @@
|
|||
|
||||
{#if hasIcon && iconPosition === 'left' && !iconOnly}
|
||||
<span class="btn-icon-wrapper">
|
||||
<slot name="icon" />
|
||||
{@render icon()}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if hasDefaultSlot && !iconOnly}
|
||||
<span class="btn-label">
|
||||
<slot />
|
||||
{@render children()}
|
||||
</span>
|
||||
{:else if iconOnly && hasIcon}
|
||||
<slot name="icon" />
|
||||
{@render icon()}
|
||||
{/if}
|
||||
|
||||
{#if hasIcon && iconPosition === 'right' && !iconOnly}
|
||||
<span class="btn-icon-wrapper">
|
||||
<slot name="icon" />
|
||||
{@render icon()}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
|
@ -174,7 +178,7 @@
|
|||
font-weight: 400;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
transition: all $transition-fast ease;
|
||||
outline: none;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
|
|
@ -198,73 +202,73 @@
|
|||
|
||||
// Ensure consistent styling for both button and anchor elements
|
||||
&:focus {
|
||||
outline: 2px solid rgba(59, 130, 246, 0.5);
|
||||
outline-offset: 2px;
|
||||
outline: $unit-2px solid rgba(59, 130, 246, 0.5);
|
||||
outline-offset: $unit-2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Size variations
|
||||
.btn-small {
|
||||
padding: $unit calc($unit * 1.5);
|
||||
font-size: 13px;
|
||||
border-radius: 20px;
|
||||
min-height: 28px;
|
||||
font-size: 0.8125rem; // 13px
|
||||
border-radius: $unit-20px;
|
||||
min-height: $unit-3x + $unit-half;
|
||||
}
|
||||
|
||||
.btn-medium {
|
||||
padding: ($unit * 1.5) $unit-2x;
|
||||
font-size: 14px;
|
||||
border-radius: 24px;
|
||||
min-height: 36px;
|
||||
font-size: $unit-14px;
|
||||
border-radius: $unit-3x;
|
||||
min-height: $unit-4x + $unit-half;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
padding: calc($unit * 1.5) $unit-3x;
|
||||
font-size: 15px;
|
||||
border-radius: 28px;
|
||||
min-height: 44px;
|
||||
font-size: 0.9375rem; // 15px
|
||||
border-radius: $unit-3x + $unit-half;
|
||||
min-height: $unit-5x + $unit-half;
|
||||
}
|
||||
|
||||
// Square corners variant
|
||||
.btn-square {
|
||||
&.btn-small {
|
||||
border-radius: 6px;
|
||||
border-radius: $corner-radius-sm;
|
||||
}
|
||||
&.btn-medium {
|
||||
border-radius: 8px;
|
||||
border-radius: $corner-radius-md;
|
||||
}
|
||||
&.btn-large {
|
||||
border-radius: 10px;
|
||||
border-radius: $corner-radius-lg;
|
||||
}
|
||||
}
|
||||
|
||||
// Icon-only button styles
|
||||
.btn-icon {
|
||||
padding: 0;
|
||||
border-radius: 8px;
|
||||
border-radius: $corner-radius-md;
|
||||
|
||||
&.btn-icon-small {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
width: $unit-3x + $unit-half;
|
||||
height: $unit-3x + $unit-half;
|
||||
border-radius: $corner-radius-sm;
|
||||
}
|
||||
|
||||
&.btn-icon-medium {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
width: $unit-4x + $unit-2px;
|
||||
height: $unit-4x + $unit-2px;
|
||||
}
|
||||
|
||||
&.btn-icon-large {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
width: $unit-5x + $unit-half;
|
||||
height: $unit-5x + $unit-half;
|
||||
border-radius: $corner-radius-lg;
|
||||
}
|
||||
|
||||
&.btn-icon-icon {
|
||||
// For circular icon buttons
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 17px;
|
||||
width: $unit-4x + $unit-2px;
|
||||
height: $unit-4x + $unit-2px;
|
||||
border-radius: ($unit-4x + $unit-2px) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -283,17 +287,17 @@
|
|||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: $grey-10;
|
||||
color: $grey-80;
|
||||
border: 1px solid $grey-20;
|
||||
background-color: $gray-10;
|
||||
color: $gray-80;
|
||||
border: $unit-1px solid $gray-20;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-20;
|
||||
border-color: $grey-30;
|
||||
background-color: $gray-20;
|
||||
border-color: $gray-30;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $grey-30;
|
||||
background-color: $gray-30;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -312,35 +316,35 @@
|
|||
|
||||
.btn-ghost {
|
||||
background-color: transparent;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-5;
|
||||
color: $grey-00;
|
||||
background-color: $gray-5;
|
||||
color: $gray-00;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $grey-10;
|
||||
background-color: $gray-10;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $grey-10;
|
||||
color: $grey-00;
|
||||
background-color: $gray-10;
|
||||
color: $gray-00;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: none;
|
||||
color: $grey-40;
|
||||
color: $gray-40;
|
||||
padding: $unit;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: $grey-20;
|
||||
background-color: $grey-5;
|
||||
color: $gray-20;
|
||||
background-color: $gray-5;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
color: $grey-00;
|
||||
color: $gray-00;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -351,31 +355,31 @@
|
|||
font-weight: 600;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-90;
|
||||
background-color: $gray-90;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $grey-80;
|
||||
background-color: $gray-80;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-overlay {
|
||||
background-color: white;
|
||||
color: $grey-20;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
color: $gray-20;
|
||||
border: $unit-1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 $unit-2px $unit-half rgba(0, 0, 0, 0.1);
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-5;
|
||||
color: $grey-00;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
||||
background-color: $gray-5;
|
||||
color: $gray-00;
|
||||
box-shadow: 0 $unit-half $unit rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $grey-10;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
background-color: $gray-10;
|
||||
box-shadow: 0 $unit-1px $unit-2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,166 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Editor from './Editor.svelte'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
interface Props {
|
||||
data?: JSONContent
|
||||
onChange?: (content: JSONContent) => void
|
||||
placeholder?: string
|
||||
minHeight?: number
|
||||
autofocus?: boolean
|
||||
mode?: 'default' | 'inline'
|
||||
showToolbar?: boolean
|
||||
class?: string
|
||||
}
|
||||
|
||||
let {
|
||||
data = $bindable(),
|
||||
onChange = () => {},
|
||||
placeholder = 'Write your content here...',
|
||||
minHeight = 400,
|
||||
autofocus = false,
|
||||
mode = 'default',
|
||||
showToolbar = true,
|
||||
class: className = ''
|
||||
}: Props = $props()
|
||||
|
||||
let editorRef: Editor | undefined = $state()
|
||||
|
||||
// Forward editor methods if needed
|
||||
export function focus() {
|
||||
editorRef?.focus()
|
||||
}
|
||||
|
||||
export function blur() {
|
||||
editorRef?.blur()
|
||||
}
|
||||
|
||||
export function getContent() {
|
||||
return editorRef?.getContent()
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
editorRef?.clear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={`case-study-editor-wrapper ${mode} ${className}`}>
|
||||
<Editor
|
||||
bind:this={editorRef}
|
||||
bind:data
|
||||
{onChange}
|
||||
{placeholder}
|
||||
{minHeight}
|
||||
{autofocus}
|
||||
{showToolbar}
|
||||
class="case-study-editor"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$styles/variables.scss';
|
||||
|
||||
.case-study-editor-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Default mode - used in ProjectForm */
|
||||
.case-study-editor-wrapper.default {
|
||||
:global(.case-study-editor) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .edra) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .editor-toolbar) {
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.12);
|
||||
background: $grey-95;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .edra-editor) {
|
||||
padding: 0 $unit-4x;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror) {
|
||||
min-height: calc(100% - 80px);
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror:focus) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror > * + *) {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror p.is-editor-empty:first-child::before) {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline mode - used in UniverseComposer */
|
||||
.case-study-editor-wrapper.inline {
|
||||
:global(.case-study-editor) {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .edra-editor) {
|
||||
padding: $unit-2x 0;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .editor-container) {
|
||||
padding: 0 $unit-3x;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .editor-content) {
|
||||
padding: 0;
|
||||
min-height: 80px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror) {
|
||||
padding: 0;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror:focus) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:global(.case-study-editor .ProseMirror p) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:global(
|
||||
.case-study-editor .ProseMirror.ProseMirror-focused .is-editor-empty:first-child::before
|
||||
) {
|
||||
color: $grey-40;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import BaseModal from './BaseModal.svelte'
|
||||
import Button from './Button.svelte'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -23,75 +24,53 @@
|
|||
|
||||
function handleConfirm() {
|
||||
onConfirm()
|
||||
isOpen = false
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
isOpen = false
|
||||
onCancel?.()
|
||||
}
|
||||
|
||||
function handleBackdropClick() {
|
||||
isOpen = false
|
||||
onCancel?.()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="modal-backdrop" onclick={handleBackdropClick}>
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()}>
|
||||
<h2>{title}</h2>
|
||||
<p>{message}</p>
|
||||
<div class="modal-actions">
|
||||
<Button variant="secondary" onclick={handleCancel}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button variant="danger" onclick={handleConfirm}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
<BaseModal bind:isOpen size="small" onClose={handleCancel} class="delete-confirmation-modal">
|
||||
<div class="modal-body">
|
||||
<h2>{title}</h2>
|
||||
<p>{message}</p>
|
||||
<div class="modal-actions">
|
||||
<Button variant="secondary" onclick={handleCancel}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button variant="danger" onclick={handleConfirm}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</BaseModal>
|
||||
|
||||
<style lang="scss">
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1050;
|
||||
}
|
||||
:global(.delete-confirmation-modal) {
|
||||
.modal-body {
|
||||
padding: $unit-4x;
|
||||
|
||||
.modal {
|
||||
background: white;
|
||||
border-radius: $unit-2x;
|
||||
padding: $unit-4x;
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||
h2 {
|
||||
margin: 0 0 $unit-2x;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: $gray-10;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 $unit-2x;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: $grey-10;
|
||||
p {
|
||||
margin: 0 0 $unit-4x;
|
||||
color: $gray-20;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 $unit-4x;
|
||||
color: $grey-20;
|
||||
line-height: 1.5;
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -34,12 +34,12 @@
|
|||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
|
|
|
|||
|
|
@ -1,86 +1,210 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import { browser } from '$app/environment'
|
||||
import { computePosition, flip, shift, offset, autoUpdate } from '@floating-ui/dom'
|
||||
import ChevronRight from '$icons/chevron-right.svg?component'
|
||||
|
||||
interface Props {
|
||||
isOpen: boolean
|
||||
triggerElement?: HTMLElement
|
||||
items: DropdownItem[]
|
||||
onClose?: () => void
|
||||
isSubmenu?: boolean
|
||||
}
|
||||
|
||||
interface DropdownItem {
|
||||
id: string
|
||||
label: string
|
||||
action: () => void
|
||||
label?: string
|
||||
action?: () => void
|
||||
variant?: 'default' | 'danger'
|
||||
divider?: boolean
|
||||
children?: DropdownItem[]
|
||||
icon?: string
|
||||
}
|
||||
|
||||
let { isOpen = $bindable(), triggerElement, items, onClose }: Props = $props()
|
||||
let { isOpen = $bindable(), triggerElement, items, onClose, isSubmenu = false }: Props = $props()
|
||||
|
||||
let dropdownElement: HTMLDivElement
|
||||
let cleanup: (() => void) | null = null
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
// Calculate position dynamically when needed
|
||||
const position = $derived(() => {
|
||||
if (!isOpen || !triggerElement || !browser) {
|
||||
return { top: 0, left: 0 }
|
||||
}
|
||||
// Track which submenu is open
|
||||
let openSubmenuId = $state<string | null>(null)
|
||||
let submenuElements = $state<Map<string, HTMLElement>>(new Map())
|
||||
let submenuCloseTimeout: number | null = null
|
||||
|
||||
const rect = triggerElement.getBoundingClientRect()
|
||||
const dropdownWidth = 180
|
||||
// Position state
|
||||
let x = $state(0)
|
||||
let y = $state(0)
|
||||
|
||||
// Action to set submenu references
|
||||
function submenuRef(
|
||||
node: HTMLElement,
|
||||
params: { item: DropdownItem; submenuElements: Map<string, HTMLElement> }
|
||||
) {
|
||||
if (params.item.children) {
|
||||
params.submenuElements.set(params.item.id, node)
|
||||
}
|
||||
|
||||
return {
|
||||
top: rect.bottom + 4,
|
||||
left: rect.right - dropdownWidth
|
||||
destroy() {
|
||||
if (params.item.children) {
|
||||
params.submenuElements.delete(params.item.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Update position using Floating UI
|
||||
async function updatePosition() {
|
||||
if (!triggerElement || !dropdownElement) return
|
||||
|
||||
const { x: newX, y: newY } = await computePosition(triggerElement, dropdownElement, {
|
||||
placement: isSubmenu ? 'right-start' : 'bottom-end',
|
||||
middleware: [offset(isSubmenu ? 0 : 4), flip(), shift({ padding: 8 })]
|
||||
})
|
||||
|
||||
x = newX
|
||||
y = newY
|
||||
}
|
||||
|
||||
function handleItemClick(item: DropdownItem, event: MouseEvent) {
|
||||
event.stopPropagation()
|
||||
item.action()
|
||||
isOpen = false
|
||||
onClose?.()
|
||||
if (item.action && !item.children) {
|
||||
item.action()
|
||||
isOpen = false
|
||||
openSubmenuId = null // Reset submenu state
|
||||
onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
function handleOutsideClick(event: MouseEvent) {
|
||||
if (!dropdownElement || !isOpen) return
|
||||
|
||||
const target = event.target as HTMLElement
|
||||
if (!dropdownElement.contains(target) && !triggerElement?.contains(target)) {
|
||||
// Check if click is inside any submenu
|
||||
const clickedInSubmenu = Array.from(submenuElements.values()).some((el) => el.contains(target))
|
||||
|
||||
if (
|
||||
!dropdownElement.contains(target) &&
|
||||
!triggerElement?.contains(target) &&
|
||||
!clickedInSubmenu
|
||||
) {
|
||||
isOpen = false
|
||||
openSubmenuId = null // Reset submenu state
|
||||
onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemMouseEnter(item: DropdownItem) {
|
||||
if (submenuCloseTimeout) {
|
||||
clearTimeout(submenuCloseTimeout)
|
||||
submenuCloseTimeout = null
|
||||
}
|
||||
|
||||
if (item.children) {
|
||||
openSubmenuId = item.id
|
||||
} else {
|
||||
openSubmenuId = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleItemMouseLeave(item: DropdownItem) {
|
||||
if (item.children) {
|
||||
// Add delay before closing submenu
|
||||
submenuCloseTimeout = window.setTimeout(() => {
|
||||
if (openSubmenuId === item.id) {
|
||||
openSubmenuId = null
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmenuMouseEnter() {
|
||||
if (submenuCloseTimeout) {
|
||||
clearTimeout(submenuCloseTimeout)
|
||||
submenuCloseTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmenuMouseLeave(itemId: string) {
|
||||
submenuCloseTimeout = window.setTimeout(() => {
|
||||
if (openSubmenuId === itemId) {
|
||||
openSubmenuId = null
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// Set up auto-update for position when dropdown is open
|
||||
$effect(() => {
|
||||
if (browser && isOpen) {
|
||||
if (browser && isOpen && triggerElement && dropdownElement) {
|
||||
// Initial position update
|
||||
updatePosition()
|
||||
|
||||
// Set up auto-update
|
||||
cleanup = autoUpdate(triggerElement, dropdownElement, updatePosition)
|
||||
|
||||
// Add outside click listener
|
||||
document.addEventListener('click', handleOutsideClick)
|
||||
|
||||
return () => {
|
||||
cleanup?.()
|
||||
cleanup = null
|
||||
document.removeEventListener('click', handleOutsideClick)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Reset submenu state when dropdown closes
|
||||
$effect(() => {
|
||||
if (!isOpen) {
|
||||
openSubmenuId = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if isOpen && browser}
|
||||
<div
|
||||
bind:this={dropdownElement}
|
||||
class="dropdown-menu"
|
||||
style="top: {position().top}px; left: {position().left}px"
|
||||
class:submenu={isSubmenu}
|
||||
style="position: fixed; left: {x}px; top: {y}px"
|
||||
>
|
||||
{#each items as item}
|
||||
{#if item.divider}
|
||||
<div class="dropdown-divider"></div>
|
||||
{:else}
|
||||
<button
|
||||
use:submenuRef={{ item, submenuElements }}
|
||||
class="dropdown-item"
|
||||
class:danger={item.variant === 'danger'}
|
||||
class:has-children={item.children}
|
||||
onclick={(e) => handleItemClick(item, e)}
|
||||
onmouseenter={() => handleItemMouseEnter(item)}
|
||||
onmouseleave={() => handleItemMouseLeave(item)}
|
||||
>
|
||||
{item.label}
|
||||
<span class="item-label">{item.label}</span>
|
||||
{#if item.children}
|
||||
<span class="submenu-icon">
|
||||
<ChevronRight />
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if item.children && openSubmenuId === item.id}
|
||||
<div
|
||||
onmouseenter={handleSubmenuMouseEnter}
|
||||
onmouseleave={() => handleSubmenuMouseLeave(item.id)}
|
||||
>
|
||||
<svelte:self
|
||||
isOpen={true}
|
||||
triggerElement={submenuElements.get(item.id)}
|
||||
items={item.children}
|
||||
{onClose}
|
||||
isSubmenu={true}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -90,14 +214,15 @@
|
|||
@import '$styles/variables.scss';
|
||||
|
||||
.dropdown-menu {
|
||||
position: fixed;
|
||||
background: white;
|
||||
border: 1px solid $grey-85;
|
||||
border: 1px solid $gray-85;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-width: 180px;
|
||||
z-index: 1050;
|
||||
z-index: $z-index-modal;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
|
|
@ -107,23 +232,57 @@
|
|||
border: none;
|
||||
text-align: left;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
display: block;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background-color: $grey-95;
|
||||
background-color: $gray-95;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
color: $red-60;
|
||||
}
|
||||
|
||||
&.has-children {
|
||||
padding-right: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.item-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.submenu-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-left: $unit;
|
||||
color: $gray-40;
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
:global(svg) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: none;
|
||||
}
|
||||
|
||||
:global(path) {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
height: 1px;
|
||||
background-color: $grey-80;
|
||||
background-color: $gray-80;
|
||||
margin: $unit-half 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@
|
|||
top: calc(100% + $unit-half);
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 1px solid $grey-85;
|
||||
border: 1px solid $gray-85;
|
||||
border-radius: $unit;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
min-width: 180px;
|
||||
z-index: 1050;
|
||||
z-index: $z-index-modal;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,446 +0,0 @@
|
|||
<script lang="ts">
|
||||
import EditorWithUpload from './EditorWithUpload.svelte'
|
||||
import type { Editor } from '@tiptap/core'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
interface Props {
|
||||
data?: JSONContent
|
||||
onChange?: (data: JSONContent) => void
|
||||
placeholder?: string
|
||||
readOnly?: boolean
|
||||
minHeight?: number
|
||||
autofocus?: boolean
|
||||
class?: string
|
||||
showToolbar?: boolean
|
||||
simpleMode?: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
data = $bindable({
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph' }]
|
||||
}),
|
||||
onChange,
|
||||
placeholder = 'Type "/" for commands...',
|
||||
readOnly = false,
|
||||
minHeight = 400,
|
||||
autofocus = false,
|
||||
class: className = '',
|
||||
showToolbar = true,
|
||||
simpleMode = false
|
||||
}: Props = $props()
|
||||
|
||||
let editor = $state<Editor | undefined>()
|
||||
let initialized = false
|
||||
|
||||
// Update content when editor changes
|
||||
function onUpdate(props: { editor: Editor }) {
|
||||
// Skip the first update to avoid circular updates
|
||||
if (!initialized) {
|
||||
initialized = true
|
||||
return
|
||||
}
|
||||
|
||||
const json = props.editor.getJSON()
|
||||
data = json
|
||||
onChange?.(json)
|
||||
}
|
||||
|
||||
// Public API
|
||||
export function save(): JSONContent | null {
|
||||
return editor?.getJSON() || null
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
editor?.commands.clearContent()
|
||||
}
|
||||
|
||||
export function focus() {
|
||||
editor?.commands.focus()
|
||||
}
|
||||
|
||||
export function getIsDirty(): boolean {
|
||||
// This would need to track changes since last save
|
||||
return false
|
||||
}
|
||||
|
||||
// Focus on mount if requested
|
||||
$effect(() => {
|
||||
if (editor && autofocus) {
|
||||
// Only focus once on initial mount
|
||||
const timer = setTimeout(() => {
|
||||
editor.commands.focus()
|
||||
}, 100)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="editor-wrapper {className}" style="--min-height: {minHeight}px">
|
||||
<div class="editor-container">
|
||||
<EditorWithUpload
|
||||
bind:editor
|
||||
content={data}
|
||||
{onUpdate}
|
||||
editable={!readOnly}
|
||||
showToolbar={!simpleMode && showToolbar}
|
||||
{placeholder}
|
||||
showSlashCommands={!simpleMode}
|
||||
showLinkBubbleMenu={!simpleMode}
|
||||
showTableBubbleMenu={false}
|
||||
class="editor-content"
|
||||
onEditorReady={(e) => {
|
||||
editor = e
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@import '$styles/variables.scss';
|
||||
@import '$styles/mixins.scss';
|
||||
|
||||
.editor-wrapper {
|
||||
width: 100%;
|
||||
min-height: var(--min-height);
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(.editor-content) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.editor-content .edra) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:global(.editor-content .editor-toolbar) {
|
||||
border-radius: $corner-radius-full;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 0 16px rgba(0, 0, 0, 0.12);
|
||||
box-sizing: border-box;
|
||||
background: $grey-95;
|
||||
padding: $unit $unit-2x;
|
||||
position: sticky;
|
||||
z-index: 10;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// Hide scrollbar but keep functionality
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// Override Edra toolbar styles
|
||||
:global(.edra-toolbar) {
|
||||
overflow: visible;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $unit;
|
||||
}
|
||||
}
|
||||
|
||||
// Override Edra styles to match our design
|
||||
:global(.edra-editor) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 $unit-4x;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
padding: $unit-3x;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror) {
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
color: $grey-10;
|
||||
min-height: 100%;
|
||||
padding-bottom: 30vh; // Give space for scrolling
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror h1) {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin: $unit-3x 0 $unit-2x;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror h2) {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
margin: $unit-3x 0 $unit-2x;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror h3) {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: $unit-3x 0 $unit-2x;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror p) {
|
||||
margin: $unit-2x 0;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror ul),
|
||||
:global(.edra .ProseMirror ol) {
|
||||
padding-left: $unit-4x;
|
||||
margin: $unit-2x 0;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror li) {
|
||||
margin: $unit 0;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror blockquote) {
|
||||
border-left: 3px solid $grey-80;
|
||||
margin: $unit-3x 0;
|
||||
padding-left: $unit-3x;
|
||||
font-style: italic;
|
||||
color: $grey-30;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror pre) {
|
||||
background: $grey-95;
|
||||
border: 1px solid $grey-80;
|
||||
border-radius: 4px;
|
||||
color: $grey-10;
|
||||
font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
margin: $unit-2x 0;
|
||||
padding: $unit-2x;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror code) {
|
||||
background: $grey-90;
|
||||
border-radius: 0.25rem;
|
||||
color: $grey-10;
|
||||
font-family: 'SF Mono', Monaco, Consolas, 'Courier New', monospace;
|
||||
font-size: 0.875em;
|
||||
padding: 0.125rem 0.25rem;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror hr) {
|
||||
border: none;
|
||||
border-top: 1px solid $grey-80;
|
||||
margin: $unit-4x 0;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror a) {
|
||||
color: #3b82f6;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror a:hover) {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror ::selection) {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
}
|
||||
|
||||
// Placeholder
|
||||
:global(.edra .ProseMirror p.is-editor-empty:first-child::before) {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
// Focus styles
|
||||
:global(.edra .ProseMirror.ProseMirror-focused) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
// Loading state
|
||||
:global(.edra-loading) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: var(--min-height);
|
||||
color: $grey-50;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
// Image styles
|
||||
:global(.edra .ProseMirror img) {
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
max-height: 400px;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
margin: $unit-2x auto;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
:global(.edra .ProseMirror .edra-url-embed-image img) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: auto;
|
||||
max-height: auto;
|
||||
margin: 0;
|
||||
object-fit: cover;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
:global(.edra-media-placeholder-wrapper) {
|
||||
margin: $unit-2x 0;
|
||||
}
|
||||
|
||||
:global(.edra-media-placeholder-content) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: $unit;
|
||||
padding: $unit-4x;
|
||||
border: 2px dashed $grey-80;
|
||||
border-radius: 8px;
|
||||
background: $grey-95;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-60;
|
||||
background: $grey-90;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.edra-media-placeholder-icon) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: $grey-50;
|
||||
}
|
||||
|
||||
:global(.edra-media-placeholder-text) {
|
||||
font-size: 1rem;
|
||||
color: $grey-30;
|
||||
}
|
||||
|
||||
// Image container styles
|
||||
:global(.edra-media-container) {
|
||||
margin: $unit-3x auto;
|
||||
position: relative;
|
||||
|
||||
&.align-left {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&.align-right {
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// URL Embed styles - ensure proper isolation
|
||||
:global(.edra .edra-url-embed-wrapper) {
|
||||
margin: $unit-3x 0;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
:global(.edra .edra-url-embed-card) {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
:global(.edra .edra-url-embed-content) {
|
||||
background: $grey-95;
|
||||
border: 1px solid $grey-85;
|
||||
|
||||
&:hover {
|
||||
border-color: $grey-60;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.edra .edra-url-embed-title) {
|
||||
color: $grey-10;
|
||||
font-family: inherit;
|
||||
margin: 0 !important; // Override ProseMirror h3 margins
|
||||
font-size: 1rem !important;
|
||||
font-weight: 600 !important;
|
||||
line-height: 1.3 !important;
|
||||
}
|
||||
|
||||
:global(.edra .edra-url-embed-description) {
|
||||
color: $grey-30;
|
||||
font-family: inherit;
|
||||
margin: 0 !important; // Override any inherited margins
|
||||
}
|
||||
|
||||
:global(.edra .edra-url-embed-meta) {
|
||||
color: $grey-40;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
// Override ProseMirror img styles for favicons only
|
||||
:global(.edra .ProseMirror .edra-url-embed-favicon) {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
margin: 0 !important; // Remove auto margins
|
||||
display: inline-block !important;
|
||||
max-width: 16px !important;
|
||||
max-height: 16px !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
:global(.edra-media-content) {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(.edra-media-caption) {
|
||||
width: 100%;
|
||||
margin-top: $unit;
|
||||
padding: $unit $unit-2x;
|
||||
border: 1px solid $grey-80;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-30;
|
||||
background: $grey-95;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $grey-60;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,6 +5,7 @@
|
|||
import Editor from './Editor.svelte'
|
||||
import Button from './Button.svelte'
|
||||
import Input from './Input.svelte'
|
||||
import { toast } from '$lib/stores/toast'
|
||||
import type { JSONContent } from '@tiptap/core'
|
||||
|
||||
interface Props {
|
||||
|
|
@ -24,8 +25,6 @@
|
|||
// State
|
||||
let isLoading = $state(false)
|
||||
let isSaving = $state(false)
|
||||
let error = $state('')
|
||||
let successMessage = $state('')
|
||||
let activeTab = $state('metadata')
|
||||
let showPublishMenu = $state(false)
|
||||
|
||||
|
|
@ -80,14 +79,14 @@
|
|||
}
|
||||
|
||||
if (!title) {
|
||||
error = 'Title is required'
|
||||
toast.error('Title is required')
|
||||
return
|
||||
}
|
||||
|
||||
const loadingToastId = toast.loading(`${mode === 'edit' ? 'Saving' : 'Creating'} essay...`)
|
||||
|
||||
try {
|
||||
isSaving = true
|
||||
error = ''
|
||||
successMessage = ''
|
||||
|
||||
const auth = localStorage.getItem('admin_auth')
|
||||
if (!auth) {
|
||||
|
|
@ -121,16 +120,16 @@
|
|||
}
|
||||
|
||||
const savedPost = await response.json()
|
||||
successMessage = `Essay ${mode === 'edit' ? 'saved' : 'created'} successfully!`
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage = ''
|
||||
if (mode === 'create') {
|
||||
goto(`/admin/posts/${savedPost.id}/edit`)
|
||||
}
|
||||
}, 1500)
|
||||
toast.dismiss(loadingToastId)
|
||||
toast.success(`Essay ${mode === 'edit' ? 'saved' : 'created'} successfully!`)
|
||||
|
||||
if (mode === 'create') {
|
||||
goto(`/admin/posts/${savedPost.id}/edit`)
|
||||
}
|
||||
} catch (err) {
|
||||
error = `Failed to ${mode === 'edit' ? 'save' : 'create'} essay`
|
||||
toast.dismiss(loadingToastId)
|
||||
toast.error(`Failed to ${mode === 'edit' ? 'save' : 'create'} essay`)
|
||||
console.error(err)
|
||||
} finally {
|
||||
isSaving = false
|
||||
|
|
@ -196,7 +195,7 @@
|
|||
<div class="header-actions">
|
||||
<div class="save-actions">
|
||||
<Button variant="primary" onclick={handleSave} disabled={isSaving} class="save-button">
|
||||
{isSaving ? 'Saving...' : status === 'published' ? 'Save' : 'Save Draft'}
|
||||
{status === 'published' ? 'Save' : 'Save Draft'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
|
|
@ -369,14 +368,14 @@
|
|||
|
||||
// Custom styles for save/publish buttons to maintain grey color scheme
|
||||
:global(.save-button.btn-primary) {
|
||||
background-color: $grey-10;
|
||||
background-color: $gray-10;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-20;
|
||||
background-color: $gray-20;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $grey-30;
|
||||
background-color: $gray-30;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,18 +386,18 @@
|
|||
}
|
||||
|
||||
:global(.chevron-button.btn-primary) {
|
||||
background-color: $grey-10;
|
||||
background-color: $gray-10;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: $grey-20;
|
||||
background-color: $gray-20;
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
background-color: $grey-30;
|
||||
background-color: $gray-30;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background-color: $grey-20;
|
||||
background-color: $gray-20;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -499,7 +498,7 @@
|
|||
margin-bottom: $unit;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -524,10 +523,10 @@
|
|||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: $unit $unit-2x;
|
||||
background: $grey-90;
|
||||
background: $gray-90;
|
||||
border-radius: 20px;
|
||||
font-size: 0.875rem;
|
||||
color: $grey-20;
|
||||
color: $gray-20;
|
||||
|
||||
:global(.btn) {
|
||||
margin-left: 4px;
|
||||
|
|
|
|||