feat: redesign album drawer into a gorgeous centered two-column modal (#243)

Co-authored-by: ralksta <ralksta@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-05-31 21:30:00 +02:00
committed by GitHub
parent 8964877f6a
commit a504f7296a
2 changed files with 214 additions and 135 deletions

View File

@@ -1882,18 +1882,20 @@ h3 .svg-icon {
background: rgba(74, 222, 128, 0.05); background: rgba(74, 222, 128, 0.05);
} }
/* ── Drawer Component styling ── */ /* ── Breitwand-Kiste Centered Modal styling ── */
.album-drawer-overlay { .album-drawer-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 1000; z-index: 1000;
background: var(--admin-overlay); background: var(--admin-overlay);
backdrop-filter: blur(8px); backdrop-filter: blur(12px);
display: flex; display: flex;
justify-content: flex-end; align-items: center;
justify-content: center;
padding: 1.5rem;
opacity: 0; opacity: 0;
pointer-events: none; pointer-events: none;
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1); transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
} }
.album-drawer-overlay.open { .album-drawer-overlay.open {
@@ -1903,19 +1905,21 @@ h3 .svg-icon {
.album-drawer { .album-drawer {
width: 100%; width: 100%;
max-width: 460px; max-width: 840px;
height: 100%; max-height: 90vh;
background: var(--admin-bg-raised); background: var(--admin-bg-raised);
border-left: 1px solid var(--admin-border); border: 1px solid var(--admin-border);
box-shadow: -8px 0 32px rgba(0, 0, 0, 0.35); border-radius: 16px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.4);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
transform: translateX(100%); overflow: hidden;
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); transform: scale(0.95) translateY(10px);
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
} }
.album-drawer-overlay.open .album-drawer { .album-drawer-overlay.open .album-drawer {
transform: translateX(0); transform: scale(1) translateY(0);
} }
.album-drawer-header { .album-drawer-header {
@@ -1928,7 +1932,7 @@ h3 .svg-icon {
} }
.album-drawer-header h3 { .album-drawer-header h3 {
font-size: 1.1rem; font-size: 1.15rem;
font-weight: 700; font-weight: 700;
margin: 0; margin: 0;
display: flex; display: flex;
@@ -1936,15 +1940,56 @@ h3 .svg-icon {
gap: 0.5rem; gap: 0.5rem;
} }
/* 2-Column Body Layout */
.album-drawer-body { .album-drawer-body {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
padding: 1.5rem; padding: 0;
display: grid;
grid-template-columns: 1.2fr 1.8fr;
min-height: 0;
}
@media (max-width: 768px) {
.album-drawer-body {
grid-template-columns: 1fr;
}
.album-drawer {
max-height: 95vh;
}
}
.modal-left-column {
padding: 1.75rem;
background: var(--admin-bg-elevated);
border-right: 1px solid var(--admin-border);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 1.5rem; gap: 1.5rem;
} }
@media (max-width: 768px) {
.modal-left-column {
border-right: none;
border-bottom: 1px solid var(--admin-border);
padding: 1.25rem;
}
}
.modal-right-column {
padding: 1.75rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
overflow-y: auto;
}
@media (max-width: 768px) {
.modal-right-column {
padding: 1.25rem;
}
}
.album-drawer-body::-webkit-scrollbar { .album-drawer-body::-webkit-scrollbar {
width: 6px; width: 6px;
} }
@@ -1960,74 +2005,86 @@ h3 .svg-icon {
} }
.album-drawer-footer { .album-drawer-footer {
padding: 1.25rem 1.5rem; padding: 1rem 1.5rem;
border-top: 1px solid var(--admin-border); border-top: 1px solid var(--admin-border);
background: var(--admin-bg-elevated); background: var(--admin-bg-elevated);
display: flex; display: flex;
justify-content: space-between;
gap: 0.75rem; gap: 0.75rem;
} }
.album-drawer-footer .admin-btn { /* Left Column Cover Preview */
flex: 1; .modal-cover-container {
} width: 100%;
aspect-ratio: 16 / 10;
/* Drawer Hero Preview Banner */
.drawer-hero-section {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
background: var(--admin-bg-elevated);
border: 1px solid var(--admin-border);
border-radius: 10px; border-radius: 10px;
}
.drawer-hero-cover {
width: 90px;
height: 60px;
border-radius: 6px;
overflow: hidden; overflow: hidden;
background: var(--admin-bg);
border: 1px solid var(--admin-border); border: 1px solid var(--admin-border);
flex-shrink: 0; background: var(--admin-bg);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
} }
.drawer-hero-cover img { .modal-cover-container img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
display: block;
} }
.drawer-hero-placeholder { .modal-cover-placeholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--admin-text-muted); color: var(--admin-text-muted);
font-size: 1.25rem; font-size: 2.5rem;
} }
.drawer-hero-meta { /* Album metadata card in left column */
.modal-meta-box {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 0; gap: 0.25rem;
} }
.drawer-hero-name { .modal-album-title {
font-size: 0.9rem; font-size: 1.1rem;
font-weight: 600; font-weight: 700;
color: var(--admin-text); color: var(--admin-text);
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.drawer-hero-count { .modal-album-subtitle {
font-size: 0.75rem; font-size: 0.8rem;
color: var(--admin-text-secondary); color: var(--admin-text-secondary);
} }
.modal-info-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: 0.8rem;
color: var(--admin-text-secondary);
}
.modal-info-item {
display: flex;
justify-content: space-between;
border-bottom: 1px dashed var(--admin-border);
padding-bottom: 0.25rem;
}
.modal-info-label {
color: var(--admin-text-muted);
}
.modal-info-value {
font-weight: 600;
}
/* Drawer Copy UUID element */ /* Drawer Copy UUID element */
.drawer-uuid-section { .drawer-uuid-section {
padding-top: 0.75rem; padding-top: 0.75rem;

View File

@@ -1276,7 +1276,7 @@ export default function PageBuilder() {
/> />
)} )}
{/* Slide-over Drawer for Album Details */} {/* Slide-over Drawer for Album Details (Centered 2-Column Modal) */}
{(() => { {(() => {
if (!editingAlbumAddress) return null; if (!editingAlbumAddress) return null;
const info = getEditingAlbumInfo(editingAlbumAddress); const info = getEditingAlbumInfo(editingAlbumAddress);
@@ -1300,24 +1300,60 @@ export default function PageBuilder() {
</button> </button>
</div> </div>
<div className="album-drawer-body"> <div className="album-drawer-body">
{/* Album Cover / Hero Preview */} {/* Left Column - Visual Info & Stats */}
<div className="drawer-hero-section"> <div className="modal-left-column">
<div className="drawer-hero-cover"> <div className="modal-cover-container">
{heroThumb ? ( {heroThumb ? (
<img src={`/api/admin/thumbnail/${heroThumb}`} alt="" loading="lazy" /> <img src={`/api/admin/thumbnail/${heroThumb}`} alt="" loading="lazy" />
) : ( ) : (
<div className="drawer-hero-placeholder"> <div className="modal-cover-placeholder">
<Icons.Camera /> <Icons.Camera />
</div> </div>
)} )}
</div> </div>
<div className="drawer-hero-meta"> <div className="modal-meta-box">
<span className="drawer-hero-name">{name}</span> <span className="modal-album-title">{album.title || name}</span>
<span className="drawer-hero-count">{count} photos</span> <span className="modal-album-subtitle">Original: {name}</span>
</div>
<div className="modal-info-list">
<div className="modal-info-item">
<span className="modal-info-label">Photo Count:</span>
<span className="modal-info-value">{count} photos</span>
</div>
<div className="modal-info-item">
<span className="modal-info-label">Status:</span>
<span className="modal-info-value">
{album.password ? 'Password Protected' : 'Public Access'}
</span>
</div>
<div className="modal-info-item">
<span className="modal-info-label">Custom Hero:</span>
<span className="modal-info-value">
{album.heroImage ? 'Yes' : 'No'}
</span>
</div> </div>
</div> </div>
{/* Form Fields */} {/* Technical UUID */}
<div className="drawer-uuid-section">
<label className="admin-field-label">IMMICH ALBUM UUID</label>
<div className="uuid-copy-box">
<code>{album.id}</code>
<button
className="uuid-copy-btn"
onClick={() => {
navigator.clipboard.writeText(album.id);
}}
title="Copy UUID"
>
<Icons.Copy />
</button>
</div>
</div>
</div>
{/* Right Column - Form Fields */}
<div className="modal-right-column">
<div className="admin-field"> <div className="admin-field">
<label>Title override</label> <label>Title override</label>
<input <input
@@ -1333,7 +1369,7 @@ export default function PageBuilder() {
value={album.description || ''} value={album.description || ''}
onChange={(e) => onUpdate({ description: e.target.value || undefined })} onChange={(e) => onUpdate({ description: e.target.value || undefined })}
placeholder="Optional description for visitors" placeholder="Optional description for visitors"
rows={3} rows={4}
/> />
</div> </div>
@@ -1385,37 +1421,23 @@ export default function PageBuilder() {
</button> </button>
</div> </div>
</div> </div>
{/* Technical UUID */}
<div className="drawer-uuid-section">
<label className="admin-field-label">IMMICH ALBUM UUID</label>
<div className="uuid-copy-box">
<code>{album.id}</code>
<button
className="uuid-copy-btn"
onClick={() => {
navigator.clipboard.writeText(album.id);
}}
title="Copy UUID"
>
<Icons.Copy />
</button>
</div>
</div> </div>
</div> </div>
<div className="album-drawer-footer"> <div className="album-drawer-footer">
<button
className="admin-btn admin-btn-primary"
onClick={() => setEditingAlbumAddress(null)}
>
Done
</button>
<button <button
className="admin-btn admin-btn-ghost admin-btn-danger" className="admin-btn admin-btn-ghost admin-btn-danger"
onClick={onRemove} onClick={onRemove}
style={{ maxWidth: '160px' }}
> >
<Icons.Trash /> Remove Album <Icons.Trash /> Remove Album
</button> </button>
<button
className="admin-btn admin-btn-primary"
onClick={() => setEditingAlbumAddress(null)}
style={{ maxWidth: '140px' }}
>
Done
</button>
</div> </div>
</div> </div>
</div> </div>