diff --git a/.planning/phases/15-dashboard-tablos/15-RESEARCH.md b/.planning/phases/15-dashboard-tablos/15-RESEARCH.md new file mode 100644 index 0000000..cae8a11 --- /dev/null +++ b/.planning/phases/15-dashboard-tablos/15-RESEARCH.md @@ -0,0 +1,592 @@ +# Phase 15: Dashboard & Tablos - Research + +**Researched:** 2026-05-16 +**Domain:** Go templ layout shell + CSS porting + HTMX dashboard +**Confidence:** HIGH + +## Summary + +Phase 15 replaces the current top-nav `Layout` with a sidebar-based `AppLayout` and reskins the tablo dashboard as a project-card grid. The go-backend (`go-backend/internal/web/views/dashboard_components.templ` + `go-backend/internal/web/ui/app.css`) is the canonical visual reference and source of CSS to port. All architecture decisions are locked in CONTEXT.md — this research confirms their technical feasibility and documents the exact files to touch, Go patterns to follow, and pitfalls to avoid. + +The primary work splits into three parallel tracks: (1) new `AppLayout` templ component in `backend/templates/`, (2) CSS extraction from `go-backend/internal/web/ui/app.css` into a new `backend/internal/web/ui/app.css`, and (3) restyled `TablosDashboard` + `TablosEmptyState` + `TabloCard` in `backend/templates/tablos.templ`. Handler changes are minimal — only the signature of `TablosDashboard` call sites changes. + +**Primary recommendation:** Port the CSS sections verbatim from go-backend's app.css (lines 455–743 cover all sidebar CSS; lines 894–1046 cover project-card CSS). Then create `AppLayout` following the `AuthLayout` pattern already established in Phase 14. + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +#### App Layout Shell +- **D-L01:** Create a new `AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)` templ in `backend/templates/`. All authenticated dashboard pages use `AppLayout`. The old `Layout` templ is preserved but no longer used by dashboard pages (it becomes dead code, can be removed in a future cleanup). +- **D-L02:** `AppLayout` uses the `.dashboard-shell` CSS grid (sidebar + main columns) from `go-backend/internal/web/ui/app.css`. Port `.dashboard-shell`, `.dashboard-sidebar`, `.sidebar-nav-shell`, all `.sidebar-*` CSS, `.sidebar-organization`, and `.dashboard-main` into `backend/internal/web/ui/app.css` (a new CSS file, not auth.css). +- **D-L03:** Active nav item: `AppLayout` receives `activePath string`. A helper function (pure Go, not templ) returns `"is-active"` class when a nav item's href matches `activePath`. Handlers pass the current route path explicitly (e.g., `"/"`, `"/planning"`, `"/tablos"`). +- **D-L04:** Scripts: `AppLayout` loads `htmx.min.js`, `sortable.min.js`, and `discussion-sse.js` — same as the current `Layout`. No change to script loading. + +#### Sidebar Nav Items +- **D-N01:** Match go-backend nav item set: Dashboard (`/`), Tablos (`/` or tablo overview — same as Dashboard for now), Tasks, Planning (`/planning`), Chat, Files, Settings. Use go-backend's `dashboard_components.templ` icon SVGs verbatim. +- **D-N02:** Routes that exist in the backend get real `href` values: Dashboard (`/`), Planning (`/planning`). Routes not yet built as standalone pages (Tasks, Files, Settings) render as visual nav items with no `href` attribute (display only, `cursor: default` on the item). +- **D-N03:** Chat is per-tablo. In the sidebar nav, Chat links to `/` (dashboard) as a placeholder, or is rendered as a visual-only item like Tasks — Claude's discretion. +- **D-N04:** Sidebar-projects section: shows the user's tablos as a list with a colored circle icon (derived from tablo color field) + truncated title. Each item links to `/tablos/{id}`. Section label: "Projects" (English, matching backend convention). + +#### Tablo Project Cards +- **D-C01:** Tablo dashboard uses the `.project-grid` 3-column CSS grid layout from go-backend. Port `.project-grid`, `.project-card`, `.project-card-top`, and related CSS into the new `app.css`. +- **D-C02:** Each project card shows (matching go-backend exactly): colored circle avatar (top-left) + title + edit icon button (top-right) + delete icon button (top-right) + creation date (bottom row). +- **D-C03:** Color avatar: use `tablo.Color` field when set. When null/empty, show a default neutral/gray circle (no derived color, no initials). The circle renders via a small `` styled with background-color, matching go-backend's `.sidebar-project-icon` pattern. +- **D-C04:** Edit and delete action icons use `@ui.IconButton(...)` from the design system (Phase 13 component). The HTMX attributes for edit (`hx-get=/tablos/{id}/edit`) and delete (`hx-delete=/tablos/{id}`) are preserved from the current `TabloCard` implementation. +- **D-C05:** The "New tablo" button and inline create form slot (`#create-form-slot`) move to a section header above the grid, matching go-backend's `.overview-section-heading` pattern. + +#### Tablo Empty State +- **D-E01:** When no tablos exist, use `@ui.EmptyState(ui.EmptyStateProps{...})` — the Phase 13 component. Replace the current raw HTML empty state in `TablosEmptyState()`. Title: "No tablos yet". Description: "Create your first tablo to get started." Action: "New tablo" button. + +#### Sidebar Footer (User/Account) +- **D-F01:** Include a sidebar footer matching go-backend's `.sidebar-organization` section. Shows: avatar circle (first letter of email, colored background) + user email + a small account link. Logout is moved from the old top-nav into this footer area. +- **D-F02:** The account/settings page doesn't exist yet, so the footer button/link only shows the email + logout button. No settings navigation for Phase 15. The `.organization-button` wraps a simple display element, not a dropdown. +- **D-F03:** Port `.sidebar-organization`, `.organization-button`, `.organization-avatar`, `.organization-name`, `.organization-meta` CSS from go-backend into `app.css`. + +### Claude's Discretion +- Exact set of SVG icons for each nav item (copy from go-backend `dashboard_components.templ` — no new icon design needed) +- Whether the sidebar-projects section is a separate templ component or inline in `AppLayout` (prefer separate `SidebarProjectsSection` for testability) +- Exact color for the neutral avatar fallback (use `--color-surface-muted` or similar design token) +- Whether sidebar-footer avatar background color is derived from email hash or a fixed brand color +- Chat nav item: visual-only placeholder or links to `/` dashboard + +### Deferred Ideas (OUT OF SCOPE) +- Sidebar collapse toggle (JS interaction) +- Mobile-responsive sidebar (hamburger menu, slide-in drawer) +- Tablo color picker in the create/edit form +- Tasks, Files, Settings as standalone pages + + +--- + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| DASH-01 | Sidebar uses the go-backend sidebar design (brand section, nav items with icons, tablo list, user/account footer) | AppLayout + SidebarProjectsSection templ components; CSS ported from go-backend app.css lines 455–743 | +| DASH-02 | Tablo list uses project-card layout with color accents, creation date, and action controls | TablosDashboard restyled with `.project-grid` + `.project-card`; CSS from app.css lines 894–1046 | +| DASH-03 | Dashboard empty state uses the empty-state component | `ui.EmptyState` already implemented (Phase 13, `backend/internal/web/ui/empty_state.templ`), ready to use | + + +--- + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| App layout shell (sidebar + main) | Backend (Go templ) | — | Server-rendered HTML; AppLayout is a templ component | +| Sidebar CSS (`.dashboard-shell`, `.sidebar-nav-shell`, etc.) | Static (CSS file) | — | `backend/internal/web/ui/app.css` serves via `/static/tailwind.css` build | +| Active nav item computation | Backend (pure Go helper) | — | `isActivePath(activePath, href)` pure function, no UI logic | +| Tablo project card grid | Backend (Go templ) | — | `TablosDashboard` restyled; CSS ported | +| Sidebar tablo list (SidebarProjectsSection) | Backend (Go templ) | — | Receives `[]sqlc.Tablo` from `AppLayout` | +| Color avatar rendering | Backend (Go templ) | — | Inline `style=` attribute with `tablo.Color` or fallback token | +| Empty state | Backend (Go templ) | — | `@ui.EmptyState(...)` already in Phase 13 | +| HTMX interactions (edit, delete) | Browser (HTMX attributes) | Backend (handler) | Attributes remain unchanged from current `TabloCard` | + +--- + +## Standard Stack + +### Core (already in backend — no new installs) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| templ | see go.mod | HTML templating for Go | Project standard; all templates use it | +| sqlc | see go.mod | Type-safe SQL | Project standard; `sqlc.Tablo` is the data type | +| chi | see go.mod | HTTP routing | Project standard | +| gorilla/csrf | see go.mod | CSRF token injection | Project standard; `csrf.Token(r)` in every handler | + +### Supporting (CSS tokens) + +| Asset | Location | Purpose | When to Use | +|-------|----------|---------|-------------| +| `backend/internal/web/ui/base.css` | Existing | Design tokens (`var(--...)`) | All new CSS must use these tokens, not raw colors | +| `go-backend/internal/web/ui/app.css` | Reference | Source CSS to port verbatim | Extract only sidebar + project-card sections | +| `backend/static/logo_dark.png` | Existing | Sidebar brand logo | Already copied in Phase 14 | + +**Version verification:** No new packages needed — all required tools are already in the go.mod. [VERIFIED: codebase grep] + +--- + +## Architecture Patterns + +### System Architecture Diagram + +``` +HTTP GET / + | + v +TablosListHandler + |-- Queries.ListTablosByUserWithDiscussionUnread(userID) --> []ListRow + |-- Queries.ListTablosByUser(userID) --> []sqlc.Tablo (for sidebar) + v +templates.TablosDashboard(user, csrfToken, activePath="/", tablos, cardViews) + | + v +AppLayout(title, user, csrfToken, activePath, tablos) + |-- DashboardSidebar + | |-- brand section (logo_dark.png + "XTablo") + | |-- SidebarNavItems (panels/tasks/layers/planning/chat/files) + | |-- SidebarProjectsSection(tablos) + | |-- SidebarOrganizationFooter(user.Email) + v + |--
+ { children... } + |-- section-heading (New tablo button + #create-form-slot) + |-- #tablos-list + |-- if empty: @ui.EmptyState(...) + |-- else: for each card: @TabloProjectCard(card, csrfToken) +``` + +### Recommended Project Structure + +``` +backend/ +├── templates/ +│ ├── app_layout.templ # NEW — AppLayout + SidebarProjectsSection + SidebarNavItem +│ ├── app_layout_helpers.go # NEW — isActivePath, sidebarNavItemClass, sidebarNavItemID +│ ├── tablos.templ # MODIFIED — TablosDashboard, TablosEmptyState, TabloCard +│ ├── layout.templ # UNCHANGED (preserved as dead code) +│ └── ... +├── internal/web/ui/ +│ ├── app.css # NEW — ported from go-backend app.css (sidebar + project-card sections only) +│ ├── base.css # UNCHANGED — design tokens used by app.css +│ └── ... +├── internal/web/ +│ ├── handlers_tablos.go # MODIFIED — TablosDashboard call adds activePath + tablos params +│ ├── handlers_planning.go # MODIFIED — Planning page switches to AppLayout +│ └── ... +``` + +### Pattern 1: AppLayout Templ (follow AuthLayout pattern) + +The AuthLayout in Phase 14 is the established pattern for a top-level HTML shell. `AppLayout` follows the same convention — package `templates`, explicit param list, `{ children... }` slot. + +```go +// Source: backend/templates/auth_layout.templ (Phase 14 pattern) +// backend/templates/app_layout.templ +package templates + +import ( + "backend/internal/auth" + "backend/internal/db/sqlc" +) + +templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo) { + + + + + + { title } + + + +
+ @DashboardSidebar(activePath, tablos, user, csrfToken) +
+ { children... } +
+
+ + + + + +} +``` + +[VERIFIED: codebase read — `AuthLayout` signature + `Layout` script list] + +### Pattern 2: Active Nav Item Helper (pure Go, not templ) + +```go +// Source: go-backend/internal/web/views/home.go — sidebarNavItemClass, isActivePath +// backend/templates/app_layout_helpers.go +package templates + +import "strings" + +func sidebarNavItemClass(active bool) string { + if active { + return "sidebar-nav-item is-active" + } + return "sidebar-nav-item" +} + +func isActivePath(activePath string, href string) bool { + return strings.TrimSpace(activePath) != "" && activePath == href +} +``` + +[VERIFIED: go-backend/internal/web/views/home.go lines 17–37] + +### Pattern 3: SidebarProjectsSection as separate templ + +Per D-N04, sidebar projects render colored circle + truncated title. The go-backend uses `.sidebar-project-icon` as a circle (border-radius: 999px). The backend `sqlc.Tablo` type has `Color pgtype.Text` (nullable). The color span uses inline `style=` only when color is set. + +```go +// Source: go-backend/internal/web/views/dashboard_components.templ lines 103–115 +// backend/templates/app_layout.templ (separate templ component) +templ SidebarProjectsSection(tablos []sqlc.Tablo) { + +} +``` + +[VERIFIED: go-backend templ + backend sqlc.Tablo type from handlers_tablos.go + discussion_forms.go] + +### Pattern 4: TabloProjectCard restyled + +The existing `TabloCard` wraps `@ui.Card(...)`. The new `TabloProjectCard` (or restyled `TabloCard`) uses the `.project-card` CSS directly. Edit/delete use `@ui.IconButton(...)` (already available, Phase 13). + +Key difference from go-backend: backend uses `hx-get=/tablos/{id}/edit` (not a full-page swap) and `hx-post=/tablos/{id}/delete` — **preserve existing HTMX attributes exactly**, only restyle the surrounding HTML. + +```go +// Source: go-backend/internal/web/views/tablos.templ TabloGridCardWithAttrs +// Adapted for backend's HTMX interaction contract +templ TabloProjectCard(card TabloCardView, csrfToken string) { +
+
+
+ @ui.IconButton(ui.IconButtonProps{ + Label: "Edit tablo", Icon: "pencil", + Variant: ui.IconButtonVariantNeutral, Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/" + card.Tablo.ID.String() + "/edit", + // ...existing HTMX attrs... + }, + }) + // delete icon button with existing HTMX attrs +
+
+
+ + +

{ card.Tablo.Title }

+
+
+ // creation date from card.Tablo.CreatedAt +
+
+} +``` + +[ASSUMED: exact HTMX attribute targets need to be verified against current TabloDeleteButtonFragment and edit handler behavior before writing the final template] + +### Pattern 5: Handler Changes — TablosDashboard Call Sites + +Currently, `TablosDashboard(user, csrfToken, tabloCards)` is called from `TablosListHandler`. Per D-L01, the new signature is `TablosDashboard(user, csrfToken, activePath, tablos, cardViews)` where `tablos []sqlc.Tablo` feeds the sidebar and `cardViews []TabloCardView` feeds the grid. + +The tablos list is **already fetched** by `TablosListHandler` — it calls `ListTablosByUserWithDiscussionUnread` which returns `[]ListRow`. We need a second lightweight query `ListTablosByUser(userID)` → `[]sqlc.Tablo` for the sidebar (or derive from existing rows). + +```go +// Source: backend/internal/db/sqlc/tablos.sql.go line 88–100 +// ListTablosByUser already exists: +// func (q *Queries) ListTablosByUser(ctx context.Context, userID uuid.UUID) ([]Tablo, error) +``` + +[VERIFIED: grep in backend/internal/db/sqlc/tablos.sql.go] + +**Simpler option:** Derive `[]sqlc.Tablo` from the already-fetched unread rows without a second DB query: + +```go +// In TabloCardsFromUnreadRows, each row already contains all Tablo fields. +// We can produce []sqlc.Tablo by extracting the Tablo field from each TabloCardView. +tablos := make([]sqlc.Tablo, 0, len(cardViews)) +for _, cv := range cardViews { + tablos = append(tablos, cv.Tablo) +} +``` + +This avoids an extra query. [ASSUMED: confirm this approach in planning since it avoids a new DB call but relies on already-fetched data order] + +### Pattern 6: Planning and Other Layout Callers + +Currently `planning.templ` calls `@Layout(...)`. Per D-L01, it must switch to `@AppLayout(...)`. Planning handler (`PlanningPageHandler`) does not currently fetch tablos. It will need to fetch `[]sqlc.Tablo` for the sidebar. The `PlanningDeps` struct already has `Queries *sqlc.Queries`, so `deps.Queries.ListTablosByUser(ctx, user.ID)` is available. + +Pages that call `@Layout(...)` today and need to switch: +- `tablos.templ`: `TablosDashboard`, `TabloDetailPage`, `TabloNotFoundPage` +- `planning.templ`: `PlanningPage` +- `account_providers.templ`: `AccountProvidersPage` (uses `@Layout`) + +[VERIFIED: grep of Layout( in backend/templates/*.templ] + +### Anti-Patterns to Avoid + +- **Re-using `.sidebar-project-icon` as a colored SVG wrapper:** In go-backend it wraps an SVG icon. In this phase, per D-C03, it should wrap a plain colored `` (no SVG) — the color is the only visual indicator. +- **Nesting OOB elements inside AppLayout:** OOB HTMX fragments (`hx-swap-oob`) must be top-level siblings, not nested under AppLayout. This pattern is already established in `TabloCardWithOOBFormClear`. +- **Hard-coded colors in app.css:** All color values must reference `var(--...)` design tokens from `base.css`. The go-backend app.css already does this — port verbatim. +- **Two queries where one suffices:** `ListTablosByUserWithDiscussionUnread` already returns all tablo fields; derive `[]sqlc.Tablo` from it rather than issuing a second query in `TablosListHandler`. + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Empty state component | Raw HTML `
` with inline styles | `@ui.EmptyState(ui.EmptyStateProps{...})` | Phase 13 component; `.ui-empty-state` CSS already loaded | +| Icon buttons (edit/delete on card) | Raw `