feat: add click-to-close backdrop behind DetailPanel
All checks were successful
Build & Publish / publish (push) Successful in 58s

Adds a subtle semi-transparent backdrop (rgba(0,0,0,0.15)) behind the
overlay panel. Clicking the backdrop closes the panel. Fades in with
the panel animation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-26 21:57:34 +01:00
parent d26dc6a8a5
commit 26de5ec58f
2 changed files with 55 additions and 39 deletions

View File

@@ -1,3 +1,16 @@
.backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.15);
z-index: 99;
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.panel { .panel {
position: fixed; position: fixed;
top: 0; top: 0;

View File

@@ -23,51 +23,54 @@ export function DetailPanel({ open, onClose, title, tabs, children, actions, cla
const activeContent = tabs?.find((t) => t.value === activeTab)?.content const activeContent = tabs?.find((t) => t.value === activeTab)?.content
const panel = ( const content = (
<aside <>
className={`${styles.panel} ${open ? styles.open : ''} ${className ?? ''}`} {open && <div className={styles.backdrop} onClick={onClose} aria-hidden="true" />}
aria-hidden={!open} <aside
> className={`${styles.panel} ${open ? styles.open : ''} ${className ?? ''}`}
<div className={styles.header}> aria-hidden={!open}
<span className={styles.title}>{title}</span> >
<button <div className={styles.header}>
className={styles.closeBtn} <span className={styles.title}>{title}</span>
onClick={onClose} <button
aria-label="Close panel" className={styles.closeBtn}
type="button" onClick={onClose}
> aria-label="Close panel"
&times; type="button"
</button> >
</div> &times;
</button>
{tabs && tabs.length > 0 && (
<div className={styles.tabs}>
{tabs.map((tab) => (
<button
key={tab.value}
className={`${styles.tab} ${tab.value === activeTab ? styles.activeTab : ''}`}
onClick={() => setActiveTab(tab.value)}
type="button"
>
{tab.label}
</button>
))}
</div> </div>
)}
<div className={styles.body}> {tabs && tabs.length > 0 && (
{children ?? activeContent} <div className={styles.tabs}>
</div> {tabs.map((tab) => (
<button
key={tab.value}
className={`${styles.tab} ${tab.value === activeTab ? styles.activeTab : ''}`}
onClick={() => setActiveTab(tab.value)}
type="button"
>
{tab.label}
</button>
))}
</div>
)}
{actions && ( <div className={styles.body}>
<div className={styles.actions}> {children ?? activeContent}
{actions}
</div> </div>
)}
</aside> {actions && (
<div className={styles.actions}>
{actions}
</div>
)}
</aside>
</>
) )
// Portal to AppShell level if target exists, otherwise render in place // Portal to AppShell level if target exists, otherwise render in place
const portalTarget = document.getElementById('cameleer-detail-panel-root') const portalTarget = document.getElementById('cameleer-detail-panel-root')
return portalTarget ? createPortal(panel, portalTarget) : panel return portalTarget ? createPortal(content, portalTarget) : content
} }