go-htmx-gsd #1
4 changed files with 4456 additions and 1 deletions
|
|
@ -632,6 +632,56 @@ func TestTablosDashboard_Sidebar(t *testing.T) {
|
|||
if !strings.Contains(body, "sidebar-nav-shell") {
|
||||
t.Errorf("body missing 'sidebar-nav-shell'; body: %.300s", body)
|
||||
}
|
||||
if !strings.Contains(body, "page-header") {
|
||||
t.Errorf("expected page-header class in response body")
|
||||
}
|
||||
if !strings.Contains(body, "breadcrumb") {
|
||||
t.Errorf("expected breadcrumb class in response body")
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TestTablosDashboard_Header ----
|
||||
|
||||
// TestTablosDashboard_Header verifies that authenticated GET / renders the page header bar
|
||||
// (NAV-02) with all expected elements.
|
||||
func TestTablosDashboard_Header(t *testing.T) {
|
||||
pool, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
q := sqlc.New(pool)
|
||||
store := auth.NewStore(q)
|
||||
router := newTabloTestRouter(q, store)
|
||||
|
||||
user := preInsertUser(t, ctx, q, "header@example.com", "correct-horse-12")
|
||||
cookieVal, _, err := store.Create(ctx, user.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("store.Create: %v", err)
|
||||
}
|
||||
sessionCookie := &http.Cookie{Name: auth.SessionCookieName, Value: cookieVal}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
req.AddCookie(sessionCookie)
|
||||
rec := httptest.NewRecorder()
|
||||
router.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET / status = %d; want 200", rec.Code)
|
||||
}
|
||||
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "page-header") {
|
||||
t.Errorf("body missing 'page-header'; body: %.300s", body)
|
||||
}
|
||||
if !strings.Contains(body, "header-avatar-menu") {
|
||||
t.Errorf("body missing 'header-avatar-menu'; body: %.300s", body)
|
||||
}
|
||||
if !strings.Contains(body, "Dashboard") {
|
||||
t.Errorf("body missing breadcrumb label 'Dashboard'; body: %.300s", body)
|
||||
}
|
||||
if !strings.Contains(body, "header-search-placeholder") {
|
||||
t.Errorf("body missing 'header-search-placeholder'; body: %.300s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- TestTablosDashboard_ProjectCards ----
|
||||
|
|
|
|||
|
|
@ -87,6 +87,10 @@ func NewRouter(pinger Pinger, staticFS fs.FS, deps AuthDeps, tabloDeps TablosDep
|
|||
r.Get("/", TablosListHandler(tabloDeps))
|
||||
r.Post("/logout", LogoutHandler(deps))
|
||||
r.Get("/account/providers", AccountProvidersHandler(deps))
|
||||
r.Get("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head><title>Settings - Xtablo</title><link rel="stylesheet" href="/static/tailwind.css"/></head><body style="padding:2rem;font-family:sans-serif"><h1>Settings</h1><p>Coming soon.</p><a href="/">Back to dashboard</a></body></html>`))
|
||||
})
|
||||
r.Get("/planning", PlanningPageHandler(planningDeps))
|
||||
// Static segments BEFORE parametric (Pitfall 1 — chi v5 route resolution).
|
||||
r.Get("/tablos/new", TablosNewHandler(tabloDeps))
|
||||
|
|
|
|||
|
|
@ -357,6 +357,215 @@
|
|||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* ── Page header bar ───────────────────────────────────────────────── */
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--color-surface-elevated);
|
||||
border-bottom: 1px solid var(--color-border-panel);
|
||||
margin: -2rem -2rem 1.5rem -2rem; /* cancel .dashboard-main padding on three sides, add bottom gap */
|
||||
}
|
||||
|
||||
.page-header-left {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.page-header-center {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-header-right {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-item--link {
|
||||
color: var(--color-text-brand);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item--link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item--current {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Search placeholder */
|
||||
.header-search-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--color-border-panel);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--color-surface-muted-inverse);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
min-width: 16rem;
|
||||
max-width: 28rem;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Icon buttons (bell, inbox) */
|
||||
.header-icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-icon-button:hover {
|
||||
background: var(--overlay-dark-soft);
|
||||
}
|
||||
|
||||
.header-icon-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Avatar dropdown */
|
||||
.header-avatar-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-avatar-menu > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-avatar-menu > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-avatar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-brand);
|
||||
color: #fff;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header-avatar-initial {
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-avatar-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 0.5rem);
|
||||
min-width: 14rem;
|
||||
background: var(--color-surface-elevated);
|
||||
border: 1px solid var(--color-border-panel);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--shadow-floating-control);
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-dropdown-workspace {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.avatar-dropdown-workspace-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.avatar-dropdown-workspace-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.avatar-dropdown-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-panel-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.avatar-dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-dropdown-item:hover {
|
||||
background: var(--overlay-dark-soft);
|
||||
}
|
||||
|
||||
.avatar-dropdown-item--danger {
|
||||
color: #dc2626; /* red — logout action; no token for this; matches Figma red logout */
|
||||
}
|
||||
|
||||
.avatar-dropdown-item--danger:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.avatar-dropdown-logout-form {
|
||||
display: contents; /* form element itself doesn't affect layout */
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
Section 12 — Dashboard main content area
|
||||
============================================================ */
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue