feat(18-03): add PageHeader component and wire into AppLayout
- Add PageHeader templ component with three-zone layout (breadcrumb left, search placeholder center, bell/inbox/avatar right) - Avatar dropdown uses native details/summary HTML (D-06, no Alpine.js) - Logout form inside dropdown with CSRF token (T-18-03-01 mitigated) - Remove Plan 01 breadcrumb/headerActions stubs from AppLayout body - Wire @PageHeader call inside dashboard-main before children - Add inline JS for avatar dropdown outside-click close behavior
This commit is contained in:
parent
5dc21340b0
commit
c190723538
1 changed files with 94 additions and 6 deletions
|
|
@ -180,6 +180,92 @@ templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User,
|
|||
</aside>
|
||||
}
|
||||
|
||||
// PageHeader renders the full-width top bar with three zones:
|
||||
// left (breadcrumb), center (search placeholder), right (bell, inbox, avatar dropdown).
|
||||
// The avatar dropdown uses native HTML details/summary — no Alpine.js (D-06).
|
||||
templ PageHeader(pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component, user *auth.User, csrfToken string) {
|
||||
<header class="page-header">
|
||||
<!-- LEFT: breadcrumb zone -->
|
||||
<div class="page-header-left">
|
||||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||||
for i, crumb := range breadcrumb {
|
||||
if i > 0 {
|
||||
<span class="breadcrumb-separator" aria-hidden="true">/</span>
|
||||
}
|
||||
if crumb.Href != "" {
|
||||
<a href={ templ.SafeURL(crumb.Href) } class="breadcrumb-item breadcrumb-item--link">{ crumb.Label }</a>
|
||||
} else {
|
||||
<span class="breadcrumb-item breadcrumb-item--current" aria-current="page">{ crumb.Label }</span>
|
||||
}
|
||||
}
|
||||
if len(breadcrumb) == 0 {
|
||||
<span class="breadcrumb-item breadcrumb-item--current">{ pageTitle }</span>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- CENTER: search placeholder -->
|
||||
<div class="page-header-center">
|
||||
<div class="header-search-placeholder" aria-label="Search (coming soon)" role="search">
|
||||
<svg class="header-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16">
|
||||
<circle cx="11" cy="11" r="8"></circle>
|
||||
<path d="m21 21-4.35-4.35"></path>
|
||||
</svg>
|
||||
<span class="header-search-text">Search...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: bell, inbox, avatar -->
|
||||
<div class="page-header-right">
|
||||
<!-- Bell placeholder -->
|
||||
<button class="header-icon-button" aria-label="Notifications (coming soon)" type="button" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"></path>
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Inbox placeholder -->
|
||||
<button class="header-icon-button" aria-label="Inbox (coming soon)" type="button" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
|
||||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"></polyline>
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Avatar dropdown using native details/summary (D-06) -->
|
||||
<details class="header-avatar-menu">
|
||||
<summary class="header-avatar-button" aria-label="User menu">
|
||||
<span class="header-avatar-initial">{ string([]rune(user.Email)[:1]) }</span>
|
||||
</summary>
|
||||
<div class="header-avatar-dropdown">
|
||||
<!-- Workspace / user info -->
|
||||
<div class="avatar-dropdown-workspace">
|
||||
<span class="avatar-dropdown-workspace-name">{ user.Email }</span>
|
||||
<span class="avatar-dropdown-workspace-meta">1 member</span>
|
||||
</div>
|
||||
<hr class="avatar-dropdown-divider"/>
|
||||
<!-- Settings -->
|
||||
<a href="/settings" class="avatar-dropdown-item">Settings</a>
|
||||
<hr class="avatar-dropdown-divider"/>
|
||||
<!-- Logout -->
|
||||
<form method="POST" action="/logout" class="avatar-dropdown-logout-form">
|
||||
@ui.CSRFField(csrfToken)
|
||||
<button type="submit" class="avatar-dropdown-item avatar-dropdown-item--danger">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Per-page header actions slot (Phase 19-22 will populate) -->
|
||||
if headerActions != nil {
|
||||
<div class="page-header-actions">
|
||||
@headerActions
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
}
|
||||
|
||||
// AppLayout is the authenticated HTML shell for all dashboard pages.
|
||||
// It replaces Layout for all authenticated routes in Phase 15+.
|
||||
//
|
||||
|
|
@ -201,18 +287,20 @@ templ AppLayout(title string, user *auth.User, csrfToken string, activePath stri
|
|||
<div class="dashboard-shell">
|
||||
@DashboardSidebar(activePath, tablos, user, csrfToken)
|
||||
<main id="app-main-content" class="dashboard-main">
|
||||
if len(breadcrumb) > 0 {
|
||||
<!-- breadcrumb: Plan 03 will render this properly -->
|
||||
}
|
||||
if headerActions != nil {
|
||||
@headerActions
|
||||
}
|
||||
@PageHeader(pageTitle, breadcrumb, headerActions, user, csrfToken)
|
||||
{ children... }
|
||||
</main>
|
||||
</div>
|
||||
<script src="/static/htmx.min.js" defer></script>
|
||||
<script src="/static/sortable.min.js" defer></script>
|
||||
<script src="/static/discussion-sse.js" defer></script>
|
||||
<script>
|
||||
document.addEventListener('click', function(e) {
|
||||
document.querySelectorAll('details.header-avatar-menu').forEach(function(d) {
|
||||
if (!d.contains(e.target)) { d.removeAttribute('open'); }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue