web-ui · diff
git:20260822.0460bf5 to git:20260823.11c9947
1 added, 1 removed. Audit B to B.
---
description: Web UI visual design, HTMX/Alpine patterns, and frontend conventions
globs: internal/modules/web/**,pkg/views/**,public/**
alwaysApply: false
---
# Web UI conventions
Stack: templ pages/partials, HTMX 2.x, Alpine.js CSP build, DaisyUI v5 + committed `public/css/app.css`. All JS/CSS vendored under `public/` (embedded via `webassets`); no CDN, no in-repo `node_modules` CSS build.
## Visual design (ops console)
Homelab ops console — not marketing, not generic DaisyUI admin. Brand: favicon, `BrandMark`, teal primary. Do not copy `docs/website/` visual language into `/service/web`.
| Axis | Rule |
|------|------|
| Color | Teal only for brand. Semantic colors for status only. Never violet/indigo/purple for brand, focus, Active, or Chart.js palettes. |
| Surfaces | `.flowbot-surface` for tables/panels. DaisyUI `card` + heavy shadow only for interactive containers. |
| Radius | Square-ish (`--flowbot-radius` ≈ `0.375rem`). Status via `.flowbot-chip`. Avoid `rounded-full` pills for badges. |
| Typography | Self-hosted IBM Plex Sans in `public/fonts/` via `custom.css`. Titles: 600 + `tracking-tight`. |
| Shadow | At most one light shadow (`--flowbot-shadow`). No glow rings. |
| Theme | Product themes: **light** and **dark** only. Overlays use `base-*` tokens. |
Tokens: `.flowbot-shell` in `public/css/custom.css`. Chart palettes: teal + greys + semantic only.
Components:
- Page titles: `partials.PageHeader(title, subtitle)` — not ad-hoc `font-bold` / `card-title`
- Status: `.flowbot-chip-*` — not DaisyUI `badge-success|error|…`
- Loading: `partials.PanelSkeleton` for HTMX panels; `partials.HtmxIndicator` on buttons; optional `#flowbot-htmx-progress` (not `body hx-indicator`)
Do / Don't: `.flowbot-surface` not `card bg-base-100 shadow-sm` list shells; teal not purple; no marketing heroes, multi-layer shadows, or hardcoded surface hex.
CSS: DaisyUI via `public/css/app.css`; tokens/chips in `custom.css`; Alpine at `/static/vendor/alpine.csp.min.js`; never load `tailwind-browser.min.js`.
## Template conventions
- Pages (`pkg/views/pages/`): `@layout.Base(title)`. Partials (`pkg/views/partials/`): HTMX fragments. Never mix page+partial in one `.templ`.
- `data-testid="kebab-case"` on interactive elements.
- Never edit generated `*_templ.go`; regenerate after `.templ` changes (`go tool templ generate`).
- No multi-line inline CSS; no `<script>` in partials; no unsanitized `templ.Raw` (use `utils.MarkdownToSafeHTML`); use `templ.URL()` for dynamic paths.
- User-visible strings: [web AGENTS.md § i18n](../../internal/modules/web/AGENTS.md).
## HTMX
- Redirect: `HX-Redirect` + `200`. Form errors: `renderFormError` → `#form-error` slot. Action-only: `toastError` / `setShowToast` — do not Retarget list/table swaps by default.
- OOB cleanup: `hx-swap-oob="delete"`. **Dangerous actions: confirmation required** — see below.
- Dual-channel (`app.js`): HTML/`HX-Retarget` swap inline (no toast); network/timeout/non-HTML toast; `401` → login with `next`.
- Silent failures (CSRF warm-up, cancel races): mark `intentionally silent` in a short comment.
- Success toasts: specific copy (`Config saved`), not generic OK.
## Dangerous actions (confirmation required)
Any control that **destroys, revokes, stops, disables, logs out, or otherwise cannot be trivially undone** must show the shared confirm modal before the request runs. No bare `hx-post` / `hx-delete` / form submit for these.
**Use** `data-confirm*` on the trigger + `confirm.js` (loaded in `layout.Base`; modal via `partials.ConfirmModal`). Do **not** use `hx-confirm` or browser `confirm()`.
| Attribute | Required | Example |
|-----------|----------|---------|
| `data-confirm` | yes | `"Delete this pipeline? Associated run records will also be removed."` |
| `data-confirm-title` | yes | `"Delete Pipeline"` |
| `data-confirm-btn` | yes | `"Delete"` / `"Revoke"` / `"Logout"` |
| `data-confirm-class` | yes | `btn-error` (delete/revoke/logout/stop) or `btn-warning` (pause/disable) |
**Applies to:** delete, revoke, logout, stop/restart apps, disable skills/triggers/pipelines, regenerate secrets/backup codes, dismiss-with-impact, and any irreversible bulk action.
**Alpine / programmatic:** call `window.showConfirmModal({ title, message, confirmText, confirmClass, onConfirm })` — same copy standards as `data-confirm*`.
**Examples:** `partials/token_row.templ`, `partials/pipeline_list.templ`, `layout/base.templ` (logout).
## Alpine (pipeline editor)
- `pipelineEditor()` in `public/js/pipeline-editor.js`; mount with `x-data` / `x-init`.
- - **CSP-safe expressions only** in templates: no `?.`, `??`, `=>`, object/array literals, or `...` in `x-text` / `x-for` / `@click`. Put logic in JS helpers; prefer getter/`x-for` data props; cache-bust Alpine scripts with `?v=` + `version.Buildtags`.
+ - Cache-bust Alpine scripts **and** layout CSS (`app.css`, `custom.css`, …) with `?v=` + `version.Buildtags`.
## Static assets
`public/` embedded (`webassets.go`); served at `/static/*`. Refresh vendors via `scripts/vendor.sh`.