feat: add branding preview with visibility toggles
Add live preview to branding form showing featured image, background color, and logo. Add database fields and toggles to control visibility of each element in project headers.
This commit is contained in:
parent
1190bfc62e
commit
12d2ba1667
6 changed files with 376 additions and 10 deletions
194
docs/branding-preview-feature.md
Normal file
194
docs/branding-preview-feature.md
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
# Project Branding Preview Enhancement
|
||||
|
||||
## Overview
|
||||
Add a live, reactive preview unit to the Branding tab showing how the project header will appear on the public site, with visibility toggles for individual branding elements.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Database & Type Updates
|
||||
|
||||
### 1.1 Database Schema Changes
|
||||
**File**: Prisma schema
|
||||
- Add new optional boolean fields to Project model:
|
||||
- `showFeaturedImageInHeader` (default: true)
|
||||
- `showBackgroundColorInHeader` (default: true)
|
||||
- `showLogoInHeader` (default: true)
|
||||
|
||||
### 1.2 Type Definition Updates
|
||||
**File**: `/src/lib/types/project.ts`
|
||||
- Add new fields to `Project` interface
|
||||
- Add new fields to `ProjectFormData` interface
|
||||
- Update `defaultProjectFormData` with default values (all true)
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Create Preview Component
|
||||
|
||||
### 2.1 New Component: ProjectBrandingPreview.svelte
|
||||
**Location**: `/src/lib/components/admin/ProjectBrandingPreview.svelte`
|
||||
|
||||
**Features**:
|
||||
- Full-width container (respects parent padding)
|
||||
- 300px height (matches public project header)
|
||||
- Responsive height (250px on tablet, 200px on mobile)
|
||||
- Display priority: featuredImage > backgroundColor > fallback gray (#f5f5f5)
|
||||
- Logo centered vertically and horizontally (85px x 85px)
|
||||
- Fallback placeholder logo when no logo provided
|
||||
- Reactive to all formData changes (featuredImage, backgroundColor, logoUrl)
|
||||
- Conditional rendering based on visibility toggles
|
||||
- Corner radius matching public site ($card-corner-radius)
|
||||
- Subtle mouse-tracking animation on logo (optional, matches public site)
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface Props {
|
||||
featuredImage: string | null
|
||||
backgroundColor: string
|
||||
logoUrl: string
|
||||
showFeaturedImage: boolean
|
||||
showBackgroundColor: boolean
|
||||
showLogo: boolean
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Visual States to Handle:
|
||||
1. **No data**: Gray background + placeholder icon
|
||||
2. **Logo only**: Show logo on fallback background
|
||||
3. **Color only**: Show color background without logo
|
||||
4. **Featured image only**: Show image without logo
|
||||
5. **All elements**: Featured image (or color) + logo
|
||||
6. **Featured image + color**: Featured image takes priority, color ignored
|
||||
7. **Visibility toggles**: Respect all toggle states
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Update ProjectBrandingForm
|
||||
|
||||
### 3.1 Form Restructure
|
||||
**File**: `/src/lib/components/admin/ProjectBrandingForm.svelte`
|
||||
|
||||
**New Layout Order**:
|
||||
1. **Preview Section** (top, unlabeled)
|
||||
- ProjectBrandingPreview component
|
||||
- Bound to all reactive form data
|
||||
|
||||
2. **Background Section**
|
||||
- Featured Image uploader (keep existing)
|
||||
- Background Color picker (keep existing)
|
||||
- Toggle: "Show featured image in header"
|
||||
- Toggle: "Show background color in header" (only visible if no featured image, or featured image toggle is off)
|
||||
- Help text: "Featured image takes priority over background color"
|
||||
|
||||
3. **Logo Section**
|
||||
- Logo uploader (keep existing)
|
||||
- Toggle: "Show logo in header"
|
||||
- Help text: "Upload an SVG logo that appears centered over the header background"
|
||||
|
||||
4. **Colors Section**
|
||||
- Highlight Color picker (keep existing)
|
||||
|
||||
### 3.2 Toggle Component Pattern
|
||||
Use existing toggle pattern from AlbumForm.svelte:
|
||||
```svelte
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" bind:checked={formData.showLogoInHeader} class="toggle-input" />
|
||||
<div class="toggle-content">
|
||||
<span class="toggle-title">Show logo in header</span>
|
||||
<span class="toggle-description">Display the project logo centered over the header</span>
|
||||
</div>
|
||||
<span class="toggle-slider"></span>
|
||||
</label>
|
||||
```
|
||||
|
||||
### 3.3 Bind FormData Fields
|
||||
- Add bindings for new toggle fields
|
||||
- Ensure auto-save triggers on toggle changes
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Additional Enhancements (Suggestions)
|
||||
|
||||
### 4.1 Preview Mode Selector
|
||||
Add segmented control to preview component header:
|
||||
- **Header View** (default): 300px tall, logo centered
|
||||
- **Card View**: 80px tall, matches ProjectItem card style
|
||||
- Shows how branding appears in different contexts
|
||||
|
||||
### 4.2 Background Priority Explanation
|
||||
Add info callout:
|
||||
- "When both featured image and background color are provided, the featured image will be used in the header"
|
||||
- Consider adding radio buttons for explicit priority selection
|
||||
|
||||
### 4.3 Logo Adjustments
|
||||
Add additional controls (future enhancement):
|
||||
- Logo size slider (small/medium/large)
|
||||
- Logo position selector (center/top-left/top-right/bottom-center)
|
||||
- Logo background blur/darken overlay toggle (for better logo visibility)
|
||||
|
||||
### 4.4 Smart Defaults
|
||||
- Auto-enable toggles when user uploads/adds content
|
||||
- Auto-disable toggles when user removes content
|
||||
- Show warning if logo would be invisible (e.g., white logo on white background)
|
||||
|
||||
### 4.5 Accessibility Improvements
|
||||
- Add alt text field for featured image in preview
|
||||
- Logo contrast checker against background
|
||||
- ARIA labels for preview container
|
||||
|
||||
### 4.6 Layout Improvements
|
||||
Add section dividers with subtle borders between:
|
||||
- Preview (unlabeled, visual-only)
|
||||
- Background settings
|
||||
- Logo settings
|
||||
- Color settings
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Database & Types
|
||||
- [ ] Add schema fields: `showFeaturedImageInHeader`, `showBackgroundColorInHeader`, `showLogoInHeader`
|
||||
- [ ] Run migration
|
||||
- [ ] Update Project type interface
|
||||
- [ ] Update ProjectFormData type interface
|
||||
- [ ] Update defaultProjectFormData with defaults
|
||||
|
||||
### Components
|
||||
- [ ] Create ProjectBrandingPreview.svelte component
|
||||
- [ ] Add preview rendering logic (image vs color priority)
|
||||
- [ ] Add fallback states (no data, partial data)
|
||||
- [ ] Style preview to match public header dimensions
|
||||
- [ ] Add reactive binding to all branding props
|
||||
|
||||
### Form Updates
|
||||
- [ ] Import ProjectBrandingPreview into ProjectBrandingForm
|
||||
- [ ] Add preview at top of form (full-width, unlabeled)
|
||||
- [ ] Add toggle for "Show featured image in header"
|
||||
- [ ] Add toggle for "Show background color in header"
|
||||
- [ ] Add toggle for "Show logo in header"
|
||||
- [ ] Bind toggles to formData
|
||||
- [ ] Add helpful descriptions to each toggle
|
||||
- [ ] Copy toggle styles from AlbumForm
|
||||
- [ ] Test auto-save with toggle changes
|
||||
|
||||
### Public Site Updates
|
||||
- [ ] Update project detail page to respect visibility toggles
|
||||
- [ ] Update ProjectItem cards to respect visibility toggles (if applicable)
|
||||
- [ ] Ensure backward compatibility (default to showing all elements)
|
||||
|
||||
### Testing
|
||||
- [ ] Test all preview states (no data, partial data, full data)
|
||||
- [ ] Test toggle interactions
|
||||
- [ ] Test auto-save with changes
|
||||
- [ ] Test on different viewport sizes
|
||||
- [ ] Test with real project data
|
||||
|
||||
---
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- **Reactivity**: Use Svelte 5 runes ($derived, $state) for reactive preview
|
||||
- **Performance**: Preview should update without lag during typing/color picking
|
||||
- **Autosave**: All toggle changes should trigger autosave
|
||||
- **Validation**: Consider warning if header would be blank (all toggles off)
|
||||
- **Migration**: Existing projects should default all visibility toggles to `true`
|
||||
|
|
@ -25,11 +25,14 @@ model Project {
|
|||
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)
|
||||
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)
|
||||
showFeaturedImageInHeader Boolean @default(true)
|
||||
showBackgroundColorInHeader Boolean @default(true)
|
||||
showLogoInHeader Boolean @default(true)
|
||||
|
||||
@@index([slug])
|
||||
@@index([status])
|
||||
|
|
|
|||
140
src/lib/components/admin/ProjectBrandingPreview.svelte
Normal file
140
src/lib/components/admin/ProjectBrandingPreview.svelte
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
<script lang="ts">
|
||||
interface Props {
|
||||
featuredImage: string | null
|
||||
backgroundColor: string
|
||||
logoUrl: string
|
||||
showFeaturedImage: boolean
|
||||
showBackgroundColor: boolean
|
||||
showLogo: boolean
|
||||
}
|
||||
|
||||
let {
|
||||
featuredImage,
|
||||
backgroundColor,
|
||||
logoUrl,
|
||||
showFeaturedImage,
|
||||
showBackgroundColor,
|
||||
showLogo
|
||||
}: Props = $props()
|
||||
|
||||
// Determine the background to display
|
||||
const effectiveBackground = $derived.by(() => {
|
||||
// Priority: featured image > background color > fallback
|
||||
if (showFeaturedImage && featuredImage) {
|
||||
return { type: 'image' as const, value: featuredImage }
|
||||
}
|
||||
if (showBackgroundColor && backgroundColor && backgroundColor.trim() !== '') {
|
||||
return { type: 'color' as const, value: backgroundColor }
|
||||
}
|
||||
return { type: 'fallback' as const, value: '#f5f5f5' }
|
||||
})
|
||||
|
||||
// Determine if we should show the logo
|
||||
const shouldShowLogo = $derived(showLogo && logoUrl && logoUrl.trim() !== '')
|
||||
|
||||
// Placeholder icon SVG for when no logo is provided
|
||||
const placeholderIcon = `<svg width="48" height="48" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M9 9h6M9 12h6M9 15h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>`
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="branding-preview"
|
||||
class:has-image={effectiveBackground.type === 'image'}
|
||||
style:background-color={effectiveBackground.type !== 'image'
|
||||
? effectiveBackground.value
|
||||
: undefined}
|
||||
style:background-image={effectiveBackground.type === 'image'
|
||||
? `url(${effectiveBackground.value})`
|
||||
: undefined}
|
||||
>
|
||||
{#if shouldShowLogo}
|
||||
<img src={logoUrl} alt="Project logo preview" class="preview-logo" />
|
||||
{:else if showLogo}
|
||||
<!-- Show placeholder when logo toggle is on but no logo provided -->
|
||||
<div class="preview-placeholder">
|
||||
{@html placeholderIcon}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.branding-preview {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: $card-corner-radius;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
margin-bottom: $unit-4x;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@include breakpoint('tablet') {
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
&.has-image {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-logo {
|
||||
width: 85px;
|
||||
height: 85px;
|
||||
object-fit: contain;
|
||||
|
||||
@include breakpoint('phone') {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
@include breakpoint('small-phone') {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-placeholder {
|
||||
width: 85px;
|
||||
height: 85px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(0, 0, 0, 0.2);
|
||||
opacity: 0.5;
|
||||
|
||||
:global(svg) {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
|
||||
:global(svg) {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint('small-phone') {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
|
||||
:global(svg) {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -37,7 +37,10 @@ export function createProjectFormStore(initialProject?: Project | null) {
|
|||
caseStudyContent: project.caseStudyContent || {
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph' }]
|
||||
}
|
||||
},
|
||||
showFeaturedImageInHeader: project.showFeaturedImageInHeader ?? true,
|
||||
showBackgroundColorInHeader: project.showBackgroundColorInHeader ?? true,
|
||||
showLogoInHeader: project.showLogoInHeader ?? true
|
||||
}
|
||||
original = { ...fields }
|
||||
}
|
||||
|
|
@ -104,7 +107,10 @@ export function createProjectFormStore(initialProject?: Project | null) {
|
|||
fields.caseStudyContent.content &&
|
||||
fields.caseStudyContent.content.length > 0
|
||||
? fields.caseStudyContent
|
||||
: null
|
||||
: null,
|
||||
showFeaturedImageInHeader: fields.showFeaturedImageInHeader,
|
||||
showBackgroundColorInHeader: fields.showBackgroundColorInHeader,
|
||||
showLogoInHeader: fields.showLogoInHeader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export interface Project {
|
|||
displayOrder: number
|
||||
status: ProjectStatus
|
||||
password: string | null
|
||||
showFeaturedImageInHeader: boolean
|
||||
showBackgroundColorInHeader: boolean
|
||||
showLogoInHeader: boolean
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
publishedAt?: string | null
|
||||
|
|
@ -41,6 +44,9 @@ export interface ProjectFormData {
|
|||
status: ProjectStatus
|
||||
password: string
|
||||
caseStudyContent: any
|
||||
showFeaturedImageInHeader: boolean
|
||||
showBackgroundColorInHeader: boolean
|
||||
showLogoInHeader: boolean
|
||||
}
|
||||
|
||||
export const defaultProjectFormData: ProjectFormData = {
|
||||
|
|
@ -61,5 +67,8 @@ export const defaultProjectFormData: ProjectFormData = {
|
|||
caseStudyContent: {
|
||||
type: 'doc',
|
||||
content: [{ type: 'paragraph' }]
|
||||
}
|
||||
},
|
||||
showFeaturedImageInHeader: true,
|
||||
showBackgroundColorInHeader: true,
|
||||
showLogoInHeader: true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,13 +148,21 @@
|
|||
<div
|
||||
bind:this={headerContainer}
|
||||
class="project-header-container"
|
||||
style="background-color: {project.backgroundColor || '#f5f5f5'}"
|
||||
class:has-image={project.showFeaturedImageInHeader && project.featuredImage}
|
||||
style:background-color={!project.showFeaturedImageInHeader || !project.featuredImage
|
||||
? project.showBackgroundColorInHeader && project.backgroundColor
|
||||
? project.backgroundColor
|
||||
: '#f5f5f5'
|
||||
: undefined}
|
||||
style:background-image={project.showFeaturedImageInHeader && project.featuredImage
|
||||
? `url(${project.featuredImage})`
|
||||
: undefined}
|
||||
onmousemove={handleMouseMove}
|
||||
onmouseleave={handleMouseLeave}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{#if project.logoUrl}
|
||||
{#if project.showLogoInHeader && project.logoUrl}
|
||||
<img
|
||||
src={project.logoUrl}
|
||||
alt="{project.title} logo"
|
||||
|
|
@ -250,6 +258,12 @@
|
|||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&.has-image {
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@include breakpoint('phone') {
|
||||
height: 250px;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue