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
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' blob:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self';" />
<meta name="referrer" content="no-referrer" />
<title>Knowledge Studio</title>
Expand Down
296 changes: 296 additions & 0 deletions plans/041-goap-ui-ux-modernization-and-feature-gaps-2026-06-18.md

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions plans/ADRs/013-semantic-design-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# ADR 013: Semantic Design Tokens & Complete Theme Coverage

## Status
PROPOSED (2026-06-18) — Implementation tracked in `plans/041-goap-ui-ux-modernization-and-feature-gaps-2026-06-18.md` (G-TOKENS, G-PRIMITIVES, G-MOTION).

## Context
The studio has a 4px-grid token layer in `src/styles/tokens.css` with four themes (`app`, `game`, `neural`, `technical`). The system is good but incomplete, and several components bypass it:

1. **Undefined token references** (verified):
- `--border-color` used in `src/features/search/SearchPanel.tsx:270,296` — the canonical token is `--border-default` (`tokens.css:41`). With no fallback the border declaration is invalid and disappears.
- `--surface-primary` / `--surface-secondary` used in `src/features/ai/ChatView.tsx:29,31` and `src/features/ai/AIHarness.tsx:203` — canonical tokens are `--bg-surface` / `--bg-base`. Tool-call/setting backgrounds render transparent across themes.

2. **Partial theme overrides**: `[data-theme='game']` (`tokens.css:123-135`) and others override only a subset of semantic tokens. `--text-muted`, `--bg-overlay`, `--interactive-disabled`, `--status-*`, `--border-focus`, `--border-error`, `--border-subtle`, and shadows inherit light-mode values in dark/high-contrast themes.

3. **Hardcoded colors bypassing themes**:
- Graph nodes/edges: `src/features/graph/GraphView.tsx:182-188,219-225,256-259` (`#2563eb`, `#ef4444`, `#8b5cf6`, `#94a3b8`, `#7c3aed`).
- Status messages: `src/styles/utilities.css:23-32` (`#dcfce7`, `#fee2e2`).
- Knowledge claims: `src/styles/features.css:267-278`.
- Type badges: `src/styles/features.css:650-653`.

4. **No reduced-motion policy**: spinner (`utilities.css:91-98`), skeleton pulse (`utilities.css:100-108`), and chat smooth-scroll (`ChatView.tsx:95-97`) ignore `prefers-reduced-motion`.

5. **Off-token raw values** scattered across feature CSS and inline styles (`3px`, `6px`, `10px`, `13px`, etc.).

## Decision
Adopt a **complete semantic token system** as the single styling source of truth, and forbid undefined-token references and hardcoded colors in feature components.

### 1. Fix undefined tokens
Add intentional aliases in `tokens.css` so legacy references resolve, then migrate call sites to canonical names:

```css
:root {
--border-color: var(--border-default); /* alias — migrate call sites, then remove */
--surface-primary: var(--bg-surface);
--surface-secondary: var(--bg-base);
}
```

### 2. Add semantic token families
Define and theme these (per the four themes, with intentional fallbacks documented):

```css
/* Status surfaces */
--status-success-bg / --status-success-border
--status-warning-bg / --status-warning-border
--status-danger-bg / --status-danger-border
--status-info-bg / --status-info-border

/* Entity type identity */
--entity-note-bg / --entity-note-text
--entity-concept-bg / --entity-concept-text
--entity-person-bg / --entity-person-text
--entity-project-bg / --entity-project-text

/* Visualization (consumed by Sigma/MindElixir via getComputedStyle — see ADR 015) */
--graph-node-default / --graph-node-selected / --graph-node-fixed / --graph-node-snapshot
--graph-edge-default / --graph-edge-snapshot

/* Control sizing & layering */
--control-height-sm / --control-height-md / --control-height-lg
--z-header / --z-sidebar / --z-overlay / --z-modal / --z-popover
--focus-ring
```

### 3. Per-theme completeness rule
Each theme block must either override every semantic token or rely on an explicitly documented base fallback. A CI-style grep check (not a new lint config) verifies no feature component references an undefined token.

### 4. Reduced-motion policy
Add one global block:

```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
scroll-behavior: auto !important;
}
}
```
Gate JS smooth-scroll with `matchMedia('(prefers-reduced-motion: reduce)')`.

### 5. UI primitive layer (token-governed)
Introduce small primitives (`Button`, `IconButton`, `Toolbar`/`ToolbarButton`, `EmptyState`, `ErrorState`, `Skeleton`) so repeated inline styles in Editor, ChatView, MindMapView, SearchPanel, GraphControls collapse onto tokens. Primitives are local CSS + TSX — **no new dependency**.

## Alternatives Considered
- **Adopt Tailwind / a CSS-in-JS lib**: rejected — large migration, conflicts with existing token CSS, adds a dependency, violates "reuse existing abstractions."
- **Leave undefined tokens with inline fallbacks**: rejected — hides theme drift and keeps colors hardcoded.
- **Only fix the two undefined tokens**: rejected — does not address theme coverage or hardcoded graph/status colors.

## Consequences
### Positive
- All four themes render consistently, including dark/high-contrast.
- Graph/status/badges adapt to themes via tokens.
- Accessibility improves (reduced motion, focus ring token).
- Inline-style drift collapses into reusable primitives.

### Negative
- One-time migration touching several CSS files and feature components.
- Temporary alias tokens must be tracked and removed after migration.

## Implementation Notes
- Do **not** modify `biome.json` / `eslint.config.js`.
- Keep `src/styles/*.css` and any split component under the 500 LOC rule.
- Verification: `grep -rn "var(--border-color)\|--surface-primary\|--surface-secondary" src/` returns nothing after migration; theme switch recolors graph; `prefers-reduced-motion` disables spin/skeleton/smooth-scroll.

## Files Affected (implementation)
- `src/styles/tokens.css`, `src/styles/utilities.css`, `src/styles/components.css`, `src/styles/features.css`
- `src/features/search/SearchPanel.tsx`, `src/features/ai/ChatView.tsx`, `src/features/ai/AIHarness.tsx`, `src/features/graph/GraphView.tsx`
- NEW `src/components/ui/*` primitives
89 changes: 89 additions & 0 deletions plans/ADRs/014-overlay-accessibility-primitive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# ADR 014: Shared Overlay/Modal Accessibility Primitive

## Status
PROPOSED (2026-06-18) — Implementation tracked in `plans/041-goap-ui-ux-modernization-and-feature-gaps-2026-06-18.md` (G-OVERLAY, G-A11Y).

## Context
The app has several overlay surfaces with **inconsistent accessibility and behavior** (verified):

| Surface | Focus trap | Escape | Scroll lock | Dialog role | Evidence |
|---------|-----------|--------|-------------|-------------|----------|
| `MobileDrawer` | ✅ `useFocusTrap` | ✅ `useEscapeKey` | ❌ | overlay is `role="button"` wrapping nested `role="dialog"` | `src/components/MobileDrawer.tsx:15-16,102-117` |
| `SnapshotBrowserModal` | ✅ | ✅ | ❌ | `role="dialog"` `aria-modal` | `src/features/graph/SnapshotBrowserModal.tsx:33-34,109-116` |
| Mobile search overlay | ❌ | ❌ | ❌ | none at wrapper | `src/app/App.tsx:299-308` |
| `CommandPalette` | ❌ | partial | ❌ | overlay `role="button"`, modal `role="presentation"` | `src/components/CommandPalette.tsx:126-139` |
| `SettingsWizard` | ❌ | ❌ | ❌ | none (inline styles) | `src/features/ai/SettingsWizard.tsx:30-41` |
| `EntityReviewDialog` | ❌ | ❌ | ✅ internal scroll | `role="dialog"` `aria-modal` | `src/features/ai/EntityReviewDialog.tsx:63-81` |

Problems:
- Keyboard users can tab **behind** the mobile search overlay, command palette, and settings wizard.
- Backdrops exposed as `role="button"` create a confusing extra focus stop and wrap interactive subtrees.
- No background scroll-lock, so the page scrolls under open overlays on mobile.
- Modals lack responsive width / `max-height` / safe-area padding, so tall content (settings, diff) can be unreachable on phones.
- Focus is not restored to the invoking element on close.

There is duplicated logic (`useFocusTrap`, `useEscapeKey`) and missing logic (scroll lock) spread unevenly across components.

## Decision
Introduce **one shared `<Overlay>` primitive** (plus a `useScrollLock` hook) that all modal/drawer/sheet surfaces use. It encapsulates the full accessibility contract.

### API
```tsx
interface OverlayProps {
isOpen: boolean;
onClose: () => void;
labelledBy?: string; // id of the title element
ariaLabel?: string; // fallback when no title element
variant?: 'center' | 'sheet-bottom' | 'sheet-left' | 'fullscreen';
initialFocusRef?: React.RefObject<HTMLElement>;
closeOnBackdrop?: boolean; // default true
children: React.ReactNode;
}
```

### Accessibility contract (every overlay)
1. Backdrop is a **non-focusable** `div` (not `role="button"`); click closes when `closeOnBackdrop`.
2. Content container is `role="dialog"` `aria-modal="true"` with `aria-labelledby` or `aria-label`.
3. **Focus trap** while open (reuse existing `useFocusTrap`).
4. **Escape** closes (reuse existing `useEscapeKey`).
5. **Background scroll lock** via new `useScrollLock` (set `overflow: hidden` + compensate scrollbar width, restore on close).
6. **Focus restoration** to the previously focused element on close.
7. Responsive sizing from variant: `width: min(100% - 2rem, <max>)`, `max-height: calc(100dvh - 2rem)`, `overflow-y: auto`, `padding-bottom: env(safe-area-inset-bottom)`.
8. Respects `prefers-reduced-motion` for enter/exit (see ADR 013).

### Migration targets
Refactor onto `<Overlay>`: mobile search overlay, `CommandPalette`, `SettingsWizard`, `EntityReviewDialog`, `MobileDrawer` (variant `sheet-left`), `SnapshotBrowserModal`, `SaveSnapshotModal`, graph mobile inspector bottom-sheet (ADR 015).

### Related semantic cleanups (G-A11Y)
- Command-palette listbox: keep DOM focus on the input; use `aria-activedescendant` only; options not individually tabbable (`CommandPalette.tsx:400-415`).
- Editor toolbar toggles get `aria-pressed` (`EditorToolbar.tsx:26-145`).
- Search virtualization: fix `ul > div > li` nesting to valid `listbox`/`option` markup (`SearchPanel.tsx:316-351`).
- Library row: prefer native `<button>` row or a single action button instead of `div role="button"` containing a nested button (`LibraryView.tsx:175-215`).

## Alternatives Considered
- **A headless dialog library (e.g. Radix/Headless UI)**: rejected — adds a dependency; existing `useFocusTrap`/`useEscapeKey` already cover most needs; local-first/minimal-deps preference.
- **Native `<dialog>` element**: considered; inconsistent styling/scroll-lock control and focus behavior across the variants (bottom sheet, fullscreen). Revisit later; not blocking.
- **Fix each overlay independently**: rejected — perpetuates drift and duplicated logic.

## Consequences
### Positive
- Uniform, correct keyboard and screen-reader behavior across all overlays.
- Background scroll-lock and safe-area handling fix mobile usability.
- Less duplicated logic; new overlays inherit correct behavior by default.

### Negative
- Refactor touches many components in one wave; needs E2E coverage to avoid regressions.

## Implementation Notes
- New files: `src/components/Overlay.tsx`, `src/hooks/useScrollLock.ts` (+ tests).
- Reuse existing `useFocusTrap`, `useEscapeKey` hooks.
- Keep each refactored component under 500 LOC; extract sub-views if needed.
- Verification: unit tests for focus-trap + Escape + scroll-lock; E2E that Tab cannot leave an open overlay and focus returns to the trigger on close.

