diff --git a/src/clare/web/app/src/App.js b/src/clare/web/app/src/App.js index 47c3bdf..662a886 100644 --- a/src/clare/web/app/src/App.js +++ b/src/clare/web/app/src/App.js @@ -1,31 +1,42 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; /** - * R1 scaffold entry. Hello-world that proves: - * - Vite serves the SPA on :5173 - * - The proxy to FastAPI on :8767 works (we hit /api/v1/health and render the result) - * - Build chain compiles + * Router + theme. Owned by the foundation (not the per-page agents). * - * Real pages (chat, projects, sessions, broadcast, dashboard) land in R3–R5. + * Route table: + * / → DashboardPage (R5) + * /chat → ChatPage scope=orchestrator (R3) + * /chat/project/:name → ChatPage scope=project (R3) + * /chat/session/:uuid → ChatPage scope=session (R3) + * /projects → ProjectsListPage (R4) + * /projects/:name → ProjectDetailPage (R4) + * /sessions → SessionsPage (R4) + * /broadcast → BroadcastPage (R5) + * + * Agents implement the pages in their respective subdirs. They MUST NOT + * edit this file (route registration) or lib/* (shared infra). */ -import { useEffect, useState } from "react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import { ThemeProvider } from "styled-components"; +import { AppShell } from "./AppShell"; +import { clareTheme } from "./theme"; +import { ChatPage } from "./chat/ChatPage"; +import { ProjectsListPage } from "./projects/ProjectsListPage"; +import { ProjectDetailPage } from "./projects/ProjectDetailPage"; +import { SessionsPage } from "./sessions/SessionsPage"; +import { BroadcastPage } from "./broadcast/BroadcastPage"; +import { DashboardPage } from "./dashboard/DashboardPage"; +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Clare's state is event-sourced + SSE-pushed; aggressive refetch is + // unnecessary. Pages that want push updates use useChatStream or + // their own EventSource. + staleTime: 5_000, + refetchOnWindowFocus: false, + }, + }, +}); export function App() { - const [health, setHealth] = useState(null); - const [error, setError] = useState(null); - useEffect(() => { - const ctrl = new AbortController(); - fetch("/api/v1/health", { signal: ctrl.signal }) - .then((r) => { - if (!r.ok) - throw new Error(`HTTP ${r.status}`); - return r.json(); - }) - .then((h) => setHealth(h)) - .catch((e) => { - if (e.name !== "AbortError") { - setError(String(e)); - } - }); - return () => ctrl.abort(); - }, []); - return (_jsxs("main", { style: { fontFamily: "ui-monospace, monospace", padding: "2rem" }, children: [_jsx("h1", { children: "clare" }), _jsx("p", { children: "R1 scaffold. Vite \u2192 FastAPI proxy check:" }), error ? (_jsxs("pre", { style: { color: "tomato" }, children: ["error: ", error] })) : health ? (_jsx("pre", { children: `status: ${health.status}\nmachine_id: ${health.machine_id}` })) : (_jsx("p", { children: "loading\u2026" })), _jsx("p", { style: { color: "#888", marginTop: "2rem" }, children: "R3: chat surface. R4: projects + sessions. R5: broadcast + dashboard. R6: delete Jinja templates." })] })); + return (_jsx(QueryClientProvider, { client: queryClient, children: _jsx(ThemeProvider, { theme: clareTheme, children: _jsx(BrowserRouter, { children: _jsx(Routes, { children: _jsxs(Route, { element: _jsx(AppShell, {}), children: [_jsx(Route, { index: true, element: _jsx(DashboardPage, {}) }), _jsx(Route, { path: "chat", element: _jsx(ChatPage, { scope: "orchestrator" }) }), _jsx(Route, { path: "chat/project/:name", element: _jsx(ChatPage, { scope: "project" }) }), _jsx(Route, { path: "chat/session/:uuid", element: _jsx(ChatPage, { scope: "session" }) }), _jsx(Route, { path: "projects", element: _jsx(ProjectsListPage, {}) }), _jsx(Route, { path: "projects/:name", element: _jsx(ProjectDetailPage, {}) }), _jsx(Route, { path: "sessions", element: _jsx(SessionsPage, {}) }), _jsx(Route, { path: "broadcast", element: _jsx(BroadcastPage, {}) }), _jsx(Route, { path: "*", element: _jsx(Navigate, { to: "/", replace: true }) })] }) }) }) }) })); } diff --git a/src/clare/web/app/src/App.tsx b/src/clare/web/app/src/App.tsx index c7a6f96..3c2ec1d 100644 --- a/src/clare/web/app/src/App.tsx +++ b/src/clare/web/app/src/App.tsx @@ -1,55 +1,66 @@ /** - * R1 scaffold entry. Hello-world that proves: - * - Vite serves the SPA on :5173 - * - The proxy to FastAPI on :8767 works (we hit /api/v1/health and render the result) - * - Build chain compiles + * Router + theme. Owned by the foundation (not the per-page agents). * - * Real pages (chat, projects, sessions, broadcast, dashboard) land in R3–R5. + * Route table: + * / → DashboardPage (R5) + * /chat → ChatPage scope=orchestrator (R3) + * /chat/project/:name → ChatPage scope=project (R3) + * /chat/session/:uuid → ChatPage scope=session (R3) + * /projects → ProjectsListPage (R4) + * /projects/:name → ProjectDetailPage (R4) + * /sessions → SessionsPage (R4) + * /broadcast → BroadcastPage (R5) + * + * Agents implement the pages in their respective subdirs. They MUST NOT + * edit this file (route registration) or lib/* (shared infra). */ -import { useEffect, useState, type ReactElement } from "react"; -interface Health { - status: string; - machine_id: string; -} +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom"; +import { ThemeProvider } from "styled-components"; +import type { ReactElement } from "react"; + +import { AppShell } from "./AppShell"; +import { clareTheme } from "./theme"; +import { ChatPage } from "./chat/ChatPage"; +import { ProjectsListPage } from "./projects/ProjectsListPage"; +import { ProjectDetailPage } from "./projects/ProjectDetailPage"; +import { SessionsPage } from "./sessions/SessionsPage"; +import { BroadcastPage } from "./broadcast/BroadcastPage"; +import { DashboardPage } from "./dashboard/DashboardPage"; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Clare's state is event-sourced + SSE-pushed; aggressive refetch is + // unnecessary. Pages that want push updates use useChatStream or + // their own EventSource. + staleTime: 5_000, + refetchOnWindowFocus: false, + }, + }, +}); export function App(): ReactElement { - const [health, setHealth] = useState(null); - const [error, setError] = useState(null); - - useEffect((): (() => void) => { - const ctrl = new AbortController(); - fetch("/api/v1/health", { signal: ctrl.signal }) - .then((r: Response): Promise => { - if (!r.ok) throw new Error(`HTTP ${r.status}`); - return r.json() as Promise; - }) - .then((h: Health): void => setHealth(h)) - .catch((e: unknown): void => { - if ((e as { name?: string }).name !== "AbortError") { - setError(String(e)); - } - }); - return (): void => ctrl.abort(); - }, []); - return ( -
-

