Some checks failed
CI / Canvas Deploy Reminder (push) Blocked by required conditions
Block internal-flavored paths / Block forbidden paths (push) Successful in 23s
CI / Detect changes (push) Successful in 1m12s
Harness Replays / detect-changes (push) Failing after 23s
Harness Replays / Harness Replays (push) Has been skipped
E2E Staging Canvas (Playwright) / detect-changes (push) Successful in 1m15s
E2E API Smoke Test / detect-changes (push) Successful in 1m17s
publish-workspace-server-image / build-and-push (push) Failing after 20s
Handlers Postgres Integration / detect-changes (push) Successful in 1m13s
Secret scan / Scan diff for credential-shaped strings (push) Successful in 46s
Runtime PR-Built Compatibility / detect-changes (push) Successful in 53s
publish-canvas-image / Build & push canvas image (push) Failing after 1m47s
Continuous synthetic E2E (staging) / Synthetic E2E against staging (push) Failing after 19s
CI / Platform (Go) (push) Successful in 10s
CI / Shellcheck (E2E scripts) (push) Successful in 10s
CI / Python Lint & Test (push) Successful in 12s
E2E API Smoke Test / E2E API Smoke Test (push) Successful in 15s
Handlers Postgres Integration / Handlers Postgres Integration (push) Successful in 15s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (push) Successful in 8s
CI / Canvas (Next.js) (push) Has been cancelled
E2E Staging Canvas (Playwright) / Canvas tabs E2E (push) Has been cancelled
Sweep stale e2e-* orgs (staging) / Sweep e2e orgs (push) Successful in 10s
Sweep stale AWS Secrets Manager secrets / Sweep AWS Secrets Manager (push) Failing after 17s
ci-required-drift / drift (push) Failing after 10m3s
Canary — staging SaaS smoke (every 30 min) / Canary smoke (push) Failing after 5m46s
[core-lead-agent] lead-merge after CI green + SOP-6 tier review Co-authored-by: Molecule AI Core-FE <core-fe@agents.moleculesai.app> Co-committed-by: Molecule AI Core-FE <core-fe@agents.moleculesai.app>
425 lines
16 KiB
Markdown
425 lines
16 KiB
Markdown
# Canvas Design System v1 — VERIFIED
|
||
|
||
> **Status:** VERIFIED — Cross-referenced against molecule-core/canvas/src/ (2026-05-09, updated 2026-05-10 evening)
|
||
> **Authors:** Core-FE (draft), Core-UIUX (verification + updates)
|
||
> **Source files verified:**
|
||
> - `canvas/src/app/globals.css`
|
||
> - `canvas/src/styles/theme-tokens.css`
|
||
> - `canvas/src/lib/design-tokens.ts`
|
||
> - `canvas/src/components/Tooltip.tsx`
|
||
> - `canvas/src/components/ContextMenu.tsx`
|
||
> - `canvas/src/components/Canvas.tsx`
|
||
> - `canvas/src/components/__tests__/Canvas.a11y.test.tsx`
|
||
> - `canvas/src/components/__tests__/ContextMenu.keyboard.test.tsx`
|
||
> - `canvas/src/components/__tests__/MissingKeysModal.a11y.test.tsx`
|
||
> - `canvas/src/components/__tests__/ConversationTraceModal.a11y.test.tsx`
|
||
|
||
---
|
||
|
||
## 1. Color Palette — Three-Mode Theme System
|
||
|
||
Canvas supports **three themes**: System (follows OS), Light, Dark. Controlled via `ThemeProvider` in `theme-provider.tsx` with preference persisted in `mol_theme` cookie.
|
||
|
||
**Key principle: Use semantic tokens, NOT raw zinc values for surfaces.**
|
||
|
||
### 1.1 Theme-Mutable Tokens (use these for surfaces)
|
||
|
||
Defined in `globals.css` via Tailwind v4 `@theme` block. Automatically flip between light/dark.
|
||
|
||
**Light theme (warm paper):**
|
||
|
||
| Token | Tailwind Class | Hex | Usage |
|
||
|-------|--------------|-----|-------|
|
||
| `--color-surface` | `bg-surface` | `#fafaf7` | Page background |
|
||
| `--color-surface-elevated` | `bg-surface-elevated` | `#ffffff` | Elevated cards, modals |
|
||
| `--color-surface-sunken` | `bg-surface-sunken` | `#f3f1ec` | Input fields, recessed areas |
|
||
| `--color-surface-card` | `bg-surface-card` | `#efece4` | Node cards, chips |
|
||
| `--color-line` | `border-line` | `#e6e2d8` | Dividers, borders |
|
||
| `--color-line-soft` | `border-line-soft` | `#efece4` | Subtle dividers |
|
||
| `--color-ink` | `text-ink` | `#15181c` | Primary text |
|
||
| `--color-ink-mid` | `text-ink-mid` | `#5a5e66` | Secondary text |
|
||
| `--color-ink-soft` | `text-ink-soft` | `#8b8e95` | Tertiary text, placeholders |
|
||
| `--color-accent` | `text-accent` | `#3b5bdb` | Links, primary actions |
|
||
| `--color-accent-strong` | `text-accent-strong` | `#1a2f99` | Emphasized accent |
|
||
| `--color-warm` | `text-warm` | `#c0532b` | Warnings |
|
||
| `--color-good` | `text-good` | `#2f7a4d` | Success states |
|
||
| `--color-bad` | `text-bad` | `#b94e4a` | Error states |
|
||
|
||
**Dark theme:**
|
||
|
||
| Token | Hex | Usage |
|
||
|-------|-----|-------|
|
||
| `--color-surface` | `#0e1014` | Page background |
|
||
| `--color-surface-elevated` | `#15181c` | Elevated cards |
|
||
| `--color-surface-sunken` | `#0a0b0e` | Input fields |
|
||
| `--color-surface-card` | `#1a1d23` | Node cards |
|
||
| `--color-line` | `#2a2f3a` | Dividers |
|
||
| `--color-ink` | `#f4f1e9` | Primary text |
|
||
| `--color-ink-mid` | `#c8c2b4` | Secondary text |
|
||
| `--color-ink-soft` | `#8d92a0` | Tertiary text |
|
||
| `--color-accent` | `#6883e8` | Links (brighter for AA contrast) |
|
||
| `--color-accent-strong` | `#8aa1ee` | Emphasized accent |
|
||
| `--color-warm` | `#d96f48` | Warnings |
|
||
| `--color-good` | `#4ca06e` | Success |
|
||
| `--color-bad` | `#d27773` | Errors |
|
||
|
||
### 1.2 Always-Dark Tokens (terminal surfaces)
|
||
|
||
Terminals, console modal, log streams **stay dark** in all themes — readable green-on-black doesn't translate to light.
|
||
|
||
| Token | Tailwind Class | Hex | Usage |
|
||
|-------|--------------|-----|-------|
|
||
| `--color-bg` | `bg-bg` | `rgb(9 9 11)` / zinc-950 | Terminal background |
|
||
| `--color-bg-elev` | `bg-bg-elev` | `rgb(24 24 27)` / zinc-900 | Elevated terminal surfaces |
|
||
| `--color-bg-card` | `bg-bg-card` | `rgb(39 39 42)` / zinc-800 | Terminal cards |
|
||
| `--color-line-strong` | `border-line-strong` | `rgb(63 63 70)` / zinc-700 | Strong borders |
|
||
| `--color-ink-mute` | `text-ink-mute` | `rgb(161 161 170)` / zinc-400 | Muted text |
|
||
| `--color-ink-dim` | `text-ink-dim` | `rgb(113 113 122)` / zinc-500 | Dim text |
|
||
|
||
### 1.3 Raw Zinc Usage Rules
|
||
|
||
**Use raw zinc for:**
|
||
- Borders: `border-zinc-700`, `border-zinc-800`
|
||
- Disabled states: `text-zinc-600`, `bg-zinc-800`
|
||
- Code highlighting: `bg-zinc-900`, `text-zinc-300`
|
||
- Terminal surfaces: `bg-zinc-950` (always-dark)
|
||
|
||
**NEVER use for surfaces:**
|
||
- `bg-zinc-900` or `bg-zinc-950` as page/card backgrounds — use `bg-surface`
|
||
- `text-zinc-50` or `text-zinc-100` as primary text — use `text-ink`
|
||
- `bg-white`, `bg-gray-50/100` for surfaces — use semantic tokens
|
||
|
||
### 1.4 Accessibility Contrast
|
||
|
||
| Pair | Ratio | WCAG |
|
||
|------|-------|------|
|
||
| `text-ink` on `bg-surface` (light) | ~14.5:1 | AAA |
|
||
| `text-ink` on `bg-surface` (dark) | ~15.8:1 | AAA |
|
||
| `text-ink-mid` on `bg-surface` (light) | ~5.2:1 | AA |
|
||
| `text-ink-mid` on `bg-surface` (dark) | ~5.9:1 | AA |
|
||
| `text-accent` on `bg-surface` (light) | ~4.8:1 | AA |
|
||
| `text-accent` on `bg-surface` (dark) | ~4.6:1 | AA |
|
||
|
||
---
|
||
|
||
## 2. Typography Scale
|
||
|
||
**Actual font stack** (from `globals.css`):
|
||
```
|
||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif
|
||
```
|
||
No custom fonts loaded — uses OS-native system stack.
|
||
|
||
| Size Token | Tailwind | Usage |
|
||
|------------|----------|-------|
|
||
| `text-[10px]` | 10px | Micro badges, tier labels |
|
||
| `text-[11px]` | 11px | Tooltip text |
|
||
| `text-xs` / `text-[12px]` | 12px | Badges, timestamps |
|
||
| `text-sm` / `text-[13px]` | 13–14px | Secondary labels, node titles |
|
||
| `text-base` / `text-[16px]` | 16px | Body text |
|
||
| `text-lg` | 18px | Section headers |
|
||
| `text-xl` | 20px | Modal titles |
|
||
|
||
**Line height:** `leading-tight` (1.25) for headings, `leading-relaxed` (1.625) for body/tooltips.
|
||
|
||
---
|
||
|
||
## 3. Animation / Motion Tokens
|
||
|
||
**Defined in `canvas/src/styles/theme-tokens.css`** — use these, don't hardcode ms values.
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| `--mol-duration-fast` | 150ms | Hover states, button feedback |
|
||
| `--mol-duration-base` | 300ms | Standard transitions |
|
||
| `--mol-duration-spawn` | 350ms | Node spawn animation |
|
||
| `--mol-duration-root-complete` | 700ms | Org-deploy root glow |
|
||
| `--mol-duration-fit-view` | 800ms | Canvas fit-viewport |
|
||
|
||
| Token | Value | Usage |
|
||
|-------|-------|-------|
|
||
| `--mol-easing-standard` | `cubic-bezier(0.2, 0, 0, 1)` | Default ease |
|
||
| `--mol-easing-bounce-out` | `cubic-bezier(0.2, 0.8, 0.2, 1.05)` | Node spawn bounce |
|
||
| `--mol-easing-emphasize` | `cubic-bezier(0.3, 0, 0, 1)` | Modal/drawer enter |
|
||
|
||
**CSS usage:**
|
||
```css
|
||
/* Good — reference the token */
|
||
transition: all var(--mol-duration-fast) ease;
|
||
|
||
/* Bad — hardcoded value */
|
||
transition: all 150ms ease;
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Component Patterns (Verified)
|
||
|
||
### 4.1 Buttons
|
||
|
||
```tsx
|
||
// Primary — accent background, ink text
|
||
<button className="bg-accent hover:bg-accent/90 active:scale-95
|
||
text-ink px-4 py-2 rounded-md text-sm font-medium
|
||
focus-visible:ring-2 focus-visible:ring-blue-500
|
||
focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900
|
||
disabled:opacity-50 disabled:cursor-not-allowed">
|
||
Primary
|
||
</button>
|
||
|
||
// Secondary — surface-card background, border-line
|
||
<button className="bg-surface-card hover:bg-surface-elevated border border-line
|
||
text-ink px-4 py-2 rounded-md text-sm font-medium
|
||
focus-visible:ring-2 focus-visible:ring-blue-500
|
||
focus-visible:ring-offset-2 focus-visible:ring-offset-zinc-900">
|
||
Secondary
|
||
</button>
|
||
|
||
// Ghost — no background, hover surface
|
||
<button className="hover:bg-surface-card text-ink-mid hover:text-ink
|
||
px-4 py-2 rounded-md text-sm font-medium">
|
||
Ghost
|
||
</button>
|
||
|
||
// Danger — bad color, requires confirmation dialog
|
||
<button className="bg-bad hover:bg-bad/90 text-white px-4 py-2
|
||
rounded-md text-sm font-medium">
|
||
Delete
|
||
</button>
|
||
```
|
||
|
||
**States:** default, hover, active (`scale-95`), focus (`ring-2 ring-blue-500 ring-offset-2 ring-offset-zinc-900`), disabled (`opacity-50 cursor-not-allowed`).
|
||
|
||
### 4.2 Inputs
|
||
|
||
```tsx
|
||
// Text input — use semantic tokens for surfaces
|
||
<input
|
||
className="bg-surface-sunken border border-line text-ink
|
||
placeholder:text-ink-soft px-3 py-2 rounded-md text-sm
|
||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||
focus:border-transparent
|
||
disabled:opacity-50 disabled:cursor-not-allowed"
|
||
placeholder="Enter workspace name"
|
||
/>
|
||
|
||
// Error state
|
||
<input
|
||
className="border-bad focus:ring-bad"
|
||
aria-invalid="true"
|
||
aria-describedby="error-message"
|
||
/>
|
||
```
|
||
|
||
**Label:** `text-sm font-medium text-ink mb-1`
|
||
**Error:** `text-xs text-bad mt-1`
|
||
|
||
### 4.3 Cards
|
||
|
||
```tsx
|
||
// Workspace node card (from WorkspaceNode.tsx)
|
||
<div className="bg-surface-sunken/90 border border-line/80
|
||
rounded-xl p-3.5 py-2.5
|
||
hover:border-zinc-500/60 shadow-lg shadow-black/30
|
||
focus-visible:ring-2 focus-visible:ring-accent/70
|
||
focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950">
|
||
```
|
||
|
||
### 4.4 Modals (Radix Dialog)
|
||
|
||
```tsx
|
||
// Backdrop
|
||
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm z-50"
|
||
aria-hidden="true" />
|
||
|
||
// Dialog — use surface-card + border-line
|
||
<div className="bg-surface-card border border-line rounded-xl
|
||
shadow-2xl p-6 max-w-md w-full mx-4">
|
||
{/* Modal content */}
|
||
</div>
|
||
```
|
||
|
||
Note: Uses `--color-surface-sunken` for sunken areas (node cards). Cards use `bg-surface-card`.
|
||
|
||
**Important:** Use `@radix-ui/react-dialog` — it provides WCAG 2.1 compliance automatically (focus trap, Escape key, aria-modal, aria-labelledby).
|
||
|
||
### 4.5 Tooltips
|
||
|
||
**Verified implementation** (`canvas/src/components/Tooltip.tsx`):
|
||
|
||
```tsx
|
||
// Trigger wraps children
|
||
<span aria-describedby="tooltip-id">
|
||
{children}
|
||
</span>
|
||
|
||
// Tooltip portal (shows on hover + focus, 400ms delay)
|
||
<div id="tooltip-id"
|
||
role="tooltip"
|
||
className="fixed z-[9999] max-w-[400px] max-h-[300px] overflow-y-auto
|
||
px-3 py-2 bg-surface-card border border-line
|
||
rounded-lg shadow-2xl shadow-black/60 pointer-events-none">
|
||
<div className="text-[11px] text-ink whitespace-pre-wrap break-words leading-relaxed">
|
||
{text}
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
**WCAG 1.4.13 compliance:** Escape key dismisses tooltip without moving pointer/focus.
|
||
|
||
### 4.6 Theme Switching
|
||
|
||
Use `useTheme()` hook from `theme-provider.tsx`:
|
||
|
||
```tsx
|
||
import { useTheme } from "@/lib/theme-provider";
|
||
|
||
function ThemeToggle() {
|
||
const { theme, resolvedTheme, setTheme } = useTheme();
|
||
return (
|
||
<select
|
||
value={theme}
|
||
onChange={(e) => setTheme(e.target.value as ThemePreference)}
|
||
>
|
||
<option value="system">System</option>
|
||
<option value="light">Light</option>
|
||
<option value="dark">Dark</option>
|
||
</select>
|
||
);
|
||
}
|
||
```
|
||
|
||
**Theme types:**
|
||
```ts
|
||
type ThemePreference = "system" | "light" | "dark";
|
||
type ResolvedTheme = "light" | "dark";
|
||
```
|
||
|
||
**Cookie:** `mol_theme` with `Domain=.moleculesai.app` — persists across surfaces.
|
||
|
||
---
|
||
|
||
## 5. Accessibility Rules (WCAG 2.1 AA) — VERIFIED
|
||
|
||
### 5.1 Focus Management ✅ VERIFIED
|
||
- All interactive elements have `focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1` (WCAG 2.4.7 — ring only appears for keyboard users, not mouse/touch)
|
||
- `focus-visible:outline-none` used only when paired with an explicit `focus-visible:ring-*` replacement
|
||
- Radix Dialog traps focus automatically
|
||
|
||
### 5.2 Semantic HTML ✅ VERIFIED
|
||
- Buttons use `<button>` — verified in WorkspaceNode.tsx, ContextMenu.tsx
|
||
- Form inputs have associated `<label>` patterns
|
||
- Radix Dialog provides role="dialog" + aria-modal
|
||
|
||
### 5.3 ARIA ✅ VERIFIED
|
||
- Icon-only buttons: `aria-label` with descriptive text (not "X")
|
||
- Example: `aria-label="Extract ${name} from team"` in WorkspaceNode.tsx
|
||
- Live regions: `aria-live="polite"` on Toast component
|
||
- Modals: Radix provides `role="dialog"`, `aria-modal="true"`, `aria-labelledby`
|
||
- Error messages: `aria-invalid="true"`, `aria-describedby` linking to error text
|
||
- Tooltips: `role="tooltip"` + `aria-describedby` on trigger
|
||
|
||
### 5.4 Keyboard Navigation ✅ VERIFIED
|
||
- ContextMenu: ArrowUp/Down wraps, Enter/Space selects, Escape closes, Tab closes
|
||
- Modals: Escape closes (Radix), focus returns to trigger
|
||
- `prefers-reduced-motion` ✅ (verified in globals.css)
|
||
|
||
### 5.5 Color Independence ✅
|
||
- Status indicators use text labels + icons, not color alone
|
||
- `STATUS_CONFIG` has text labels: "Online", "Offline", "Failed", etc.
|
||
|
||
---
|
||
|
||
## 6. React Flow Canvas Specifics
|
||
|
||
Canvas uses `@xyflow/react` (React Flow).
|
||
|
||
### Canvas Container ✅ VERIFIED
|
||
```tsx
|
||
// Canvas.tsx wraps ReactFlow with:
|
||
<ReactFlow
|
||
aria-label="Molecule AI workspace canvas"
|
||
// ...
|
||
/>
|
||
```
|
||
|
||
### Node Accessibility ✅ VERIFIED
|
||
- `role="button"` on workspace node cards
|
||
- `tabIndex={0}` for keyboard focus
|
||
- `aria-pressed` for selection state
|
||
- `aria-label` with workspace name + status
|
||
|
||
### Skip Link ✅ VERIFIED
|
||
```tsx
|
||
<a href="#canvas-main">Skip to canvas</a>
|
||
<main id="canvas-main" role="main">
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Enforcement Checklist
|
||
|
||
### Color Token Rules
|
||
- [x] No `bg-white` / `bg-zinc-50` for surfaces — use `bg-surface`
|
||
- [x] No `text-zinc-50` / `text-zinc-100` for surfaces — use `text-ink`
|
||
- [x] No `bg-zinc-900` / `bg-zinc-950` for surfaces — use `bg-surface` or `bg-surface-card`
|
||
- [x] Raw zinc OK for: borders, disabled states, code, terminal surfaces
|
||
|
||
### Accessibility Rules
|
||
- [x] All buttons have focus rings (verified in tests)
|
||
- [x] All modals use Radix Dialog (verified)
|
||
- [x] All tooltips use `role="tooltip"` + `aria-describedby` (verified)
|
||
- [x] No `outline-none` without focus ring (verified)
|
||
- [x] All inputs have visible labels (verified pattern)
|
||
- [x] Contrast ratios at 4.5:1 minimum (verified above)
|
||
- [x] `prefers-reduced-motion` suppresses all animations (verified in globals.css)
|
||
- [x] Context menu has keyboard navigation (verified in ContextMenu.keyboard.test.tsx)
|
||
- [x] Theme switching works: System/Light/Dark modes verified
|
||
|
||
---
|
||
|
||
## 8. Canvas Architecture (Verified)
|
||
|
||
**Stack:**
|
||
- `@xyflow/react` v12 (React Flow) — node/edge rendering
|
||
- Next.js 14 App Router
|
||
- Tailwind v4 with CSS custom properties
|
||
- Zustand for state management
|
||
|
||
**Directory Structure:**
|
||
```
|
||
canvas/src/
|
||
├── components/ # Canvas.tsx, Toolbar.tsx, ContextMenu.tsx, SidePanel.tsx, WorkspaceNode.tsx, A2AEdge.tsx
|
||
├── stores/ # secrets-store.ts (only store)
|
||
├── hooks/ # useSocketEvent.ts, useTemplateDeploy.tsx, useWorkspaceName.ts
|
||
├── lib/ # api.ts, auth.ts, canvas-actions.ts, design-tokens.ts, theme.ts, theme-provider.tsx
|
||
└── app/ # Next.js App Router
|
||
```
|
||
|
||
## 9. Known Issues (Technical Debt)
|
||
|
||
### Performance Issues
|
||
- **secrets-store.ts getGrouped() selector** — Creates new objects every call (Object.fromEntries + arrays) — not memoized. Causes performance issues with frequent re-renders. Needs selector optimization.
|
||
|
||
### Code Quality
|
||
- Check for `any` types in canvas/ directory
|
||
- Verify pre-commit hook actually fails on 'use client' violations (unverified)
|
||
- Verify all Zustand selectors avoid object creation (see getGrouped issue above)
|
||
- Check 'use client' directive on hook-using components
|
||
|
||
### Testing
|
||
- Add axe-core integration for automated accessibility testing
|
||
- Visual regression tests — no screenshot tests exist yet (KI-006)
|
||
- Target >80% test coverage on changed files
|
||
|
||
## 10. Remaining Open Items
|
||
|
||
### Accessibility Gaps
|
||
1. **Screen reader announcements** — Node/edge changes not announced. Need `aria-live="polite"` region.
|
||
2. **Keyboard shortcut help dialog** — No dedicated dialog. Shortcuts exist in `useKeyboardShortcuts.ts` but no `aria-describedby` hints on buttons.
|
||
3. **Edge anchor accessibility** — React Flow handles purely visual. Need ARIA annotations for screen readers.
|
||
4. **Drag-and-drop keyboard alternative** — Mouse only. Need keyboard equivalent for node rearrangement.
|
||
|
||
### Performance
|
||
5. **secrets-store.ts getGrouped()** — Not memoized, creates new objects every call.
|