## Files Affected (implementation)
- NEW `src/components/Overlay.tsx`, `src/hooks/useScrollLock.ts`
- `src/components/MobileDrawer.tsx`, `src/components/CommandPalette.tsx`
- `src/features/ai/SettingsWizard.tsx`, `src/features/ai/EntityReviewDialog.tsx`
- `src/features/graph/SnapshotBrowserModal.tsx`, `src/features/graph/SaveSnapshotModal.tsx`
- `src/app/App.tsx` (mobile search overlay)
- `src/features/editor/EditorToolbar.tsx`, `src/features/search/SearchPanel.tsx`, `src/features/library/LibraryView.tsx` (semantic cleanups)
103 changes: 103 additions & 0 deletions plans/ADRs/015-responsive-and-visualization-theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# ADR 015: Mobile-First Responsive Strategy & Visualization Theming Contract

## Status
PROPOSED (2026-06-18) — Implementation tracked in `plans/041-goap-ui-ux-modernization-and-feature-gaps-2026-06-18.md` (G-LAYOUT, G-TOUCH, G-VIZ-THEME).

## Context
AGENTS.md mandates **mobile-first** design with interactive targets **≥ 44×44px**. The shell is responsive, but feature views are desktop-first and several rules are violated (all verified):

### Layout / viewport
- App shell and chat use `100vh` (`src/styles/layout.css:10-16`; `src/styles/features.css:92-103`) — ignores mobile dynamic viewport; composers/controls hide behind browser chrome.
- Viewport meta lacks `viewport-fit=cover` (`index.html:5`); safe-area insets unused on header/drawer.
- Search sidebar hidden below a wide **1200px** breakpoint (`layout.css:68-77`); nav "Search" opens the command palette on desktop but the search overlay on mobile (inconsistent).
- **Bug**: mobile drawer "Search" calls `setCurrentView('search')` but there is no `search` branch in `main-content`, so it routes to a blank view (`App.tsx:275-280` vs `App.tsx:177-256`; `SidebarNav.tsx:42,60-68`).

### Visualizations
- Graph/mind-map canvases force inline `600px`/`minHeight:600px` (`GraphView.tsx:412-416`; `MindMapView.tsx:441-442`), overriding `.viz-container { height: 60vh }` and overflowing short phones.
- Graph inspector is a fixed `360px` side panel (`features.css:424-436`) that covers/overflows phones — no bottom-sheet breakpoint.
- Mobile graph controls lose snapshot/export/layout/snapshot-mode because the drawer mounts `GraphControls` with partial props (`App.tsx:284-294` vs `GraphView.tsx:395-409`).
- Mind-map toolbar is a dense desktop bar with selects + chips + sync + export + keyboard hints; wraps/overflows on phones and shows keyboard hints on touch (`MindMapView.tsx:323-439`).
- Graph node/edge colors are hardcoded hex (see ADR 013), so non-DOM renderers (Sigma, MindElixir) don't follow the active theme.

### Touch targets below 44px
- `.filter-chip` 32px (`components.css:439-449`) — used for search filters and mind-map actions.
- `.source-chip-remove` 20px (`features.css:209-220`).
- `.close-button` 36px (`features.css:481-488`).
- `.layout-toggle button` 36px (`features.css:390-393`).
- `.input-clear-button` auto (`components.css:543-552`).

### Library
- 4-column desktop grid with no mobile card layout (`features.css:593-610`); virtualizer 64px estimate breaks if rows wrap (`LibraryView.tsx:63-68`).

## Decision
Adopt a **mobile-first responsive baseline** and a **CSS-token theming contract for non-DOM visualizations**.

### 1. Breakpoint scale (named constants, not magic numbers)
```
--bp-sm: 640px (phone → large phone)
--bp-md: 768px (tablet portrait)
--bp-lg: 1024px (tablet landscape / small laptop)
--bp-xl: 1200px (desktop with both sidebars)
```
- Re-evaluate the search sidebar hide point; provide a **tablet side-sheet** for search between `md` and `xl` so search is not modal-only on tablets.
- Make nav "Search" open the **same** search experience across desktop/tablet/mobile (fix the inconsistency + the dead-view bug).

