Revamp /admin with drill-down sidebar nav and focused sub-views; v1.1.50
Publish Container / docker (push) Successful in 13m50s
Publish Container / docker (push) Successful in 13m50s
- Replace flat sidebar with accordion nav: parent items (Homepage, About, Contact, Podcast Hub) expand to reveal focused sub-items - Split big sections into granular sub-views so each form shows only 3-8 fields (Homepage: Hero/Share/PRISM/Where-to-Next, About: Show Copy/Bio/ Testimonials, Contact: Profile/Copy & Scripture, Podcast: Current Series/ Highlights/Finished Books/Scripts/RSS) - New topbar: brand + breadcrumb (Homepage › Hero), clean gold Save Draft button, ghost Preview/Publish buttons — no more jump-to select - New sidebar: compact 196px width, filter input at top, small-caps group labels, chevron-animated parent items, indented sub-items with active highlight, Back-to-site/Log-out/version at the bottom - Replace PodcastTab state with adminView sub-views (podcast:current-series etc.) eliminating the secondary tab bar inside the panel - Extend AdminView type with all new colon-separated sub-view values Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
a072076818
commit
e593031d0d
3 files changed
+683
-527
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "siteforge",
|
||||
"private": true,
|
||||
"version": "1.1.49",
|
||||
"version": "1.1.50",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+286
-264
@@ -697,8 +697,12 @@ interface Subscriber {
|
||||
type StringField = Exclude<keyof SiteContent, 'customLinks' | 'customBlocks' | 'redirects' | 'podcastFeaturedLinks' | 'finishedBooks' | 'testimonials' | 'seo' | 'legal' | 'whereToNextCards' | 'colossiansStudySections' | 'studies'>
|
||||
|
||||
type AdminView =
|
||||
| 'dashboard' | 'homepage' | 'start-here' | 'about' | 'contact'
|
||||
| 'podcast'
|
||||
| 'dashboard'
|
||||
| 'homepage:hero' | 'homepage:share' | 'homepage:prism' | 'homepage:where-to-next'
|
||||
| 'start-here'
|
||||
| 'about:show-copy' | 'about:bio' | 'about:testimonials'
|
||||
| 'contact:profile' | 'contact:copy'
|
||||
| 'podcast:current-series' | 'podcast:highlights' | 'podcast:finished-books' | 'podcast:scripts' | 'podcast:rss'
|
||||
| 'downloads' | 'custom-links' | 'content-blocks' | 'books' | 'recommended-resources'
|
||||
| 'questions' | 'study-comments' | 'analytics' | 'assets' | 'colossians-study' | 'qr-codes'
|
||||
| 'subscribers' | 'study-users' | 'email-templates'
|
||||
@@ -709,92 +713,109 @@ interface AdminSectionLink {
|
||||
label: string
|
||||
}
|
||||
|
||||
const ADMIN_VIEW_OPTIONS: Array<{ group: string; options: Array<{ value: AdminView; label: string }> }> = [
|
||||
type NavLeaf = { kind: 'leaf'; view: AdminView; label: string }
|
||||
type NavParent = { kind: 'parent'; id: string; label: string; children: NavLeaf[] }
|
||||
type NavEntry = NavLeaf | NavParent
|
||||
interface NavGroup { group: string; items: NavEntry[] }
|
||||
|
||||
const ADMIN_NAV: NavGroup[] = [
|
||||
{
|
||||
group: 'Overview',
|
||||
options: [{ value: 'dashboard', label: 'Dashboard' }],
|
||||
items: [{ kind: 'leaf', view: 'dashboard', label: 'Dashboard' }],
|
||||
},
|
||||
{
|
||||
group: 'Site',
|
||||
options: [
|
||||
{ value: 'homepage', label: 'Homepage' },
|
||||
{ value: 'start-here', label: 'Start Here' },
|
||||
{ value: 'about', label: 'About' },
|
||||
{ value: 'contact', label: 'Contact' },
|
||||
items: [
|
||||
{ kind: 'parent', id: 'homepage', label: 'Homepage', children: [
|
||||
{ kind: 'leaf', view: 'homepage:hero', label: 'Hero' },
|
||||
{ kind: 'leaf', view: 'homepage:share', label: 'Share Section' },
|
||||
{ kind: 'leaf', view: 'homepage:prism', label: 'PRISM Block' },
|
||||
{ kind: 'leaf', view: 'homepage:where-to-next', label: 'Where to Next' },
|
||||
]},
|
||||
{ kind: 'leaf', view: 'start-here', label: 'Start Here' },
|
||||
{ kind: 'parent', id: 'about', label: 'About', children: [
|
||||
{ kind: 'leaf', view: 'about:show-copy', label: 'Show Copy' },
|
||||
{ kind: 'leaf', view: 'about:bio', label: 'Nate Bio' },
|
||||
{ kind: 'leaf', view: 'about:testimonials', label: 'Testimonials' },
|
||||
]},
|
||||
{ kind: 'parent', id: 'contact', label: 'Contact', children: [
|
||||
{ kind: 'leaf', view: 'contact:profile', label: 'Profile' },
|
||||
{ kind: 'leaf', view: 'contact:copy', label: 'Copy & Scripture' },
|
||||
]},
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Content',
|
||||
options: [
|
||||
{ value: 'podcast', label: 'Podcast Hub' },
|
||||
{ value: 'colossians-study', label: 'Studies' },
|
||||
{ value: 'downloads', label: 'Resources' },
|
||||
{ value: 'custom-links', label: 'Custom Links' },
|
||||
{ value: 'content-blocks', label: 'Content Blocks' },
|
||||
{ value: 'books', label: 'Books' },
|
||||
{ value: 'recommended-resources', label: 'Recommended Resources' },
|
||||
items: [
|
||||
{ kind: 'parent', id: 'podcast', label: 'Podcast Hub', children: [
|
||||
{ kind: 'leaf', view: 'podcast:current-series', label: 'Current Series' },
|
||||
{ kind: 'leaf', view: 'podcast:highlights', label: 'Ep. Highlights' },
|
||||
{ kind: 'leaf', view: 'podcast:finished-books', label: 'Finished Books' },
|
||||
{ kind: 'leaf', view: 'podcast:scripts', label: 'Episode Scripts' },
|
||||
{ kind: 'leaf', view: 'podcast:rss', label: 'RSS Feed' },
|
||||
]},
|
||||
{ kind: 'leaf', view: 'colossians-study', label: 'Studies' },
|
||||
{ kind: 'leaf', view: 'downloads', label: 'Resources' },
|
||||
{ kind: 'leaf', view: 'custom-links', label: 'Custom Links' },
|
||||
{ kind: 'leaf', view: 'content-blocks', label: 'Content Blocks' },
|
||||
{ kind: 'leaf', view: 'books', label: 'Books' },
|
||||
{ kind: 'leaf', view: 'recommended-resources', label: 'Recommended Resources' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Audience',
|
||||
options: [
|
||||
{ value: 'questions', label: 'Questions' },
|
||||
{ value: 'study-comments', label: 'Study Comments' },
|
||||
{ value: 'subscribers', label: 'Subscribers' },
|
||||
{ value: 'study-users', label: 'Study Users' },
|
||||
items: [
|
||||
{ kind: 'leaf', view: 'questions', label: 'Questions' },
|
||||
{ kind: 'leaf', view: 'study-comments', label: 'Study Comments' },
|
||||
{ kind: 'leaf', view: 'subscribers', label: 'Subscribers' },
|
||||
{ kind: 'leaf', view: 'study-users', label: 'Study Users' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Insights',
|
||||
options: [
|
||||
{ value: 'analytics', label: 'Analytics' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Tools',
|
||||
options: [
|
||||
{ value: 'assets', label: 'Asset Manager' },
|
||||
{ value: 'qr-codes', label: 'QR Codes' },
|
||||
group: 'Insights & Tools',
|
||||
items: [
|
||||
{ kind: 'leaf', view: 'analytics', label: 'Analytics' },
|
||||
{ kind: 'leaf', view: 'assets', label: 'Asset Manager' },
|
||||
{ kind: 'leaf', view: 'qr-codes', label: 'QR Codes' },
|
||||
],
|
||||
},
|
||||
{
|
||||
group: 'Configure',
|
||||
options: [
|
||||
{ value: 'global', label: 'Footer & Global' },
|
||||
{ value: 'seo', label: 'SEO & Redirects' },
|
||||
{ value: 'email-templates', label: 'Email Templates' },
|
||||
{ value: 'legal', label: 'Legal Pages' },
|
||||
{ value: 'security', label: 'Security' },
|
||||
{ value: 'brand', label: 'Brand Kit' },
|
||||
items: [
|
||||
{ kind: 'leaf', view: 'global', label: 'Footer & Global' },
|
||||
{ kind: 'leaf', view: 'seo', label: 'SEO & Redirects' },
|
||||
{ kind: 'leaf', view: 'email-templates', label: 'Email Templates' },
|
||||
{ kind: 'leaf', view: 'legal', label: 'Legal Pages' },
|
||||
{ kind: 'leaf', view: 'security', label: 'Security' },
|
||||
{ kind: 'leaf', view: 'brand', label: 'Brand Kit' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
function getNavParentId(view: AdminView): string | null {
|
||||
for (const group of ADMIN_NAV) {
|
||||
for (const item of group.items) {
|
||||
if (item.kind === 'parent' && item.children.some(c => c.view === view)) return item.id
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function getNavBreadcrumb(view: AdminView): { parent?: string; label: string } {
|
||||
for (const group of ADMIN_NAV) {
|
||||
for (const item of group.items) {
|
||||
if (item.kind === 'parent') {
|
||||
const child = item.children.find(c => c.view === view)
|
||||
if (child) return { parent: item.label, label: child.label }
|
||||
} else if (item.view === view) {
|
||||
return { label: item.label }
|
||||
}
|
||||
}
|
||||
}
|
||||
return { label: String(view) }
|
||||
}
|
||||
|
||||
const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
|
||||
homepage: [
|
||||
{ id: 'homepage-hero', label: 'Hero' },
|
||||
{ id: 'homepage-share', label: 'Share Section' },
|
||||
{ id: 'homepage-prism', label: 'PRISM Block' },
|
||||
{ id: 'homepage-where-to-next-labels', label: 'Where to Next Labels' },
|
||||
{ id: 'homepage-where-to-next-cards', label: 'Where to Next Cards' },
|
||||
],
|
||||
'start-here': [
|
||||
{ id: 'start-here-intro', label: 'Intro' },
|
||||
{ id: 'start-here-step-1', label: 'Step 1' },
|
||||
{ id: 'start-here-step-2', label: 'Step 2' },
|
||||
{ id: 'start-here-step-3', label: 'Step 3' },
|
||||
],
|
||||
about: [
|
||||
{ id: 'about-show-copy', label: 'Show Copy' },
|
||||
{ id: 'about-nate-bio', label: 'Nate Bio' },
|
||||
{ id: 'about-images', label: 'Images' },
|
||||
{ id: 'about-testimonials', label: 'Testimonials' },
|
||||
],
|
||||
contact: [
|
||||
{ id: 'contact-profile', label: 'Profile' },
|
||||
{ id: 'contact-intro', label: 'Intro Copy' },
|
||||
{ id: 'contact-scripture', label: 'Scripture' },
|
||||
],
|
||||
downloads: [
|
||||
{ id: 'downloads-study-guide', label: 'Study Guide' },
|
||||
{ id: 'downloads-library', label: 'Download Library' },
|
||||
@@ -816,8 +837,6 @@ const ADMIN_SECTION_LINKS: Partial<Record<AdminView, AdminSectionLink[]>> = {
|
||||
],
|
||||
}
|
||||
|
||||
type PodcastTab = 'current-series' | 'episode-highlights' | 'finished-books' | 'episode-scripts' | 'rss-feed'
|
||||
|
||||
type MainContentSection = 'hero' | 'start-here' | 'about' | 'contact' | 'series' | 'share' | 'prism' | 'global' | 'email-templates'
|
||||
|
||||
const BRAND_KIT_SWATCHES = [
|
||||
@@ -1106,7 +1125,7 @@ export default function AdminPage({ content, onSave, onLogout }: Props) {
|
||||
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const [adminView, setAdminView] = useState<AdminView>('dashboard')
|
||||
const [podcastTab, setPodcastTab] = useState<PodcastTab>('current-series')
|
||||
const [navOpenGroups, setNavOpenGroups] = useState<Set<string>>(new Set())
|
||||
const [stats, setStats] = useState<AdminStats | null>(null)
|
||||
const [statsStatus, setStatsStatus] = useState<'loading' | 'ready' | 'error'>('loading')
|
||||
const [statsRange, setStatsRange] = useState<'7d' | '30d' | '90d'>('30d')
|
||||
@@ -1204,15 +1223,28 @@ const [dashboardNow, setDashboardNow] = useState(() => new Date())
|
||||
const unreadEmailCount = contactSubmissions.filter(s => !s.archived).length
|
||||
const unansweredCount = questions.filter(q => !q.answer?.trim()).length
|
||||
const navSearchTerm = navSearch.trim().toLowerCase()
|
||||
const filteredAdminViewOptions = ADMIN_VIEW_OPTIONS
|
||||
const filteredNav: NavGroup[] = ADMIN_NAV
|
||||
.map(group => ({
|
||||
...group,
|
||||
options: group.options.filter(option => {
|
||||
if (!navSearchTerm) return true
|
||||
return `${group.group} ${option.label} ${option.value}`.toLowerCase().includes(navSearchTerm)
|
||||
}),
|
||||
items: group.items
|
||||
.map(item => {
|
||||
if (item.kind === 'parent') {
|
||||
if (!navSearchTerm) return item
|
||||
const matchesParent = `${group.group} ${item.label}`.toLowerCase().includes(navSearchTerm)
|
||||
const filteredChildren = item.children.filter(c =>
|
||||
`${group.group} ${item.label} ${c.label}`.toLowerCase().includes(navSearchTerm)
|
||||
)
|
||||
if (matchesParent) return item
|
||||
if (filteredChildren.length > 0) return { ...item, children: filteredChildren }
|
||||
return null
|
||||
} else {
|
||||
if (!navSearchTerm) return item
|
||||
return `${group.group} ${item.label}`.toLowerCase().includes(navSearchTerm) ? item : null
|
||||
}
|
||||
})
|
||||
.filter((item): item is NavEntry => item !== null),
|
||||
}))
|
||||
.filter(group => group.options.length > 0)
|
||||
.filter(group => group.items.length > 0)
|
||||
|
||||
// Silently saves draft every 30s when there are unsaved changes
|
||||
useAutosave(form, isDirty, handleSaveDraft)
|
||||
@@ -2070,6 +2102,8 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
if (!confirmLeaveUnsavedChanges()) return
|
||||
setAdminView(view)
|
||||
setMobileNavOpen(false)
|
||||
const parentId = getNavParentId(view)
|
||||
if (parentId) setNavOpenGroups(prev => new Set([...prev, parentId]))
|
||||
}
|
||||
|
||||
function getAdminViewBadge(view: AdminView) {
|
||||
@@ -2643,52 +2677,40 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
aria-expanded={mobileNavOpen}
|
||||
aria-controls="admin-sidebar-nav"
|
||||
>
|
||||
{mobileNavOpen ? 'Close' : 'Menu'}
|
||||
<span className="admin-menu-bar" /><span className="admin-menu-bar" /><span className="admin-menu-bar" />
|
||||
</button>
|
||||
<div className="admin-topbar-brand">
|
||||
<span className="admin-topbar-ornament">✦</span>
|
||||
<span className="admin-topbar-title">Site Admin</span>
|
||||
<span className="admin-topbar-sub">Verse by Verse with Nate</span>
|
||||
<span className="admin-topbar-title">Siteforge</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const crumb = getNavBreadcrumb(adminView)
|
||||
return (
|
||||
<div className="admin-topbar-crumb">
|
||||
{crumb.parent && <><span className="admin-crumb-parent">{crumb.parent}</span><span className="admin-crumb-sep">›</span></>}
|
||||
<span className="admin-crumb-current">{crumb.label}</span>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
<div className="admin-topbar-state">
|
||||
{isDirty && <span className="admin-status admin-status--warn">● Unsaved</span>}
|
||||
<div className="admin-topbar-right">
|
||||
{isDirty && <span className="admin-unsaved-dot">● Unsaved</span>}
|
||||
<span className="admin-topbar-meta">Draft: {formatDate(publishState.draftUpdatedAt)}</span>
|
||||
<span className="admin-topbar-meta">Live: {formatDate(publishState.publishedAt)}</span>
|
||||
</div>
|
||||
<div className="admin-topbar-actions">
|
||||
<label className="admin-view-picker" aria-label="Jump to admin section">
|
||||
<span className="admin-view-picker-label">Jump to</span>
|
||||
<select
|
||||
className="admin-view-picker-select"
|
||||
value={adminView}
|
||||
onChange={e => navigateTo(e.target.value as AdminView)}
|
||||
>
|
||||
{ADMIN_VIEW_OPTIONS.map(group => (
|
||||
<optgroup key={group.group} label={group.group}>
|
||||
{group.options.map(option => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<button type="button" className="btn-admin-save" onClick={handleSaveDraft} disabled={status === 'saving' || !isDirty}>
|
||||
{status === 'saving' ? 'Saving…' : 'Save Draft'}
|
||||
</button>
|
||||
<button type="button" className={`btn-admin-reset${previewOpen ? ' btn-admin-reset--active' : ''}`} onClick={() => setPreviewOpen(o => !o)}>
|
||||
<button type="button" className={`btn-admin-ghost${previewOpen ? ' btn-admin-ghost--active' : ''}`} onClick={() => setPreviewOpen(o => !o)}>
|
||||
{previewOpen ? 'Close Preview' : 'Preview'}
|
||||
</button>
|
||||
{publishConfirmOpen ? (
|
||||
<>
|
||||
<span style={{ fontSize: '0.82rem', color: '#e0c070' }}>Publish to live site?</span>
|
||||
<button type="button" className="btn-admin-save" onClick={() => { setPublishConfirmOpen(false); handlePublishDraft() }}>Yes, Publish</button>
|
||||
<button type="button" className="btn-admin-reset" onClick={() => setPublishConfirmOpen(false)}>Cancel</button>
|
||||
<button type="button" className="btn-admin-ghost" onClick={() => setPublishConfirmOpen(false)}>Cancel</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" className="btn-admin-reset" onClick={() => setPublishConfirmOpen(true)}>Publish</button>
|
||||
<button type="button" className="btn-admin-publish" onClick={() => setPublishConfirmOpen(true)}>Publish</button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
@@ -2696,7 +2718,73 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
<div className={`admin-layout${previewOpen ? ' admin-layout--split' : ''}`}>
|
||||
{/* ── Sidebar ── */}
|
||||
<nav id="admin-sidebar-nav" className={`admin-sidebar${mobileNavOpen ? ' admin-sidebar--open' : ''}`} aria-label="Admin navigation">
|
||||
<div className="admin-sidebar-meta-links">
|
||||
<div className="admin-sidebar-top">
|
||||
<div className="admin-nav-search">
|
||||
<input
|
||||
id="admin-nav-search-input"
|
||||
type="search"
|
||||
value={navSearch}
|
||||
onChange={e => setNavSearch(e.target.value)}
|
||||
placeholder="Filter…"
|
||||
aria-label="Filter admin sections"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-sidebar-scroll">
|
||||
{filteredNav.length === 0 ? (
|
||||
<p className="admin-nav-empty">No sections match.</p>
|
||||
) : (
|
||||
filteredNav.map(group => (
|
||||
<div key={group.group} className="admin-nav-group">
|
||||
<span className="admin-nav-group-label">{group.group}</span>
|
||||
{group.items.map(item => {
|
||||
if (item.kind === 'parent') {
|
||||
const isOpen = navOpenGroups.has(item.id) || (navSearchTerm !== '' && item.children.some(c => c.view === adminView))
|
||||
const hasActive = item.children.some(c => c.view === adminView)
|
||||
return (
|
||||
<div key={item.id} className="admin-nav-parent-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className={`admin-nav-parent${hasActive ? ' admin-nav-parent--active' : ''}`}
|
||||
onClick={() => setNavOpenGroups(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(item.id)) next.delete(item.id)
|
||||
else next.add(item.id)
|
||||
return next
|
||||
})}
|
||||
>
|
||||
<span>{item.label}</span>
|
||||
<svg className={`admin-nav-chevron${isOpen ? ' admin-nav-chevron--open' : ''}`} width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
||||
<path d="M3 4.5L6 7.5L9 4.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="admin-nav-sub">
|
||||
{item.children.map(child => (
|
||||
<button
|
||||
key={child.view}
|
||||
type="button"
|
||||
className={`admin-nav-sub-item${adminView === child.view ? ' admin-nav-sub-item--active' : ''}`}
|
||||
onClick={() => navigateTo(child.view)}
|
||||
>
|
||||
{child.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return renderAdminNavItem(item.view, item.label)
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="admin-sidebar-footer">
|
||||
<Link
|
||||
to="/"
|
||||
className="admin-meta-link"
|
||||
@@ -2718,32 +2806,8 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
>
|
||||
Log Out
|
||||
</button>
|
||||
{appVersion && <span className="admin-sidebar-version">{appVersion}</span>}
|
||||
</div>
|
||||
{appVersion && (
|
||||
<div className="admin-sidebar-version">{appVersion}</div>
|
||||
)}
|
||||
|
||||
<div className="admin-nav-search">
|
||||
<label htmlFor="admin-nav-search-input">Find a section</label>
|
||||
<input
|
||||
id="admin-nav-search-input"
|
||||
type="search"
|
||||
value={navSearch}
|
||||
onChange={e => setNavSearch(e.target.value)}
|
||||
placeholder="Search admin..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredAdminViewOptions.length === 0 ? (
|
||||
<p className="admin-stats-note admin-nav-empty">No admin sections match this search.</p>
|
||||
) : (
|
||||
filteredAdminViewOptions.map(group => (
|
||||
<div key={group.group} className="admin-nav-group">
|
||||
<span className="admin-nav-label">{group.group}</span>
|
||||
{group.options.map(option => renderAdminNavItem(option.value, option.label))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{mobileNavOpen && <button type="button" className="admin-sidebar-backdrop" aria-label="Close navigation menu" onClick={() => setMobileNavOpen(false)} />}
|
||||
@@ -2863,7 +2927,7 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
{c.message && <div className="admin-dashboard-feed-preview">{c.message.slice(0, 100)}{c.message.length > 100 ? '…' : ''}</div>}
|
||||
</div>
|
||||
))}
|
||||
<button type="button" className="admin-dashboard-card-action" style={{ marginTop: '0.5rem', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }} onClick={() => setAdminView('contact')}>View All Contacts →</button>
|
||||
<button type="button" className="admin-dashboard-card-action" style={{ marginTop: '0.5rem', background: 'none', border: 'none', cursor: 'pointer', padding: 0 }} onClick={() => navigateTo('contact:profile')}>View All Contacts →</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2976,18 +3040,11 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
})()}
|
||||
|
||||
{/* HOMEPAGE */}
|
||||
{adminView === 'homepage' && (
|
||||
{adminView === 'homepage:hero' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Homepage</h2>
|
||||
<p>Controls the hero, share section, PRISM video block, and "Where to Next" navigation cards.</p>
|
||||
</div>
|
||||
|
||||
{renderSectionJumpNav('homepage')}
|
||||
|
||||
<div className="admin-panel-subhead" id="homepage-hero">
|
||||
<h3>Hero</h3>
|
||||
<p>Primary landing page headline, tag line, and hero call-to-action labels.</p>
|
||||
<h2>Hero</h2>
|
||||
<p>Primary landing page headline, tagline, podcast art, and call-to-action button labels.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => f.section === 'hero').map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
@@ -2999,9 +3056,14 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
{(key.includes('ArtUrl') || key.includes('ImageUrl')) && renderImagePreview(form[key] as string, `${label} preview`)}
|
||||
</div>
|
||||
))}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="admin-panel-subhead" id="homepage-share">
|
||||
<h3>Share Section</h3>
|
||||
{adminView === 'homepage:share' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Share Section</h2>
|
||||
<p>Controls the share block that points visitors toward the next step.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => f.section === 'share').map(({ key, label, multiline }) => (
|
||||
@@ -3014,9 +3076,14 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
{key.includes('ImageUrl') && renderImagePreview(form[key] as string, `${label} preview`)}
|
||||
</div>
|
||||
))}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="admin-panel-subhead" id="homepage-prism">
|
||||
<h3>PRISM Block</h3>
|
||||
{adminView === 'homepage:prism' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>PRISM Block</h2>
|
||||
<p>Video URL, intro text, pattern steps, and closing copy for the PRISM section.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => f.section === 'prism').map(({ key, label, multiline }) => (
|
||||
@@ -3027,23 +3094,25 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
: <input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />}
|
||||
</div>
|
||||
))}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="admin-panel-subhead" id="homepage-where-to-next-labels">
|
||||
<h3>Where to Next — Section Labels</h3>
|
||||
{adminView === 'homepage:where-to-next' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Where to Next</h2>
|
||||
<p>Section heading, eyebrow, and navigation cards shown at the bottom of the homepage.</p>
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="field-whereToNextEyebrow">Eyebrow</label>
|
||||
<label htmlFor="field-whereToNextEyebrow">Section Eyebrow</label>
|
||||
<input id="field-whereToNextEyebrow" type="text" value={form.whereToNextEyebrow ?? ''} onChange={e => setForm(f => ({ ...f, whereToNextEyebrow: e.target.value }))} />
|
||||
</div>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="field-whereToNextHeading">Heading</label>
|
||||
<label htmlFor="field-whereToNextHeading">Section Heading</label>
|
||||
<input id="field-whereToNextHeading" type="text" value={form.whereToNextHeading ?? ''} onChange={e => setForm(f => ({ ...f, whereToNextHeading: e.target.value }))} />
|
||||
</div>
|
||||
|
||||
<div className="admin-panel-subhead" id="homepage-where-to-next-cards">
|
||||
<h3>Where to Next — Navigation Cards</h3>
|
||||
<p>Edit the title, description, and link path for each card.</p>
|
||||
</div>
|
||||
<div className="admin-panel-subhead" style={{ marginTop: '1.5rem' }}><h3>Navigation Cards</h3><p>Title, description, and path for each card.</p></div>
|
||||
{(form.whereToNextCards ?? []).map((card, idx) => (
|
||||
<div key={card.id} className="admin-array-row">
|
||||
<div className="admin-array-fields">
|
||||
@@ -3063,11 +3132,7 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
<button type="button" className="btn-admin-remove" onClick={() => setForm(f => ({ ...f, whereToNextCards: (f.whereToNextCards ?? []).filter(c => c.id !== card.id) }))}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="btn-admin-add"
|
||||
onClick={() => setForm(f => ({ ...f, whereToNextCards: [...(f.whereToNextCards ?? []), { id: Date.now().toString(36), title: '', description: '', path: '/' }] }))}
|
||||
>
|
||||
<button type="button" className="btn-admin-add" onClick={() => setForm(f => ({ ...f, whereToNextCards: [...(f.whereToNextCards ?? []), { id: Date.now().toString(36), title: '', description: '', path: '/' }] }))}>
|
||||
+ Add Card
|
||||
</button>
|
||||
{renderSaveStatus()}
|
||||
@@ -3136,21 +3201,14 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* ABOUT */}
|
||||
{adminView === 'about' && (
|
||||
{/* ABOUT — SHOW COPY */}
|
||||
{adminView === 'about:show-copy' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>About</h2>
|
||||
<p>Manage the main show description, Nate bio, and about images.</p>
|
||||
<h2>Show Copy</h2>
|
||||
<p>Top section of the About page — eyebrow, heading, show description paragraphs, and listen button label.</p>
|
||||
</div>
|
||||
|
||||
{renderSectionJumpNav('about')}
|
||||
|
||||
<div className="admin-panel-subhead" id="about-show-copy">
|
||||
<h3>Show Copy</h3>
|
||||
<p>Top section content that explains the show and listening invitation.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => ['aboutShowHeading', 'aboutShowP1', 'aboutShowP2', 'aboutShowEyebrow', 'aboutListenBtnLabel'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
{FIELDS.filter(f => ['aboutShowEyebrow', 'aboutShowHeading', 'aboutShowP1', 'aboutShowP2', 'aboutListenBtnLabel'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
@@ -3160,58 +3218,57 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="admin-field">
|
||||
<label htmlFor="field-aboutVerseArtUrl">Scripture Artwork Image URL</label>
|
||||
<input id="field-aboutVerseArtUrl" type="text" value={form.aboutVerseArtUrl ?? ''} onChange={e => handleChange('aboutVerseArtUrl', e.target.value)} />
|
||||
{renderImageAssetSelector(form.aboutVerseArtUrl ?? '', value => handleChange('aboutVerseArtUrl', value), 'field-aboutVerseArtUrl-asset')}
|
||||
<p className="admin-field-note">Scripture artwork shown under the "About the Show" text.</p>
|
||||
{renderImagePreview(form.aboutVerseArtUrl ?? '', 'Scripture artwork preview')}
|
||||
</div>
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="admin-panel-subhead" id="about-nate-bio">
|
||||
<h3>Nate Bio</h3>
|
||||
<p>Profile eyebrow and biography copy for the About Nate block.</p>
|
||||
{/* ABOUT — BIO */}
|
||||
{adminView === 'about:bio' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Nate Bio</h2>
|
||||
<p>Profile eyebrow, biography copy, and portrait image for the About Nate block.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => ['aboutEyebrow', 'aboutNate'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
<textarea id={`field-${key}`} value={form[key] as string} onChange={e => handleChange(key, e.target.value)} rows={4} />
|
||||
<textarea id={`field-${key}`} value={form[key] as string} onChange={e => handleChange(key, e.target.value)} rows={6} />
|
||||
) : (
|
||||
<input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="admin-panel-subhead" id="about-images">
|
||||
<h3>Images</h3>
|
||||
<p>Portrait and scripture artwork used on the About page.</p>
|
||||
<div className="admin-field">
|
||||
<label htmlFor="field-aboutPhotoUrl">Portrait Image URL</label>
|
||||
<input id="field-aboutPhotoUrl" type="text" value={form.aboutPhotoUrl ?? ''} onChange={e => handleChange('aboutPhotoUrl', e.target.value)} />
|
||||
{renderImageAssetSelector(form.aboutPhotoUrl ?? '', value => handleChange('aboutPhotoUrl', value), 'field-aboutPhotoUrl-asset')}
|
||||
<p className="admin-field-note">Portrait shown on the left side of the About page.</p>
|
||||
{renderImagePreview(form.aboutPhotoUrl ?? '', 'About page portrait preview')}
|
||||
</div>
|
||||
{FIELDS.filter(f => ['aboutPhotoUrl', 'aboutVerseArtUrl'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
<textarea id={`field-${key}`} value={form[key] as string} onChange={e => handleChange(key, e.target.value)} rows={4} />
|
||||
) : (
|
||||
<>
|
||||
<input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />
|
||||
{(key.includes('ImageUrl') || key.includes('PhotoUrl') || key.includes('ArtUrl')) && renderImageAssetSelector(form[key] as string, value => handleChange(key, value), `field-${key}-asset`)}
|
||||
{key === 'aboutPhotoUrl' && <p className="admin-field-note">This controls the portrait shown on the left side of the About page.</p>}
|
||||
{key === 'aboutVerseArtUrl' && <p className="admin-field-note">This controls the scripture artwork shown under the “About the Show” text. It is separate from the Contact page image.</p>}
|
||||
{key === 'aboutPhotoUrl' && renderImagePreview(form[key] as string, 'About page portrait preview')}
|
||||
{key === 'aboutVerseArtUrl' && renderImagePreview(form[key] as string, 'About page scripture artwork preview')}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="admin-panel-subhead" id="about-testimonials">
|
||||
<h3>Listener Testimonials</h3>
|
||||
<p>Quotes shown on the About page and homepage. Add Apple Podcast reviews, listener emails, or any kind words.</p>
|
||||
{/* ABOUT — TESTIMONIALS */}
|
||||
{adminView === 'about:testimonials' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Testimonials</h2>
|
||||
<p>Listener quotes shown on the About page and homepage. Add Apple Podcast reviews, emails, or any kind words.</p>
|
||||
</div>
|
||||
{(form.testimonials ?? []).length === 0 && (
|
||||
<p className="admin-stats-note">No testimonials yet. Add one below.</p>
|
||||
)}
|
||||
{(form.testimonials ?? []).map(item => (
|
||||
<AdminCollapsibleCard
|
||||
key={item.id}
|
||||
title={item.name || 'Unnamed listener'}
|
||||
subtitle={item.source || 'No source'}
|
||||
>
|
||||
<AdminCollapsibleCard key={item.id} title={item.name || 'Unnamed listener'} subtitle={item.source || 'No source'}>
|
||||
<div className="admin-array-row">
|
||||
<div className="admin-array-fields">
|
||||
<div className="admin-field">
|
||||
@@ -3234,22 +3291,16 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
<button type="button" className="btn-admin-add" onClick={() => setForm(f => ({ ...f, testimonials: [...(f.testimonials ?? []), { id: Date.now().toString(36), quote: '', name: '', source: '' }] }))}>
|
||||
+ Add Testimonial
|
||||
</button>
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CONTACT */}
|
||||
{adminView === 'contact' && (
|
||||
{/* CONTACT — PROFILE */}
|
||||
{adminView === 'contact:profile' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Contact</h2>
|
||||
<p>Manage the contact page profile image and contact copy.</p>
|
||||
</div>
|
||||
|
||||
{renderSectionJumpNav('contact')}
|
||||
|
||||
<div className="admin-panel-subhead" id="contact-profile">
|
||||
<h3>Profile</h3>
|
||||
<p>Name, role, headline, and image shown at the top of the contact page.</p>
|
||||
<h2>Profile</h2>
|
||||
<p>Name, role, section eyebrow, page heading, and profile photo shown on the contact page.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => ['contactPhotoUrl', 'contactEyebrow', 'contactHeading', 'contactName', 'contactRole'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
@@ -3265,62 +3316,33 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="admin-panel-subhead" id="contact-intro">
|
||||
<h3>Intro Copy</h3>
|
||||
<p>Intro paragraph, quote, and main contact bullet points.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => ['contactQuote', 'contactIntro', 'contactPoint1', 'contactPoint2'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
<textarea id={`field-${key}`} value={form[key] as string} onChange={e => handleChange(key, e.target.value)} rows={4} />
|
||||
) : (
|
||||
<input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="admin-panel-subhead" id="contact-scripture">
|
||||
<h3>Scripture</h3>
|
||||
</div>
|
||||
{FIELDS.filter(f => ['contactVerse', 'contactVerseRef'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
<textarea id={`field-${key}`} value={form[key] as string} onChange={e => handleChange(key, e.target.value)} rows={4} />
|
||||
) : (
|
||||
<>
|
||||
<input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />
|
||||
{(key.includes('ImageUrl') || key.includes('PhotoUrl')) && renderImageAssetSelector(form[key] as string, value => handleChange(key, value), `field-${key}-asset`)}
|
||||
{(key.includes('ImageUrl') || key.includes('PhotoUrl')) && renderImagePreview(form[key] as string, `${label} preview`)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* PODCAST HUB */}
|
||||
{adminView === 'podcast' && (
|
||||
{/* CONTACT — COPY & SCRIPTURE */}
|
||||
{adminView === 'contact:copy' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Podcast Hub</h2>
|
||||
<p>Manage all podcast-related content from one place.</p>
|
||||
</div>
|
||||
<div className="admin-tabs admin-tabs--podcast">
|
||||
<button type="button" className={`admin-tab${podcastTab === 'current-series' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('current-series')}>Current Series</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'episode-highlights' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-highlights')}>Ep. Highlights</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'finished-books' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('finished-books')}>Finished Books</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'episode-scripts' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('episode-scripts')}>Episode Scripts</button>
|
||||
<button type="button" className={`admin-tab${podcastTab === 'rss-feed' ? ' admin-tab--active' : ''}`} onClick={() => setPodcastTab('rss-feed')}>RSS Feed</button>
|
||||
<h2>Copy & Scripture</h2>
|
||||
<p>Intro paragraph, quote, contact bullet points, and the scripture block.</p>
|
||||
</div>
|
||||
{FIELDS.filter(f => ['contactQuote', 'contactIntro', 'contactPoint1', 'contactPoint2', 'contactVerse', 'contactVerseRef'].includes(f.key)).map(({ key, label, multiline }) => (
|
||||
<div className="admin-field" key={key}>
|
||||
<label htmlFor={`field-${key}`}>{label}</label>
|
||||
{multiline ? (
|
||||
<textarea id={`field-${key}`} value={form[key] as string} onChange={e => handleChange(key, e.target.value)} rows={4} />
|
||||
) : (
|
||||
<input id={`field-${key}`} type="text" value={form[key] as string} onChange={e => handleChange(key, e.target.value)} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{renderSaveStatus()}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* CURRENT SERIES */}
|
||||
{(adminView === 'podcast' && podcastTab === 'current-series') && (
|
||||
{/* PODCAST — CURRENT SERIES */}
|
||||
{adminView === 'podcast:current-series' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Current Series</h2>
|
||||
@@ -3403,8 +3425,8 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* EPISODE HIGHLIGHTS */}
|
||||
{(adminView === 'podcast' && podcastTab === 'episode-highlights') && (
|
||||
{/* PODCAST — EPISODE HIGHLIGHTS */}
|
||||
{adminView === 'podcast:highlights' && (
|
||||
<section className="admin-panel-section" aria-label="Episode highlights">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Episode Highlights</h2>
|
||||
@@ -3467,8 +3489,8 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* FINISHED BOOKS */}
|
||||
{(adminView === 'podcast' && podcastTab === 'finished-books') && (
|
||||
{/* PODCAST — FINISHED BOOKS */}
|
||||
{adminView === 'podcast:finished-books' && (
|
||||
<section className="admin-panel-section" aria-label="Finished books">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Finished Books</h2>
|
||||
@@ -4171,7 +4193,7 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
)}
|
||||
|
||||
{/* EPISODE SCRIPTS */}
|
||||
{(adminView === 'podcast' && podcastTab === 'episode-scripts') && (
|
||||
{adminView === 'podcast:scripts' && (
|
||||
<section className="admin-panel-section">
|
||||
<div className="admin-panel-head">
|
||||
<h2>Episode Scripts</h2>
|
||||
@@ -4247,8 +4269,8 @@ async function handleAssetUpload(event: ChangeEvent<HTMLInputElement>) {
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* RSS FEED */}
|
||||
{(adminView === 'podcast' && podcastTab === 'rss-feed') && (() => {
|
||||
{/* PODCAST — RSS FEED */}
|
||||
{adminView === 'podcast:rss' && (() => {
|
||||
const canonicalUrl = (form as { seo?: { canonicalUrl?: string } }).seo?.canonicalUrl?.replace(/\/$/, '') ?? ''
|
||||
const feedUrl = canonicalUrl ? `${canonicalUrl}/feed.xml` : '/feed.xml'
|
||||
const anchorFeedUrl = 'https://anchor.fm/s/11068d290/podcast/rss'
|
||||
|
||||
+396
-262
@@ -7492,278 +7492,420 @@
|
||||
|
||||
/* ── Admin Shell Layout ── */
|
||||
.admin-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #070707;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #0c0a08;
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
|
||||
.admin-topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #070707;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.2);
|
||||
padding: 0.65rem 1.5rem;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* ── Topbar ── */
|
||||
.admin-topbar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 48px;
|
||||
background: #0f0d0b;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.14);
|
||||
padding: 0 1.25rem;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-topbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
.admin-topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.admin-topbar-ornament {
|
||||
color: var(--brand-gold);
|
||||
font-size: 1rem;
|
||||
}
|
||||
.admin-topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.admin-topbar-title {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1.1rem;
|
||||
color: var(--brand-warm-white);
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
.admin-mobile-menu-btn {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-topbar-sub {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(201,168,76,0.5);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.admin-mobile-menu-btn:hover { background: rgba(201,168,76,0.1); }
|
||||
|
||||
.admin-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.admin-menu-bar {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 1.5px;
|
||||
background: rgba(240,234,216,0.7);
|
||||
border-radius: 1px;
|
||||
transition: background 150ms;
|
||||
}
|
||||
|
||||
.admin-view-picker {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border: 1px solid rgba(201,168,76,0.22);
|
||||
border-radius: 999px;
|
||||
background: rgba(201,168,76,0.06);
|
||||
}
|
||||
.admin-topbar-brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-view-picker-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(240,234,216,0.6);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.admin-topbar-ornament {
|
||||
color: var(--brand-gold);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.admin-view-picker-select {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--brand-warm-white);
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
min-width: 11rem;
|
||||
outline: none;
|
||||
}
|
||||
.admin-topbar-title {
|
||||
font-family: var(--brand-font-heading);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--brand-warm-white);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.admin-topbar-meta {
|
||||
font-size: 0.78rem;
|
||||
color: rgba(240,234,216,0.45);
|
||||
}
|
||||
.admin-topbar-crumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-mobile-menu-btn {
|
||||
display: none;
|
||||
background: transparent;
|
||||
color: var(--brand-gold);
|
||||
border: 1px solid rgba(201,168,76,0.35);
|
||||
border-radius: 999px;
|
||||
padding: 0.45rem 0.9rem;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.admin-crumb-sep {
|
||||
color: rgba(201,168,76,0.35);
|
||||
font-size: 0.9rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-mobile-menu-btn--open {
|
||||
background: rgba(201,168,76,0.15);
|
||||
border-color: rgba(201,168,76,0.6);
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
.admin-crumb-parent {
|
||||
font-size: 0.82rem;
|
||||
color: rgba(240,234,216,0.45);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.admin-crumb-current {
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
color: var(--brand-warm-white);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.admin-sidebar {
|
||||
width: 210px;
|
||||
min-width: 210px;
|
||||
background: #09090a;
|
||||
border-right: 1px solid rgba(201,168,76,0.1);
|
||||
padding: 1.25rem 0 2rem;
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 51px; /* matches topbar height */
|
||||
height: calc(100vh - 51px);
|
||||
}
|
||||
.admin-topbar-meta {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(240,234,216,0.38);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-sidebar-meta-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 1.1rem 1rem;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.08);
|
||||
margin-bottom: 1rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.admin-unsaved-dot {
|
||||
font-size: 0.75rem;
|
||||
color: #e09a40;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.admin-meta-link {
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(240,234,216,0.45);
|
||||
font-size: 0.82rem;
|
||||
padding: 0.25rem 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
transition: color 150ms;
|
||||
}
|
||||
/* ── Topbar buttons ── */
|
||||
.btn-admin-save {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 0.9rem;
|
||||
background: var(--brand-gold);
|
||||
color: #100d07;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 150ms, opacity 150ms;
|
||||
}
|
||||
|
||||
.admin-meta-link:hover {
|
||||
color: var(--brand-warm-white);
|
||||
}
|
||||
.btn-admin-save:disabled { opacity: 0.45; cursor: default; }
|
||||
.btn-admin-save:not(:disabled):hover { background: #d9b85a; }
|
||||
|
||||
.admin-sidebar-version {
|
||||
padding: 0.5rem 1.1rem 0.75rem;
|
||||
font-size: 0.72rem;
|
||||
color: rgba(240,234,216,0.28);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.btn-admin-publish {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 0.9rem;
|
||||
background: rgba(201,168,76,0.12);
|
||||
color: var(--brand-gold);
|
||||
border: 1px solid rgba(201,168,76,0.3);
|
||||
border-radius: 6px;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 150ms, border-color 150ms;
|
||||
}
|
||||
|
||||
.admin-nav-search {
|
||||
display: grid;
|
||||
gap: 0.4rem;
|
||||
padding: 0 1.1rem 1rem;
|
||||
}
|
||||
.btn-admin-publish:hover { background: rgba(201,168,76,0.2); border-color: rgba(201,168,76,0.55); }
|
||||
|
||||
.admin-nav-search label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(240,234,216,0.6);
|
||||
}
|
||||
.btn-admin-ghost {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
padding: 0 0.8rem;
|
||||
background: transparent;
|
||||
color: rgba(240,234,216,0.6);
|
||||
border: 1px solid rgba(240,234,216,0.14);
|
||||
border-radius: 6px;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 150ms, color 150ms;
|
||||
}
|
||||
|
||||
.admin-nav-search input {
|
||||
width: 100%;
|
||||
border: 1px solid rgba(201,168,76,0.18);
|
||||
border-radius: 10px;
|
||||
background: rgba(201,168,76,0.05);
|
||||
color: var(--brand-warm-white);
|
||||
padding: 0.65rem 0.8rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
.btn-admin-ghost:hover { background: rgba(240,234,216,0.06); color: var(--brand-warm-white); }
|
||||
.btn-admin-ghost--active { background: rgba(240,234,216,0.08); color: var(--brand-warm-white); }
|
||||
|
||||
.admin-nav-search input::placeholder {
|
||||
color: rgba(240,234,216,0.34);
|
||||
}
|
||||
/* ── Layout ── */
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.admin-nav-empty {
|
||||
padding: 0 1.1rem;
|
||||
}
|
||||
/* ── Sidebar ── */
|
||||
.admin-sidebar {
|
||||
width: 196px;
|
||||
min-width: 196px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #0d0b09;
|
||||
border-right: 1px solid rgba(201,168,76,0.09);
|
||||
position: sticky;
|
||||
top: 48px;
|
||||
height: calc(100vh - 48px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.admin-nav-group {
|
||||
margin-bottom: 1.15rem;
|
||||
padding-top: 0.55rem;
|
||||
border-top: 1px solid rgba(201, 168, 76, 0.14);
|
||||
}
|
||||
.admin-sidebar-top {
|
||||
padding: 0.75rem 0.85rem 0.5rem;
|
||||
border-bottom: 1px solid rgba(201,168,76,0.07);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-nav-group:first-of-type {
|
||||
padding-top: 0;
|
||||
border-top: 0;
|
||||
}
|
||||
.admin-nav-search input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(201,168,76,0.15);
|
||||
border-radius: 6px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--brand-warm-white);
|
||||
padding: 0.45rem 0.65rem;
|
||||
font-size: 0.82rem;
|
||||
font-family: var(--brand-font-body);
|
||||
transition: border-color 150ms;
|
||||
}
|
||||
|
||||
.admin-nav-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: #ffe3a3;
|
||||
background: rgba(201, 168, 76, 0.22);
|
||||
border: 1px solid rgba(201, 168, 76, 0.45);
|
||||
border-radius: 999px;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);
|
||||
padding: 0.2rem 0.62rem;
|
||||
margin-left: 1.1rem;
|
||||
margin-bottom: 0.42rem;
|
||||
}
|
||||
.admin-nav-search input:focus { outline: none; border-color: rgba(201,168,76,0.4); }
|
||||
.admin-nav-search input::placeholder { color: rgba(240,234,216,0.28); }
|
||||
|
||||
.admin-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: rgba(240,234,216,0.84);
|
||||
padding: 0.4rem 1.1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 150ms, background 150ms;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.admin-sidebar-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0.5rem 0 0.75rem;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(201,168,76,0.15) transparent;
|
||||
}
|
||||
|
||||
.admin-nav-badge {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
padding: 0 0.35rem;
|
||||
border-radius: 999px;
|
||||
background: var(--brand-gold);
|
||||
color: #1a160d;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
.admin-sidebar-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
padding: 0.65rem 0.85rem 0.75rem;
|
||||
border-top: 1px solid rgba(201,168,76,0.07);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.admin-nav-badge--neutral {
|
||||
background: rgba(201,168,76,0.25);
|
||||
color: var(--brand-gold);
|
||||
}
|
||||
.admin-meta-link {
|
||||
display: block;
|
||||
background: none;
|
||||
border: none;
|
||||
color: rgba(240,234,216,0.38);
|
||||
font-size: 0.78rem;
|
||||
font-family: var(--brand-font-body);
|
||||
padding: 0.2rem 0;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
text-align: left;
|
||||
transition: color 150ms;
|
||||
}
|
||||
|
||||
.admin-nav-item:hover {
|
||||
color: #fff6de;
|
||||
background: rgba(201,168,76,0.11);
|
||||
}
|
||||
.admin-meta-link:hover { color: rgba(240,234,216,0.7); }
|
||||
|
||||
.admin-nav-item--active {
|
||||
color: #ffe4a8 !important;
|
||||
background: rgba(201,168,76,0.18);
|
||||
border-left-color: var(--brand-gold);
|
||||
}
|
||||
.admin-sidebar-version {
|
||||
font-size: 0.68rem;
|
||||
color: rgba(240,234,216,0.2);
|
||||
margin-top: 0.35rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ── Content Panel ── */
|
||||
.admin-panel {
|
||||
.admin-nav-empty {
|
||||
padding: 0.5rem 0.85rem;
|
||||
font-size: 0.82rem;
|
||||
color: rgba(240,234,216,0.35);
|
||||
}
|
||||
|
||||
/* ── Nav groups ── */
|
||||
.admin-nav-group {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.admin-nav-group-label {
|
||||
display: block;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.11em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(201,168,76,0.5);
|
||||
padding: 0.55rem 0.85rem 0.2rem;
|
||||
}
|
||||
|
||||
/* ── Flat leaf nav items ── */
|
||||
.admin-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: rgba(240,234,216,0.72);
|
||||
padding: 0.38rem 0.85rem;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 150ms, background 150ms;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-nav-item:hover { color: var(--brand-warm-white); background: rgba(201,168,76,0.07); }
|
||||
|
||||
.admin-nav-item--active {
|
||||
color: #ffe4a8 !important;
|
||||
background: rgba(201,168,76,0.12);
|
||||
border-left-color: var(--brand-gold);
|
||||
}
|
||||
|
||||
.admin-nav-badge {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
padding: 0 0.3rem;
|
||||
border-radius: 999px;
|
||||
background: var(--brand-gold);
|
||||
color: #1a160d;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.admin-nav-badge--neutral {
|
||||
background: rgba(201,168,76,0.22);
|
||||
color: var(--brand-gold);
|
||||
}
|
||||
|
||||
/* ── Parent nav items (accordion) ── */
|
||||
.admin-nav-parent-wrap { }
|
||||
|
||||
.admin-nav-parent {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: rgba(240,234,216,0.72);
|
||||
padding: 0.38rem 0.85rem;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: color 150ms, background 150ms;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-nav-parent:hover { color: var(--brand-warm-white); background: rgba(201,168,76,0.07); }
|
||||
|
||||
.admin-nav-parent--active {
|
||||
color: rgba(240,234,216,0.95);
|
||||
border-left-color: rgba(201,168,76,0.4);
|
||||
}
|
||||
|
||||
.admin-nav-chevron {
|
||||
color: rgba(240,234,216,0.3);
|
||||
flex-shrink: 0;
|
||||
transition: transform 200ms;
|
||||
}
|
||||
|
||||
.admin-nav-chevron--open { transform: rotate(180deg); }
|
||||
|
||||
/* ── Sub-items ── */
|
||||
.admin-nav-sub {
|
||||
padding-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.admin-nav-sub-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 2px solid transparent;
|
||||
color: rgba(240,234,216,0.55);
|
||||
padding: 0.32rem 0.85rem 0.32rem 1.75rem;
|
||||
font-family: var(--brand-font-body);
|
||||
font-size: 0.81rem;
|
||||
font-weight: 400;
|
||||
cursor: pointer;
|
||||
transition: color 150ms, background 150ms;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.admin-nav-sub-item:hover { color: rgba(240,234,216,0.85); background: rgba(201,168,76,0.06); }
|
||||
|
||||
.admin-nav-sub-item--active {
|
||||
color: #ffe4a8 !important;
|
||||
background: rgba(201,168,76,0.1);
|
||||
border-left-color: var(--brand-gold);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Content Panel ── */
|
||||
.admin-panel {
|
||||
flex: 1;
|
||||
padding: 2rem 2.5rem;
|
||||
overflow-y: auto;
|
||||
@@ -8247,27 +8389,31 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.admin-mobile-menu-btn {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.admin-sidebar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 52px;
|
||||
top: 48px;
|
||||
left: 0;
|
||||
z-index: 130;
|
||||
width: min(320px, 84vw);
|
||||
width: min(280px, 84vw);
|
||||
min-width: 0;
|
||||
height: calc(100vh - 52px);
|
||||
height: calc(100vh - 48px);
|
||||
border-right: 1px solid rgba(201,168,76,0.22);
|
||||
box-shadow: 16px 0 40px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 16px 0 48px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
|
||||
.admin-sidebar--open {
|
||||
display: block;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.admin-sidebar-backdrop {
|
||||
display: block;
|
||||
position: fixed;
|
||||
inset: 52px 0 0 0;
|
||||
inset: 48px 0 0 0;
|
||||
z-index: 120;
|
||||
border: none;
|
||||
background: rgba(0, 0, 0, 0.48);
|
||||
@@ -8277,9 +8423,11 @@
|
||||
.admin-panel {
|
||||
padding: 1.25rem 1rem;
|
||||
}
|
||||
.admin-topbar-sub {
|
||||
|
||||
.admin-topbar-meta {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.admin-preview-pane {
|
||||
display: none;
|
||||
}
|
||||
@@ -8650,21 +8798,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Admin Redesign: Topbar state zone ── */
|
||||
.admin-topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.admin-topbar-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
/* ── Admin Redesign: Analytics CSS classes ── */
|
||||
|
||||
/* ── Admin Redesign: Analytics CSS classes ── */
|
||||
.admin-analytics-grid {
|
||||
|
||||
Reference in new issue
Block a user