clare

-

R1 scaffold. Vite → FastAPI proxy check:

- {error ? ( -
error: {error}
- ) : health ? ( -
-          {`status:      ${health.status}\nmachine_id:  ${health.machine_id}`}
-        
- ) : ( -

loading…

- )} -

- R3: chat surface. R4: projects + sessions. R5: broadcast + dashboard. - R6: delete Jinja templates. -

-
+ + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + + ); } diff --git a/src/clare/web/app/src/AppShell.js b/src/clare/web/app/src/AppShell.js new file mode 100644 index 0000000..4e6e48c --- /dev/null +++ b/src/clare/web/app/src/AppShell.js @@ -0,0 +1,49 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +/** + * AppShell — top navigation + content slot. Wrapped by ThemeProvider in App.tsx. + * + * Pages are rendered into the
via react-router's . + * The terminal aesthetic comes from @lilith/react-terminal-themes; we wire + * the "nord" theme by default and let pages override via context if they + * want a different vibe (e.g. orchestrator could go "matrix"). + */ +import { NavLink, Outlet } from "react-router-dom"; +import styled from "styled-components"; +const Shell = styled.div ` + min-height: 100vh; + display: flex; + flex-direction: column; + background: ${({ theme }) => theme.colors?.bg ?? "#0f1115"}; + color: ${({ theme }) => theme.colors?.fg ?? "#d8dee9"}; + font: 14px/1.5 ui-monospace, "SF Mono", Menlo, monospace; +`; +const Nav = styled.nav ` + display: flex; + gap: 1.5rem; + padding: 0.75rem 1.5rem; + border-bottom: 1px solid ${({ theme }) => theme.colors?.border ?? "#2a2f3a"}; + background: ${({ theme }) => theme.colors?.bgAlt ?? "#0a0c11"}; + align-items: center; +`; +const Brand = styled(NavLink) ` + font-weight: bold; + color: ${({ theme }) => theme.colors?.accent ?? "#88c0d0"}; + text-decoration: none; + margin-right: auto; +`; +const NavItem = styled(NavLink) ` + color: ${({ theme }) => theme.colors?.dim ?? "#7a8290"}; + text-decoration: none; + + &.active { + color: ${({ theme }) => theme.colors?.accent ?? "#88c0d0"}; + } +`; +const Main = styled.main ` + flex: 1 1 auto; + display: flex; + min-height: 0; +`; +export function AppShell() { + return (_jsxs(Shell, { children: [_jsxs(Nav, { children: [_jsx(Brand, { to: "/", children: "clare" }), _jsx(NavItem, { to: "/chat", children: "chat" }), _jsx(NavItem, { to: "/projects", children: "projects" }), _jsx(NavItem, { to: "/sessions", children: "sessions" }), _jsx(NavItem, { to: "/broadcast", children: "broadcast" })] }), _jsx(Main, { children: _jsx(Outlet, {}) })] })); +} diff --git a/src/clare/web/app/src/AppShell.tsx b/src/clare/web/app/src/AppShell.tsx new file mode 100644 index 0000000..da99e43 --- /dev/null +++ b/src/clare/web/app/src/AppShell.tsx @@ -0,0 +1,69 @@ +/** + * AppShell — top navigation + content slot. Wrapped by ThemeProvider in App.tsx. + * + * Pages are rendered into the
via react-router's . + * The terminal aesthetic comes from @lilith/react-terminal-themes; we wire + * the "nord" theme by default and let pages override via context if they + * want a different vibe (e.g. orchestrator could go "matrix"). + */ + +import { NavLink, Outlet } from "react-router-dom"; +import styled from "styled-components"; +import type { ReactElement } from "react"; + +const Shell = styled.div` + min-height: 100vh; + display: flex; + flex-direction: column; + background: ${({ theme }): string => theme.colors?.bg ?? "#0f1115"}; + color: ${({ theme }): string => theme.colors?.fg ?? "#d8dee9"}; + font: 14px/1.5 ui-monospace, "SF Mono", Menlo, monospace; +`; + +const Nav = styled.nav` + display: flex; + gap: 1.5rem; + padding: 0.75rem 1.5rem; + border-bottom: 1px solid ${({ theme }): string => theme.colors?.border ?? "#2a2f3a"}; + background: ${({ theme }): string => theme.colors?.bgAlt ?? "#0a0c11"}; + align-items: center; +`; + +const Brand = styled(NavLink)` + font-weight: bold; + color: ${({ theme }): string => theme.colors?.accent ?? "#88c0d0"}; + text-decoration: none; + margin-right: auto; +`; + +const NavItem = styled(NavLink)` + color: ${({ theme }): string => theme.colors?.dim ?? "#7a8290"}; + text-decoration: none; + + &.active { + color: ${({ theme }): string => theme.colors?.accent ?? "#88c0d0"}; + } +`; + +const Main = styled.main` + flex: 1 1 auto; + display: flex; + min-height: 0; +`; + +export function AppShell(): ReactElement { + return ( + + +
+ +
+
+ ); +} diff --git a/src/clare/web/app/src/broadcast/BroadcastPage.js b/src/clare/web/app/src/broadcast/BroadcastPage.js new file mode 100644 index 0000000..b88f9c3 --- /dev/null +++ b/src/clare/web/app/src/broadcast/BroadcastPage.js @@ -0,0 +1,4 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +export function BroadcastPage() { + return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "broadcast" }), _jsx("p", { style: { color: "#7a8290" }, children: "R5 placeholder." })] })); +} diff --git a/src/clare/web/app/src/broadcast/BroadcastPage.tsx b/src/clare/web/app/src/broadcast/BroadcastPage.tsx new file mode 100644 index 0000000..fefcf9d --- /dev/null +++ b/src/clare/web/app/src/broadcast/BroadcastPage.tsx @@ -0,0 +1,13 @@ +/** + * R5 placeholder — broadcast form. Owned by the R5 agent. + */ +import type { ReactElement } from "react"; + +export function BroadcastPage(): ReactElement { + return ( +
+

broadcast

+

R5 placeholder.

+
+ ); +} diff --git a/src/clare/web/app/src/chat/ChatPage.js b/src/clare/web/app/src/chat/ChatPage.js new file mode 100644 index 0000000..254db3a --- /dev/null +++ b/src/clare/web/app/src/chat/ChatPage.js @@ -0,0 +1,11 @@ +import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; +/** + * R3 placeholder — replaced by the orchestrator/project/session chat surface. + * Owned by the R3 agent. Don't edit `App.tsx` or `lib/*` from here. + */ +import { useParams } from "react-router-dom"; +export function ChatPage({ scope }) { + const params = useParams(); + const ref = scope === "project" ? params.name : scope === "session" ? params.uuid : null; + return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsxs("h2", { children: ["chat \u00B7 ", scope, ref ? ` · ${ref}` : ""] }), _jsx("p", { style: { color: "#7a8290" }, children: "R3 placeholder \u2014 agent will replace this." })] })); +} diff --git a/src/clare/web/app/src/chat/ChatPage.tsx b/src/clare/web/app/src/chat/ChatPage.tsx new file mode 100644 index 0000000..a909a2a --- /dev/null +++ b/src/clare/web/app/src/chat/ChatPage.tsx @@ -0,0 +1,22 @@ +/** + * R3 placeholder — replaced by the orchestrator/project/session chat surface. + * Owned by the R3 agent. Don't edit `App.tsx` or `lib/*` from here. + */ +import { useParams } from "react-router-dom"; +import type { ReactElement } from "react"; +import type { ChatScope } from "../lib/types"; + +export interface ChatPageProps { + scope: ChatScope; +} + +export function ChatPage({ scope }: ChatPageProps): ReactElement { + const params = useParams<{ name?: string; uuid?: string }>(); + const ref = scope === "project" ? params.name : scope === "session" ? params.uuid : null; + return ( +
+

chat · {scope}{ref ? ` · ${ref}` : ""}

+

R3 placeholder — agent will replace this.

+
+ ); +} diff --git a/src/clare/web/app/src/dashboard/DashboardPage.js b/src/clare/web/app/src/dashboard/DashboardPage.js new file mode 100644 index 0000000..9f173d5 --- /dev/null +++ b/src/clare/web/app/src/dashboard/DashboardPage.js @@ -0,0 +1,4 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +export function DashboardPage() { + return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "dashboard" }), _jsx("p", { style: { color: "#7a8290" }, children: "R5 placeholder." })] })); +} diff --git a/src/clare/web/app/src/dashboard/DashboardPage.tsx b/src/clare/web/app/src/dashboard/DashboardPage.tsx new file mode 100644 index 0000000..1fd5ca5 --- /dev/null +++ b/src/clare/web/app/src/dashboard/DashboardPage.tsx @@ -0,0 +1,13 @@ +/** + * R5 placeholder — landing dashboard. Owned by the R5 agent. + */ +import type { ReactElement } from "react"; + +export function DashboardPage(): ReactElement { + return ( +
+

dashboard

+

R5 placeholder.

+
+ ); +} diff --git a/src/clare/web/app/src/lib/api.js b/src/clare/web/app/src/lib/api.js new file mode 100644 index 0000000..211d2eb --- /dev/null +++ b/src/clare/web/app/src/lib/api.js @@ -0,0 +1,128 @@ +/** + * Typed fetch wrapper for Clare's JSON API. + * + * Every page calls these — never raw `fetch`. Centralizes: + * - URL construction (relative paths; Vite proxies in dev, same-origin in prod) + * - error translation: non-2xx → `ApiError(status, detail)` + * - JSON parsing with typed return + * - AbortSignal threading so callers can cancel + */ +export class ApiError extends Error { + status; + detail; + constructor(status, detail) { + super(`${status}: ${detail}`); + this.status = status; + this.detail = detail; + this.name = "ApiError"; + } +} +async function request(method, path, options = {}) { + const url = buildUrl(path, options.params); + const init = { + method, + signal: options.signal, + headers: options.body !== undefined ? { "content-type": "application/json" } : undefined, + body: options.body !== undefined ? JSON.stringify(options.body) : undefined, + }; + const resp = await fetch(url, init); + if (!resp.ok) { + let detail = resp.statusText; + try { + const j = await resp.json(); + if (j && typeof j === "object" && "detail" in j) { + detail = String(j.detail); + } + } + catch { + // body wasn't json — keep statusText + } + throw new ApiError(resp.status, detail); + } + // 204 / empty body → cast to T (caller must declare void in that case) + if (resp.status === 204) + return undefined; + return (await resp.json()); +} +function buildUrl(path, params) { + if (!params) + return path; + const usp = new URLSearchParams(); + for (const [k, v] of Object.entries(params)) { + if (v !== undefined && v !== null) + usp.append(k, String(v)); + } + const q = usp.toString(); + return q ? `${path}?${q}` : path; +} +// --------------------------------------------------------------------------- +// Projects +// --------------------------------------------------------------------------- +export async function listProjects(signal) { + const r = await request("GET", "/api/v1/projects", { signal }); + return r.projects; +} +export async function createProject(body, signal) { + return request("POST", "/api/v1/projects", { body, signal }); +} +export async function getProject(nameOrId, signal) { + return request("GET", `/api/v1/projects/${encodeURIComponent(nameOrId)}`, { signal }); +} +// --------------------------------------------------------------------------- +// Tasks +// --------------------------------------------------------------------------- +export async function listTasks(params = {}, signal) { + const r = await request("GET", "/api/v1/tasks", { params, signal }); + return r.tasks; +} +export async function createTask(body, signal) { + return request("POST", "/api/v1/tasks", { body, signal }); +} +export async function updateTask(taskId, body, signal) { + return request("POST", `/api/v1/tasks/${encodeURIComponent(taskId)}`, { body, signal }); +} +// --------------------------------------------------------------------------- +// Assignments +// --------------------------------------------------------------------------- +export async function listAssignments(active = true, signal) { + const r = await request("GET", "/api/v1/assignments", { + params: { active: active ? 1 : 0 }, signal, + }); + return r.assignments; +} +export async function createAssignment(body, signal) { + return request("POST", "/api/v1/assignments", { body, signal }); +} +// --------------------------------------------------------------------------- +// Sessions + pull +// --------------------------------------------------------------------------- +export async function listSessions(signal) { + const r = await request("GET", "/api/v1/sessions", { signal }); + return r.sessions; +} +export async function pull(signal) { + return request("POST", "/api/v1/pull", { signal }); +} +// --------------------------------------------------------------------------- +// Broadcast +// --------------------------------------------------------------------------- +export async function broadcast(body, signal) { + return request("POST", "/api/v1/broadcast", { body, signal }); +} +// --------------------------------------------------------------------------- +// Chat +// --------------------------------------------------------------------------- +export async function listChatMessages(params, signal) { + const r = await request("GET", "/api/v1/chat", { params, signal }); + return r.messages; +} +export async function postChatMessage(body, signal) { + return request("POST", "/api/v1/chat", { body, signal }); +} +// --------------------------------------------------------------------------- +// Autocomplete +// --------------------------------------------------------------------------- +export async function autocomplete(params, signal) { + const r = await request("GET", "/api/v1/autocomplete", { params, signal }); + return r.hits; +} diff --git a/src/clare/web/app/src/lib/sse.js b/src/clare/web/app/src/lib/sse.js new file mode 100644 index 0000000..65c1be8 --- /dev/null +++ b/src/clare/web/app/src/lib/sse.js @@ -0,0 +1,67 @@ +/** + * useChatStream — subscribe to the SSE channel for a chat scope. + * + * The FastAPI endpoint at `/chat/stream?scope=...&scope_ref=...&after_rowid=N` + * emits `event: chat\ndata: ` frames whenever new chat_messages rows + * appear. The HTML payload was for HTMX; we need parsed ChatMessage objects. + * + * The cleanest path is a sibling JSON SSE channel — but plumbing a second + * stream is out of scope here. Instead the hook re-fetches `listChatMessages` + * with the latest known rowid each time a `chat` event fires (and on mount). + * The SSE channel is used only as a wake-up signal. + * + * Hand-rolled hook (no react-query streaming) to keep the dependency surface + * minimal and the wake-up logic explicit. + */ +import { useEffect, useState, useRef } from "react"; +import { listChatMessages } from "./api"; +export function useChatStream({ scope, scopeRef }) { + const [messages, setMessages] = useState([]); + const [connected, setConnected] = useState(false); + const lastRowidRef = useRef(0); + useEffect(() => { + let cancelled = false; + const ctrl = new AbortController(); + async function refresh() { + try { + const rows = await listChatMessages({ scope, scope_ref: scopeRef, after_rowid: lastRowidRef.current, limit: 200 }, ctrl.signal); + if (cancelled || rows.length === 0) + return; + lastRowidRef.current = rows[rows.length - 1].rowid; + setMessages((prev) => mergeById(prev, rows)); + } + catch (e) { + if (e.name === "AbortError") + return; + // Network blips during reconnect are common; swallow rather than + // surface — the SSE channel will fire another wake-up. + } + } + // Initial backfill (rowid 0 → everything). + void refresh(); + const params = new URLSearchParams({ scope, scope_ref: scopeRef ?? "", after_rowid: "0" }); + const es = new EventSource(`/chat/stream?${params.toString()}`); + es.addEventListener("open", () => setConnected(true)); + es.addEventListener("error", () => setConnected(false)); + es.addEventListener("chat", () => void refresh()); + return () => { + cancelled = true; + ctrl.abort(); + es.close(); + }; + }, [scope, scopeRef]); + function ingest(msgs) { + if (msgs.length === 0) + return; + lastRowidRef.current = Math.max(lastRowidRef.current, msgs.reduce((m, r) => Math.max(m, r.rowid), 0)); + setMessages((prev) => mergeById(prev, msgs)); + } + return { messages, connected, ingest }; +} +function mergeById(prev, next) { + const seen = new Set(prev.map((m) => m.rowid)); + const additions = next.filter((m) => !seen.has(m.rowid)); + if (additions.length === 0) + return prev; + return [...prev, ...additions].sort((a, b) => a.rowid - b.rowid); +} diff --git a/src/clare/web/app/src/lib/types.js b/src/clare/web/app/src/lib/types.js new file mode 100644 index 0000000..d926760 --- /dev/null +++ b/src/clare/web/app/src/lib/types.js @@ -0,0 +1,6 @@ +/** + * Wire types mirroring the FastAPI JSON shapes in src/clare/web/api.py. + * Hand-written rather than generated — the surface is small and stable. + * If a field is added to a Pydantic model, mirror it here too. + */ +export {}; diff --git a/src/clare/web/app/src/projects/ProjectDetailPage.js b/src/clare/web/app/src/projects/ProjectDetailPage.js new file mode 100644 index 0000000..0d49d74 --- /dev/null +++ b/src/clare/web/app/src/projects/ProjectDetailPage.js @@ -0,0 +1,9 @@ +import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime"; +/** + * R4 placeholder — single project detail. Owned by the R4 agent. + */ +import { useParams } from "react-router-dom"; +export function ProjectDetailPage() { + const { name } = useParams(); + return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsxs("h2", { children: ["project \u00B7 ", name] }), _jsx("p", { style: { color: "#7a8290" }, children: "R4 placeholder." })] })); +} diff --git a/src/clare/web/app/src/projects/ProjectDetailPage.tsx b/src/clare/web/app/src/projects/ProjectDetailPage.tsx new file mode 100644 index 0000000..1af92bb --- /dev/null +++ b/src/clare/web/app/src/projects/ProjectDetailPage.tsx @@ -0,0 +1,15 @@ +/** + * R4 placeholder — single project detail. Owned by the R4 agent. + */ +import { useParams } from "react-router-dom"; +import type { ReactElement } from "react"; + +export function ProjectDetailPage(): ReactElement { + const { name } = useParams<{ name: string }>(); + return ( +
+

project · {name}

+

R4 placeholder.

+
+ ); +} diff --git a/src/clare/web/app/src/projects/ProjectsListPage.js b/src/clare/web/app/src/projects/ProjectsListPage.js new file mode 100644 index 0000000..10a6dbb --- /dev/null +++ b/src/clare/web/app/src/projects/ProjectsListPage.js @@ -0,0 +1,4 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +export function ProjectsListPage() { + return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "projects" }), _jsx("p", { style: { color: "#7a8290" }, children: "R4 placeholder." })] })); +} diff --git a/src/clare/web/app/src/projects/ProjectsListPage.tsx b/src/clare/web/app/src/projects/ProjectsListPage.tsx new file mode 100644 index 0000000..a5ab339 --- /dev/null +++ b/src/clare/web/app/src/projects/ProjectsListPage.tsx @@ -0,0 +1,13 @@ +/** + * R4 placeholder — projects list. Owned by the R4 agent. + */ +import type { ReactElement } from "react"; + +export function ProjectsListPage(): ReactElement { + return ( +
+

projects

+

R4 placeholder.

+
+ ); +} diff --git a/src/clare/web/app/src/sessions/SessionsPage.js b/src/clare/web/app/src/sessions/SessionsPage.js new file mode 100644 index 0000000..bd86eaf --- /dev/null +++ b/src/clare/web/app/src/sessions/SessionsPage.js @@ -0,0 +1,4 @@ +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +export function SessionsPage() { + return (_jsxs("div", { style: { padding: "1.5rem" }, children: [_jsx("h2", { children: "sessions" }), _jsx("p", { style: { color: "#7a8290" }, children: "R4 placeholder." })] })); +} diff --git a/src/clare/web/app/src/sessions/SessionsPage.tsx b/src/clare/web/app/src/sessions/SessionsPage.tsx new file mode 100644 index 0000000..c3669e6 --- /dev/null +++ b/src/clare/web/app/src/sessions/SessionsPage.tsx @@ -0,0 +1,13 @@ +/** + * R4 placeholder — fleet sessions list. Owned by the R4 agent. + */ +import type { ReactElement } from "react"; + +export function SessionsPage(): ReactElement { + return ( +
+

sessions

+

R4 placeholder.

+
+ ); +} diff --git a/src/clare/web/app/src/theme.js b/src/clare/web/app/src/theme.js new file mode 100644 index 0000000..c9f87ec --- /dev/null +++ b/src/clare/web/app/src/theme.js @@ -0,0 +1,21 @@ +/** + * Terminal-styled theme for styled-components. + * + * Hand-tuned to match the Nord-ish look the prior Jinja CSS used. + * @lilith/react-terminal-themes is available too — agents can import its + * themes for specific surfaces (e.g. the xterm widget in @lilith/ + * react-terminal-ui) without disturbing this base palette. + */ +export const clareTheme = { + colors: { + bg: "#0f1115", + bgAlt: "#0a0c11", + fg: "#d8dee9", + dim: "#7a8290", + accent: "#88c0d0", + border: "#2a2f3a", + warn: "#ebcb8b", + good: "#a3be8c", + bad: "#bf616a", + }, +}; diff --git a/src/clare/web/app/src/theme.ts b/src/clare/web/app/src/theme.ts new file mode 100644 index 0000000..b93f3a4 --- /dev/null +++ b/src/clare/web/app/src/theme.ts @@ -0,0 +1,44 @@ +/** + * Terminal-styled theme for styled-components. + * + * Hand-tuned to match the Nord-ish look the prior Jinja CSS used. + * @lilith/react-terminal-themes is available too — agents can import its + * themes for specific surfaces (e.g. the xterm widget in @lilith/ + * react-terminal-ui) without disturbing this base palette. + */ + +export interface ClareTheme { + colors: { + bg: string; + bgAlt: string; + fg: string; + dim: string; + accent: string; + border: string; + warn: string; + good: string; + bad: string; + }; +} + +export const clareTheme: ClareTheme = { + colors: { + bg: "#0f1115", + bgAlt: "#0a0c11", + fg: "#d8dee9", + dim: "#7a8290", + accent: "#88c0d0", + border: "#2a2f3a", + warn: "#ebcb8b", + good: "#a3be8c", + bad: "#bf616a", + }, +}; + +// Augment styled-components' DefaultTheme so `theme.colors.bg` is typed +// throughout the app without explicit casts. +declare module "styled-components" { + export interface DefaultTheme extends ClareTheme { + readonly _brand?: never; + } +} diff --git a/src/clare/web/app/tsconfig.tsbuildinfo b/src/clare/web/app/tsconfig.tsbuildinfo index 936babd..8be9647 100644 --- a/src/clare/web/app/tsconfig.tsbuildinfo +++ b/src/clare/web/app/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/appshell.tsx","./src/main.tsx","./src/theme.ts","./src/broadcast/broadcastpage.tsx","./src/chat/chatpage.tsx","./src/dashboard/dashboardpage.tsx","./src/lib/api.ts","./src/lib/sse.ts","./src/lib/types.ts","./src/projects/projectdetailpage.tsx","./src/projects/projectslistpage.tsx","./src/sessions/sessionspage.tsx","./vite.config.ts"],"version":"5.9.3"} \ No newline at end of file