Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.104.1",
"@auto-code/ui": "*",
"@codemirror/lang-css": "^6.3.1",
"@codemirror/lang-html": "^6.4.11",
"@codemirror/lang-javascript": "^6.2.4",
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/renderer/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ initSentryRenderer().catch((err) => {
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
import '@auto-code/ui/tokens.css';
import './styles/globals.css';

ReactDOM.createRoot(document.getElementById('root')!).render(
Expand Down
3 changes: 2 additions & 1 deletion apps/web-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@
"test:e2e:report": "playwright show-report"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@auto-code/ui": "*",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.3",
Expand Down
1 change: 1 addition & 0 deletions apps/web-frontend/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import "./i18n";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "@auto-code/ui/tokens.css";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
Expand Down
33 changes: 33 additions & 0 deletions libs/ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# @auto-code/ui

Shared Auto Code design system and UI components, consumed by both the Electron
desktop app (`apps/frontend`) and the web app (`apps/web-frontend`). Presentational
and transport-agnostic: components take props/callbacks; data is injected per app
(IPC on desktop, REST/WS on web) via the planned `AutoCodeClient` ports-and-adapters
pattern (introduced at the Kanban pilot — see `docs/strategy/roadmap.md`, Track D).

Source of design truth: `.lazyweb/mockups/` (`_desktop-tokens.css` + screen mockups).

## Current contents (U0 foundation)

- `src/tokens/tokens.css` — design tokens (light + `[data-theme="dark"]`). Import once
per app: `import '@auto-code/ui/tokens.css'`. Variables only — non-breaking.
- `src/theme/ThemeProvider.tsx` — `ThemeProvider` + `useTheme()`; sets `data-theme`
on `<html>`, resolves `system` from `prefers-color-scheme`, persists the override.
- `src/shell/AppShell.tsx` — layout primitive (sidebar / topbar / main / statusbar slots).

## Usage

```tsx
import '@auto-code/ui/tokens.css';
import { ThemeProvider, AppShell } from '@auto-code/ui';

<ThemeProvider>
<AppShell sidebar={<Sidebar />} topbar={<TopBar />} main={<Page />} />
</ThemeProvider>
```

## Roadmap (Track D)

U0 foundation (this) → U0.4 Sidebar → U0.5 Storybook → U1 Kanban pilot (introduces
`AutoCodeClient`) → canonical screens → chrome → states → remaining views.
28 changes: 28 additions & 0 deletions libs/ui/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@auto-code/ui",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Shared Auto Code design system and UI components (desktop + web).",
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
},
"./tokens.css": "./src/tokens/tokens.css"
},
"sideEffects": [
"*.css"
],
"scripts": {
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.2.7",
"typescript": "^6.0.2"
}
}
97 changes: 97 additions & 0 deletions libs/ui/src/client/AutoCodeClient.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { createContext, useCallback, useContext, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
import type { UiTask } from './types';

/**
* The data port the shared UI depends on. Each app provides a concrete adapter
* (Electron → IPC, web → REST/WS) so `libs/ui` stays transport-agnostic — this
* is the "ports and adapters" seam introduced at the Kanban pilot (U1).
*/
export interface AutoCodeClient {
/** Fetch the current task/spec list for the active workspace. */
listTasks(): Promise<UiTask[]>;
/**
* Optional realtime subscription. Returns an unsubscribe function; when
* provided, useTasks() refreshes from the pushed snapshots.
*/
subscribeTasks?(onChange: (tasks: UiTask[]) => void): () => void;
}

const AutoCodeClientContext = createContext<AutoCodeClient | null>(null);

export interface AutoCodeClientProviderProps {
client: AutoCodeClient;
children: ReactNode;
}

export function AutoCodeClientProvider({
client,
children,
}: AutoCodeClientProviderProps) {

Check warning on line 30 in libs/ui/src/client/AutoCodeClient.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=OBenner_Auto-Coding&issues=AZ76xuUIWYpOdl3gwRdt&open=AZ76xuUIWYpOdl3gwRdt&pullRequest=364
return (
<AutoCodeClientContext.Provider value={client}>
{children}
</AutoCodeClientContext.Provider>
);
}

export function useAutoCodeClient(): AutoCodeClient {
const client = useContext(AutoCodeClientContext);
if (!client) {
throw new Error('useAutoCodeClient must be used within an AutoCodeClientProvider');
}
return client;
}

export interface UseTasksResult {
tasks: UiTask[];
loading: boolean;
error: Error | null;
reload: () => void;
}

/**
* Loads tasks from the injected AutoCodeClient and, when the adapter supports
* it, keeps them live via subscribeTasks. Presentational components (e.g.
* KanbanBoard) receive the result as props.
*/
export function useTasks(): UseTasksResult {
const client = useAutoCodeClient();
const [tasks, setTasks] = useState<UiTask[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [reloadKey, setReloadKey] = useState(0);

const reload = useCallback(() => setReloadKey((key) => key + 1), []);

useEffect(() => {
let active = true;
setLoading(true);
setError(null);

client
.listTasks()
.then((next) => {
if (active) setTasks(next);
})
.catch((err: unknown) => {
if (active) {
setError(err instanceof Error ? err : new Error(String(err)));
}
})
.finally(() => {
if (active) setLoading(false);
});

const unsubscribe = client.subscribeTasks?.((next) => {
if (active) setTasks(next);
});

return () => {
active = false;
unsubscribe?.();
};
}, [client, reloadKey]);

return { tasks, loading, error, reload };
}
23 changes: 23 additions & 0 deletions libs/ui/src/client/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type TaskStatus = 'draft' | 'running' | 'review' | 'done';

export type BadgeTone = 'good' | 'info' | 'warn' | 'bad' | 'neutral';

export interface UiTaskBadge {
label: string;
tone?: BadgeTone;
}

/**
* The lowest-common task shape the Kanban board renders. Each app maps its own
* model (desktop spec/task over IPC, web task over REST) into this shape via its
* AutoCodeClient adapter, so the board stays transport- and source-agnostic.
*/
export interface UiTask {
id: string;
title: string;
status: TaskStatus;
description?: string;
badges?: UiTaskBadge[];
/** 0–100 progress, typically for running tasks. */
progress?: number;
}
2 changes: 2 additions & 0 deletions libs/ui/src/css.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Allow importing CSS side-effect files (resolved by the consuming app's bundler).
declare module '*.css';
22 changes: 22 additions & 0 deletions libs/ui/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export { ThemeProvider, useTheme } from './theme/ThemeProvider';
export type { Theme, ResolvedTheme, ThemeProviderProps } from './theme/ThemeProvider';
export { AppShell } from './shell/AppShell';
export type { AppShellProps } from './shell/AppShell';
export { Sidebar } from './shell/Sidebar';
export type { SidebarProps } from './shell/Sidebar';
export type { SidebarSection, SidebarItem } from './shell/nav-config';

// Data layer (ports & adapters) + Kanban pilot (U1)
export {
AutoCodeClientProvider,
useAutoCodeClient,
useTasks,
} from './client/AutoCodeClient';
export type {
AutoCodeClient,
AutoCodeClientProviderProps,
UseTasksResult,
} from './client/AutoCodeClient';
export type { UiTask, UiTaskBadge, TaskStatus, BadgeTone } from './client/types';
export { KanbanBoard, DEFAULT_KANBAN_COLUMNS } from './screens/KanbanBoard';
export type { KanbanBoardProps, KanbanColumn } from './screens/KanbanBoard';
139 changes: 139 additions & 0 deletions libs/ui/src/screens/KanbanBoard.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
.ac-kanban {
display: grid;
grid-template-columns: repeat(4, minmax(220px, 1fr));
gap: 14px;
align-items: start;
padding: 18px 28px 28px;
font-family: var(--font-sans);
}

.ac-kanban__column {
min-width: 0;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--soft);
}

.ac-kanban__column-head {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
}

.ac-kanban__column-title {
margin: 0;
font-size: 13px;
font-weight: 780;
}

.ac-kanban__count {
color: var(--quiet);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
}

.ac-kanban__column--draft .ac-kanban__column-head {
color: var(--muted);
}
.ac-kanban__column--running .ac-kanban__column-head {
color: var(--blue);
}
.ac-kanban__column--review .ac-kanban__column-head {
color: var(--amber);
}
.ac-kanban__column--done .ac-kanban__column-head {
color: var(--green);
}

.ac-kanban__column-body {
display: grid;
gap: 10px;
padding: 10px;
}

.ac-kanban__card {
display: grid;
gap: 8px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--panel);
text-align: left;
}

.ac-kanban__card[role='button'] {
cursor: pointer;
}

.ac-kanban__card-id {
color: var(--quiet);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
}

.ac-kanban__card-title {
margin: 0;
color: var(--ink);
font-size: 13.5px;
line-height: 1.3;
font-weight: 760;
}

.ac-kanban__card-desc {
margin: 0;
color: var(--muted);
font-size: 12px;
line-height: 1.4;
}

.ac-kanban__card-badges {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}

.ac-kanban__badge {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 7px;
border-radius: 6px;
font-size: 10.5px;
font-weight: 780;
white-space: nowrap;
}

.ac-kanban__badge--good { color: var(--green); background: var(--green-soft); }
.ac-kanban__badge--info { color: var(--blue); background: var(--blue-soft); }
.ac-kanban__badge--warn { color: var(--amber); background: var(--amber-soft); }
.ac-kanban__badge--bad { color: var(--red); background: var(--red-soft); }
.ac-kanban__badge--neutral { color: var(--muted); background: var(--rail); }

.ac-kanban__progress {
height: 5px;
overflow: hidden;
border-radius: 999px;
background: #e6e6df;
}

.ac-kanban__progress > span {
display: block;
height: 100%;
background: var(--blue);
}

@media (max-width: 1180px) {
.ac-kanban {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

@media (max-width: 760px) {
.ac-kanban {
grid-template-columns: 1fr;
padding: 14px;
}
}
Loading
Loading