### 2. Dynamic viewport + safe area
- Replace `100vh` shells with `100dvh` (fallback `100svh`/`100vh`).
- Add `viewport-fit=cover` to the viewport meta.
- Apply `env(safe-area-inset-*)` padding to mobile header, drawer, full-screen overlays, and bottom sheets.

### 3. Touch-target enforcement
Use a coarse-pointer media query so desktop can keep compact controls while touch gets ≥44px:
```css
@media (pointer: coarse) {
.filter-chip, .close-button, .layout-toggle button,
.input-clear-button, .source-chip-remove { min-height: 44px; min-width: 44px; }
}
```

### 4. Responsive visualization canvas
- Remove inline heights; size via CSS:
```css
.viz-container { min-height: clamp(360px, calc(100dvh - var(--header-height) - 160px), 720px); }
```
- Graph inspector becomes a **bottom sheet** (`<Overlay variant="sheet-bottom">`, ADR 014) below `--bp-md`.
- Mobile graph/mind-map controls move to a **compact action bar + bottom sheet** with **full action parity** to desktop (pass complete `GraphControls` props on mobile). Hide keyboard-shortcut hints under `--bp-md`.

### 5. Visualization theming contract
Non-DOM renderers must read theme values from CSS custom properties at render time and on theme change:
```ts
const cs = getComputedStyle(containerRef.current);
const nodeColor = cs.getPropertyValue('--graph-node-default').trim();
// re-apply Sigma node/edge reducers; re-render MindElixir on theme-change event
```
Token names are defined in ADR 013 (`--graph-*`). A theme-change broadcast (existing theme switcher) triggers re-read so colors stay in sync.

### 6. Library mobile layout
Below `--bp-sm`, hide the grid header and render each entity as a **card** (name + type/date metadata + action). Use dynamic row measurement in the virtualizer instead of a fixed 64px estimate when cards can vary in height.

## Alternatives Considered
- **JS `window.innerWidth` branching in render** (current pattern at `App.tsx:212`): rejected — not reactive to resize/orientation, causes hydration/SSR-style mismatches; prefer CSS + the existing `useMediaQuery` hook.
- **Single layout for all sizes**: rejected — violates mobile-first guardrails; visualizations are unusable on phones.
- **Bake theme colors into JS constants per theme**: rejected — duplicates token truth; ADR 013 makes CSS the source, read via `getComputedStyle`.

## Consequences
### Positive
- Every view usable at 320 / 390 / 768 / 1200px; visualizations adapt and stay on-theme.
- Touch targets meet the 44px rule on phones/tablets without bloating desktop density.
- Search reachable consistently across device classes; dead-view bug fixed.

### Negative
- Touches many CSS files and the two visualization components; needs device-class E2E.
- `GraphView.tsx` / `MindMapView.tsx` may approach the 500 LOC cap — extract control/inspector sub-components before extending.

## Implementation Notes
- Reuse the existing `useMediaQuery` hook; avoid `window.innerWidth` in render.
- Mobile inspector/control sheets use the `<Overlay>` primitive (ADR 014).
- Verification: manual device-class sweep (320/390/768/1200px) — no overflow, all actions reachable; `prefers-reduced-motion` honored; theme switch recolors graph and mind map; `tap-target` audit ≥44px on coarse pointer.

## Files Affected (implementation)
- `index.html`, `src/styles/layout.css`, `src/styles/components.css`, `src/styles/features.css`, `src/styles/tokens.css`
- `src/app/App.tsx`, `src/components/SidebarNav.tsx`, `src/components/Header.tsx`, `src/components/MobileDrawer.tsx`
- `src/features/graph/GraphView.tsx`, `src/features/graph/GraphControls.tsx`, `src/features/graph/GraphInspector.tsx`
- `src/features/mindmap/MindMapView.tsx`, `src/features/library/LibraryView.tsx`
Loading
Loading