bobbylite / bobbylite/service-template
Add UI
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
# Task Brief for Claude Code: Service Template UI
Companion to `NOTES.md`, which specifies the FastAPI + MCP backend. This
brief covers the **frontend**. Read both — the identity seam described in
`NOTES.md` (`src/app/identity/provider.py`) is the backend half of the auth
story specified here.
## Goal
Build a **generic, reusable dashboard UI template** for the service backend.
Like the backend, it must be clonable and renamable for unrelated projects —
no project-specific branding baked into the logic.
Three surfaces:
1. **Agent chat** — conversational UI against the backend's MCP tools
2. **Admin dashboard** — operational overview
3. **Metrics tab** — gauges and charts, heavily animated
Cross-cutting: light + dark theming, pluggable identity providers, and a
local-development switch that bypasses login.
## Stack
Versions verified 2026-08-31. **Re-check with `npm view version`
before pinning** — do not trust this table blindly if months have passed.
| Package | Version | Role |
| --- | --- | --- |
| `vite` | 8.2.2 | Build tool |
| `react` / `react-dom` | 19.2.8 | UI runtime |
| `typescript` | 7.0.2 | Types (the native/Go compiler — much faster) |
| `tailwindcss` | 4.3.3 | Utilities, CSS-first `@theme` config |
| `@tanstack/react-query` | 5.102.8 | Server state, caching, polling |
| `oidc-client-ts` | 3.5.0 | OIDC protocol layer |
| `react-oidc-context` | 3.3.1 | React bindings for the above |
| `motion` | 13.1.1 | JS animation — **only** where CSS genuinely can't |
| `recharts` | 3.10.1 | Line/bar/area charts |
**Use a Vite SPA, not Next.js.** The backend is Python; adding a Node SSR
runtime buys nothing here. The build output is static assets that FastAPI can
serve directly (or a CDN can). Pick React Router or TanStack Router for
routing — verify the current major version at install time.
## Design system
### Token architecture
One set of semantic tokens, defined once, resolved per theme. Use native
`light-dark()` so most tokens need a single declaration:
```css
@layer tokens {
:root {
color-scheme: light dark;
/* Surfaces */
--surface-canvas: light-dark(#ffffff, #0d1117);
--surface-raised: light-dark(#ffffff, #161b22);
--surface-inset: light-dark(#f6f8fa, #010409);
/* Text */
--fg-default: light-dark(#1f2328, #e6edf3);
--fg-muted: light-dark(#59636e, #8b949e);
/* Lines */
--border-default: light-dark(#d1d9e0, #30363d);
/* Semantic accents */
--accent: light-dark(#0969da, #2f81f7);
--success: light-dark(#1a7f37, #3fb950);
--danger: light-dark(#cf222e, #f85149);
--attention: light-dark(#9a6700, #d29922);
--done: light-dark(#8250df, #a371f7);
}
}
```
Theme selection is three-state: `light`, `dark`, `system`. `system` sets no
attribute and lets `color-scheme` resolve it; explicit choices stamp
`data-theme` on `` and override via `:root[data-theme="dark"]`.
Persist the choice in `localStorage`, wrapped in try/catch, and render
correctly when nothing is stored.
**Never** define a color only inside a media query or `[data-theme]` block.
### Dark theme: GitHub
Dark mode follows GitHub's Primer palette — the hex values above are Primer's
dark tokens. The character to reproduce: cool near-black layered surfaces
(`#0d1117` page, `#161b22` raised cards), low-chroma borders (`#30363d`) that
separate by value rather than contrast, and **accent color reserved for
meaning** — blue for navigation and links, green/red/yellow/purple only for
status semantics. Never decorative.
Light mode is a clean neutral companion, not a GitHub clone.
### Agent chat: Google/Material styling
The chat surface deliberately departs from the GitHub aesthetic. It should
read as Material:
- **Flat color fills.** No gradients, no glassmorphism. Solid tonal surfaces.
- **Raised buttons.** Real elevation via layered shadows, a visible lift on
hover, and a press-down on `:active`. Include a ripple on press — do it
with CSS (a pseudo-element scaling from the pointer position set via a
custom property) rather than a library.
- **Raised inputs.** Elevated filled text fields with floating labels that
animate to the top-left on focus/fill. Use `:placeholder-shown` and `:has()`
for the label state — no JS state tracking for a purely visual concern.
- Generous corner radii, deliberate 4px-grid spacing, one type scale.
Elevation tokens must be theme-aware: in dark mode shadows read weakly, so
elevated surfaces also step up in lightness, exactly as Material specifies.
## Layout
```
┌────────────┬───────────────────────────────────┐
│ │ Top bar: title, theme switch, │
│ Sidebar │ user menu │
│ ├───────────────────────────────────┤
│ · Chat │ │
│ · Admin │ Route outlet │
│ · Metrics │ │
└────────────┴───────────────────────────────────┘
```
Collapsible sidebar. The shell is responsive via **container queries**, not
viewport media queries — panels adapt to their own space so they stay correct
when embedded or resized.
## Admin dashboard
Generic and driven by backend data, not hardcoded:
- Service health from `GET /health` (status, uptime, version), polled with
TanStack Query
- Discovered MCP tools — list what the server advertises, with schemas
- Recent activity / request log table with sorting and filtering
- Session and identity info: which IDP authenticated the user, claims,
token expiry
## Metrics tab
Where the animation work is most visible.
**Gauges: hand-rolled, not from a chart library.** A radial gauge is a
`conic-gradient` whose sweep is a registered custom property, which makes it
animatable and interpolatable:
```css
@property --gauge-angle {
syntax: "";
inherits: false;
initial-value: 0deg;
}
.gauge {
background: conic-gradient(
from -90deg,
var(--accent) var(--gauge-angle),
var(--surface-inset) 0
);
transition: --gauge-angle 600ms cubic-bezier(0.2, 0, 0, 1);
}
```
Use Recharts only for conventional line/bar/area series.
### Required modern CSS techniques
Use these deliberately — they are the point of the brief. Verify current
browser support before relying on any of them, and degrade gracefully:
- **`@property`** — typed custom properties, so gradients, angles and numbers
animate at all (the foundation of the gauges)
- **Scroll-driven animations** — `animation-timeline`, `scroll()`, `view()`
to reveal metric cards on scroll with zero JS scroll listeners
- **View Transitions** — `view-transition-name` for route changes and for
cards morphing into detail panels. Use the same-document API; cross-document
is still not in Firefox.
- **`sibling-index()`** — stagger card entrances without per-element inline
delays: `transition-delay: calc((sibling-index() - 1) * 40ms)`
- **`@starting-style`** — entry transitions for toasts, popovers and newly
streamed chat messages, with no first-frame flash
- **`if()`** — inline conditionals, ideal for honoring reduced motion:
`transition-duration: if(media(prefers-reduced-motion: reduce): 0ms; else: 180ms;)`
- **Anchor positioning** — `anchor-name` / `position-anchor` for tooltips,
menus and popovers, with no JS rect measurement
- **Container queries** — including **scroll-state queries** for styling
stuck headers
- **`@scope`** — scope the Material chat styles so they cannot leak into the
GitHub-themed shell
- **Popover API** — native `popover` for menus and dialogs
- **`oklch()` + `color-mix()`** — derive hover/active/disabled variants from
base tokens instead of hand-picking hexes
- **`text-wrap: balance`** on headings, `pretty` on body copy
- **`field-sizing: content`** — the chat composer grows with its content, no
JS autosize
**Motion is a feature, not decoration.** Every animation must respect
`prefers-reduced-motion: reduce`. Animate `transform`, `opacity` and
registered custom properties — never layout properties.
## Authentication: multiple IDPs
Generic OIDC. **Never hardcode a provider.** Any compliant IDP — Entra ID,
Okta, Auth0, Keycloak, Google, Cognito — must work by configuration alone,
using discovery (`/.well-known/openid-configuration`).
Use **Authorization Code + PKCE**, no implicit flow, no client secret in the
browser.
```ts
export interface IdpConfig {
id: string; // "corp-okta"
displayName: string; // rendered on the login button
authority: string; // issuer URL; metadata comes from discovery
clientId: string;
scopes: string[];
}
```
Configuration comes from the environment at build time, or better, from a
runtime `GET /config` endpoint so one build can be promoted across
environments. Multiple configured IDPs render as a provider-picker on the
login screen; a single one skips the picker.
Mirror the backend's seam: define an `AuthProvider` interface, put the OIDC
implementation behind it, and have components depend on the interface. Adding
SPIFFE/SPIRE or swapping providers later must not touch feature code.
### Local development bypass
A config switch that skips login entirely and injects a fake authenticated
user, so the UI can be worked on without an IDP.
**This is the most dangerous thing in the brief. It must fail closed:**
- Driven by an env flag (e.g. `VITE_AUTH_BYPASS`) that is honored **only**
when the Vite mode is not `production`
- Dead-code-eliminated from production bundles — gate on a compile-time
constant so the bypass branch is not even present in a prod build
- A build-time assertion that **fails the build** if the flag is set while
building for production
- Loud, permanent, unmissable in-app banner whenever it is active
- The fake user must exercise the same `AuthProvider` interface as real OIDC,
so bypass and real mode cannot drift
## Directory layout
```
ui/
├── package.json
├── vite.config.ts
├── tsconfig.json
├── .env.example
├── index.html
└── src/
├── main.tsx
├── app/
│ ├── router.tsx
│ └── providers.tsx # query client, auth, theme
├── styles/
│ ├── tokens.css # semantic tokens, both themes
│ ├── base.css # reset, typography
│ └── motion.css # keyframes, scroll timelines
├── auth/
│ ├── types.ts # AuthProvider interface — the seam
│ ├── oidc-provider.tsx # real OIDC implementation
│ ├── bypass-provider.tsx # dev-only, fails closed
│ └── idp-config.ts
├── components/ # generic, presentational
│ ├── ui/ # Button, Input, Card, Dialog...
│ └── chat/ # Material-styled chat surface
├── features/
│ ├── chat/
│ ├── admin/
│ └── metrics/ # gauges, charts, animated tiles
├── lib/
│ ├── api-client.ts # typed fetch against the backend
│ └── mcp-client.ts # MCP tool discovery + invocation
└── hooks/
```
Keep `components/` generic and `features/` composed from it. Feature code
must never reach for a raw color — only tokens.
## Accessibility
Not optional, and not a later pass:
- Both themes meet WCAG AA contrast — verify, don't assume
- Full keyboard navigation; visible focus rings using `:focus-visible`
- Chat streams into an appropriate live region
- Gauges and charts expose accessible text alternatives; color is never the
sole carrier of meaning
- Respect `prefers-reduced-motion` and `prefers-contrast`
## Testing
- **Vitest** + **React Testing Library** for components and hooks
- **Playwright** for flows: login via a mock OIDC provider, theme switching,
chat round-trip
- Explicit tests that the bypass provider is absent from a production build
and that both themes render every token
## Non-goals
- No component library (MUI, shadcn, Chakra) — the styling *is* the work here
- No backend changes; consume the existing endpoints
- No real IDP tenant setup; ship config and docs
- No i18n yet, but do not hardcode strings in a way that blocks it later
Contributor guide
No contributing guide indexed for this repository
Research direction
The issue describes building a complete frontend dashboard with three surfaces (chat, admin, metrics) from scratch. Start by reading the detailed brief and the companion NOTES.md. The work involves setting up a Vite SPA with React, implementing a design system with CSS custom properties, creating multiple feature areas, and integrating authentication. The directory layout in the brief shows where to place files. 'Done' means a fully functional, themed UI that consumes the existing backend endpoints.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react, tailwindcss, typescript, vite
- Domain
- frontend, web-dev
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 30/100