# Octopus UI

[![Storybook](https://img.shields.io/badge/docs-Storybook-FF4785?logo=storybook&logoColor=white)](https://octopus-ui.aistrike.dev/)

`@aistrike-dev/ui` is the **AiStrike design system**: a dark-first component library built as a **theme layer over [MUI](https://mui.com/)** and organized with **Atomic Design**. Design tokens are sourced from the [AiStrike Complete Design System](https://www.figma.com/design/LJdrBxmaROpXg0hLW3GKe6/AiStrike-Complete-Design-System?node-id=23-476) in Figma.

Built with **React 19**, **TypeScript**, **Vite**, **Storybook 10**, and **Emotion**. Published to the **public npm registry**; interactive docs are hosted on **[GitHub Pages](https://octopus-ui.aistrike.dev/)**.

## Table of contents

- [Features](#features)
- [Install](#install)
- [Quick start](#quick-start)
- [Develop](#develop)
- [Scripts](#scripts)
- [Architecture](#architecture)
- [Component catalog](#component-catalog)
- [Code editor (Monaco)](#code-editor-monaco)
- [Theming](#theming)
- [Migrating to 2.0](#migrating-to-20)
- [Agent guidance kit](#agent-guidance-kit)
- [LLM / generative-UI registry](#llm--generative-ui-registry)
- [Contributing](#contributing)
- [Releases](#releases)
- [Related docs](#related-docs)

## Features

- **Dark-first theming** — `darkTheme` is the default; `lightTheme` is available for completeness.
- **Design tokens** — colors, typography, spacing, radius, shadows, z-index, and attack-path semantics as a single source of truth.
- **Atomic Design** — atoms → molecules → organisms → templates → pages.
- **MUI-native** — themed re-exports, overrides, and thin wrappers where product behavior is needed.
- **Storybook docs** — autodocs, controls, accessibility checks, and dark/light theme switching.
- **Machine-readable registry** — `ComponentMeta` per component for LLM and generative-UI tooling.
- **Agent guidance kit** — `npx aistrike-ui init-agent` installs design-system rules into your repo, so AI agents pick the right component instead of hand-rolling one.

## Install

`@aistrike-dev/ui` is published to the public npm registry - no registry login or scoped `.npmrc` required:

```bash
npm install @aistrike-dev/ui
```

### Peer dependencies

Install these in your consuming app:

| Package | Version |
| --- | --- |
| `react` / `react-dom` | ^19.2.6 |
| `@mui/material` | ^9.1.1 |
| `@mui/icons-material` | ^9.1.1 |
| `@mui/x-date-pickers` | ^9.6.0 |
| `@emotion/react` / `@emotion/styled` | ^11.14.x |
| `material-react-table` | ^3.2.1 |
| `date-fns` | ^4.4.0 |

Runtime dependencies installed with the package: `dayjs` and `rsuite` (used by `DateRangePicker`). When using `DateRangePicker`, also import rsuite's stylesheet once at the app root:

```ts
import 'rsuite/dist/rsuite-no-reset.min.css';
```

## Quick start

Wrap your app with the Octopus theme and import components from the package entry:

```tsx
import { ThemeProvider, CssBaseline, darkTheme, Button } from '@aistrike-dev/ui';

export function App() {
  return (
    <ThemeProvider theme={darkTheme}>
      <CssBaseline />
      <Button variant="primary">Investigate</Button>
    </ThemeProvider>
  );
}
```

Octopus UI uses system-native sans-serif and monospace font stacks, so consuming applications do not need to load external fonts.

## Develop

### Prerequisites

- **Node.js** 20+ (LTS recommended)
- **npm** 10+

### Setup

```bash
git clone git@github-aistrike:aistrike-dev/octopus-ui.git
cd octopus-ui
npm ci
npm run storybook   # http://localhost:6006
```

`npm run dev` starts a minimal Vite landing page; the component library and documentation live in Storybook.

### Quality checks

Before opening a pull request, run:

```bash
npm run typecheck
npm run lint
npm run build
npm run build-storybook
```

If any `*.meta.ts` files changed, also run `npm run build:registry`.

## Scripts

| Script | Description |
| --- | --- |
| `npm run storybook` | Run Storybook locally |
| `npm run build-storybook` | Build static Storybook to `storybook-static/` |
| `npm run build` | Build the publishable library (`dist/` ESM + CJS + types + agent kit) |
| `npm run build:registry` | Regenerate the LLM/generative-UI registry JSON |
| `npm run build:agent-kit` | Regenerate `dist/agent/` (also runs as part of `build`) |
| `npm run lint` | ESLint |
| `npm run test` | Vitest (unit guards + Storybook interaction tests) |
| `npm run release:dry` | Preview the next version and release notes (read-only) |
| `npm run release` | semantic-release (**CI only**; blocked locally for agents) |

## Architecture

```
src/
  tokens/      colors, typography, spacing, radius, shadows, zIndex, attackPath
  theme/       palette, typography, component overrides, variants → darkTheme / lightTheme
  atoms/       Button, Checkbox, TextField, Chip, Icon, …
  molecules/   Search, FormField, Tabs, Alert, DatePicker, …
  organisms/   Card, Dialog, Drawer, Table, LeftNavigation, AttackPathLegend, …
  templates/   DashboardLayout, TablePageLayout, SettingsLayout, …
  pages/       DashboardExample, TableExample, …
  docs/        MDX documentation and token stories
  registry/    ComponentMeta types and generated JSON
  index.ts     public library entry (published surface)
.storybook/    Storybook config, ThemeProvider, theme switcher, a11y, dayjs
```

### Component pattern

Every component folder follows a consistent 4-file pattern:

```
Name/
  Name.tsx          implementation (themed re-export, override, wrapper, or custom)
  Name.stories.tsx  autodocs, controls, and real states
  Name.meta.ts      machine-readable ComponentMeta
  index.ts          barrel export
```

### Classification

| Type | Description |
| --- | --- |
| **themed-mui** | Pure MUI component, styled only via the theme |
| **mui-overrides** | MUI with theme-level `styleOverrides` or variants |
| **mui-wrapper** | Thin wrapper adding product-specific behavior |
| **custom** | Fully custom, domain-specific implementation |
| **third-party-wrapper** | Wraps a heavy third-party dependency declared as a peer |

## Component catalog

| Layer | Count | Examples |
| --- | ---: | --- |
| Atoms | 19 | Button, Chip, TextField, Select, Icon, Tooltip |
| Molecules | 15 | Search, FormField, Tabs, Alert, DatePicker, Pagination |
| Organisms | 17 | Card, Dialog, Drawer, Table, FilterPanel, LeftNavigation |
| Templates | 6 | DashboardLayout, TablePageLayout, InvestigationLayout |
| Pages | 8 | DashboardExample, ThreatsExample, InventoryExample |

Browse the full catalog with live examples at [octopus-ui.aistrike.dev](https://octopus-ui.aistrike.dev/).

## Code editor (Monaco)

The Monaco editor ships at a **separate, ESM-only entry** so consumers that
never mount an editor never pay for it, and so the root `.` entry stays dual
ESM/CJS:

```tsx
import { CodeEditor } from '@aistrike-dev/ui/monaco';
```

Install the peer — it is optional, but pinned:

```bash
npm install monaco-editor@^0.56.0
```

The version is pinned to `^0.56.0` because the design system ships a pre-built
worker, and Monaco's main-thread/worker RPC protocol is internal and changes
between minors. A mismatch does not error cleanly; it silently degrades
(suggestions, diff computation and link detection stop working).

Because `@aistrike-dev/ui/monaco` needs `import.meta.url` to resolve its
worker, it is published **ESM-only** — there is no `require()` entry. If your
project is still CommonJS, import it from an ESM context (a dynamic
`import()`, an ESM-only bundler pipeline, or an `.mjs`/`"type": "module"`
boundary).

### Choosing what loads

```tsx
<CodeEditor
  value={sql}
  onChange={setSql}
  language="sql"
  features={['suggest', 'hover']}   // replaces the default set
  ariaLabel="SQL query"
/>
```

Built-in languages: `sql`, `python`, `javascript`, `shell`, `markdown`, `yaml`.
`json`, `css`, `html` and `typescript` are **not** available — they are
language *services* that need their own dedicated workers, which this design
deliberately does not ship.

**`languages` and `features` are mount-only.** Both are read once, when the
editor is created, and are not re-applied if the prop changes on a later
render — there is no live "add a language" or "add a feature" to an editor
that already exists. If your instance needs a different `languages`/`features`
set, remount the component (e.g. by changing its React `key`). `value`,
`readOnly`, `ariaLabel` and `options` are not mount-only; they stay in sync on
every render.

### Loading state and failures

```tsx
<CodeEditor
  value={sql}
  onChange={setSql}
  language="sql"
  ariaLabel="SQL query"
  loadingFallback={<Skeleton variant="rectangular" height={260} />}
  onLoadError={(error) => reportToErrorTracker(error)}
/>
```

`loadingFallback` renders in place of the editor until Monaco has finished
loading (`loadingFallback = null` by default, so nothing is shown). `onLoadError`
fires if that load — the dynamic `monaco-editor` import, a requested language's
loader, or an external language's `register()` — rejects.

Wire `onLoadError`. If `loadMonaco` rejects and it is not provided, the
component has no way to surface the failure: `ready` never flips to `true`, so
with the default `loadingFallback = null` the consumer is left looking at a
permanently blank box with nothing in the console pointing at why. Passing
`onLoadError` (to a toast, an error tracker, a fallback UI) is how you find out
a load failed at all, let alone why.

### Adding a language from another package

The design system never needs to know your package exists. Export `register`:

```ts
// @acme/my-language
import type { MonacoApi } from '@aistrike-dev/ui/monaco';

export function register(monaco: MonacoApi) {
  monaco.languages.register({ id: 'mylang' });
  monaco.languages.setMonarchTokensProvider('mylang', {
    tokenizer: { root: [[/\b(select|from)\b/, 'keyword']] },
  });
}
```

```tsx
<CodeEditor
  language="mylang"
  languages={[{ id: 'mylang', load: () => import('@acme/my-language') }]}
  value={code}
  ariaLabel="My language"
/>
```

Emit token **names**, never colours. The design system maps names to design
tokens for both light and dark. Names follow Monaco's convention (`keyword`,
`string`, `number`, `comment`, `operator`, `type`, `identifier`, `delimiter`,
`variable`, `invalid`); dotted sub-names such as `operator.pipe` inherit their
parent's colour.

### Accessibility

`ariaLabel` is a **required** prop — Monaco renders a bare textarea to
assistive technology, so an unlabelled editor is unusable with a screen
reader.

Tab inserts a tab character rather than moving focus, which would otherwise be
a keyboard trap (WCAG 2.1.2). The escape is Monaco's tab-focus mode:
`toggleTabFocusMode` is part of `DEFAULT_FEATURES`, so **Ctrl+M** (Windows and
Linux) or **Ctrl+Shift+M** (macOS) always works out of the box — it toggles Tab
between inserting a tab character and moving focus to the next element.

That keystroke is undiscoverable, so `CodeEditor` can advertise it in the corner
while the editor has focus. Pass `showTabFocusHint` to switch that on; it is
**off by default**, because the hint is chrome that every user sees in order to
serve the keyboard-only ones, and which surfaces are worth that trade is a
product decision. The prop controls the hint only — Tab behaviour and the escape
keystroke are identical either way.

Note the macOS keystroke is Ctrl+Shift+M, **not** Cmd+M: Monaco binds
`WinCtrl|Shift|KeyM` there, and Cmd+M is the OS minimise-window shortcut.

### Theme

The editor follows your MUI theme's `palette.mode`. Monaco's theme is
**page-global**, so all editors on a page share it — there is deliberately no
per-instance theme prop.

### Content Security Policy

```
worker-src 'self' blob:;
style-src  'self' 'unsafe-inline';   /* Monaco injects <style> at runtime */
```

### Server rendering

The component is client-only (`'use client'`). Monaco touches `window` and
`document` and cannot render on the server.

### Pre-warming the worker

`CodeEditor` registers Monaco's web worker automatically on mount, so this is
optional. If your app knows an editor is coming soon (e.g. it's one route
away), call `configureMonacoWorker()` earlier — at app bootstrap, or on a
route transition — to avoid a cold-start delay on the first mount:

```ts
import { configureMonacoWorker } from '@aistrike-dev/ui/monaco';

configureMonacoWorker();
```

It is idempotent — safe to call more than once, or in addition to the
automatic registration `CodeEditor` already performs.

### Troubleshooting

The worker resolves from a normally installed `monaco-editor` package with no
extra bundler configuration in both dev and production Vite builds. If your
setup still fails to resolve the worker (an unusual bundler config, a
monorepo with hoisting quirks), excluding it from dependency pre-bundling is a
fallback worth trying:

```ts
// vite.config.ts
export default {
  optimizeDeps: { exclude: ['monaco-editor'] },
};
```

## Theming

`darkTheme` is the default export and the primary, Figma-accurate theme. Custom additions include:

- `<Button variant="primary" />` — emphasis ladder of `primary` / `secondary` / `ghost` / `destructive` variants (fully typed via theme augmentation)
- `<Chip variant="severity" color="critical" />` — security severity scale (fully typed via theme augmentation)

Import tokens directly when you need values outside of MUI's `theme` object:

```tsx
import { tokens, darkTheme } from '@aistrike-dev/ui';
```

## Migrating to 2.0

`2.0.0` renames the `Button` variants to an emphasis ladder and changes the default variant. Update any `variant` props:

| Before (`1.x`) | After (`2.0`) |
| --- | --- |
| `variant="contained"` | `variant="primary"` |
| `variant="outlined"` | `variant="secondary"` |
| `variant="text"` | `variant="ghost"` |
| `variant="attack"` | `variant="destructive"` |

The default variant is now `secondary` (previously `contained`), and `secondary` renders on the `#182724` dark-green surface. Reserve `primary` for the single most important action per view, and always pair `destructive` with a confirmation step. See **Guidelines → Agent Guide** in Storybook and the `Button` stories for the full guidance.

## Agent guidance kit

If AI agents write UI in your repository, install the agent kit once. It teaches them which component
to reach for, which variant, and what never to hand-roll:

```bash
npm i @aistrike-dev/ui
npx --package=@aistrike-dev/ui aistrike-ui init-agent
```

That writes three things, all safe to commit:

| Installed | What it is |
| --- | --- |
| `AGENTS.md` | A managed stanza between `<!-- octopus-ui:begin -->` markers: the hard rules (imports, no hand-rolling, tokens, `llmSafe`, ask when unsure) plus an instruction to invoke the `octopus-ui` skill |
| `.claude/skills/octopus-ui` | A symlink to `node_modules/@aistrike-dev/ui/dist/agent` — where Claude Code looks for project skills |
| `.cursor/skills/octopus-ui` | The same symlink, where Cursor looks |

The skill directories are **links, not copies**. Nothing is cached in your repo, so `npm update`
upgrades the guidance with nothing to re-run — that is the whole design. The links are relative and
committed, so a teammate who clones and runs `npm install` gets working guidance immediately.

**In a monorepo,** run it in the workspace package that uses the design system: the link then points at
wherever the dependency actually is, which for a hoisted install is the monorepo root — a link that
escapes the package directory (`../../../../node_modules/…`). It resolves for anyone working in the
monorepo, but tooling that takes that one package on its own — `npm pack`, a Docker `COPY` of the
package directory — may refuse to follow a symlink pointing outside it, or dereference it into a copy;
if that matters, exclude the skills directories from those builds.

`AGENTS.md` names the skill **by name**, never by path: Claude Code and Cursor look in different
directories, and Codex reads `AGENTS.md` with no skill mechanism at all. The stanza therefore carries
the hard rules itself and routes to the skill for the detail. It carries no version number, so a new
release of this package does not make your `AGENTS.md` stale.

`aistrike-ui init-agent --check` verifies both links resolve and the stanza is current, and exits
non-zero if not — a one-line CI guard. It writes nothing.

There is deliberately no `.cursor/rules/*.mdc`. Cursor treats a root `AGENTS.md` as a rule and injects
it at the start of context on **every** agent turn — the same reach `alwaysApply: true` has — and the
Cursor CLI and Cloud Agents read it too. A rule file would therefore be a second always-on copy of the
same stanza, one that Claude Code, Codex and everything else reading `AGENTS.md` would never see.

**This package ships no install script**, and cannot usefully do so: npm ≥ 11.6 blocks dependency
lifecycle scripts by default, and approvals written by `npm install-scripts approve` are pinned to a
version, so they stop applying on the next upgrade. Your *own* root `postinstall` is never blocked, so
if you want the links repaired automatically after every install, add this to your `package.json`:

```json
"scripts": { "postinstall": "aistrike-ui init-agent --quiet" }
```

That is optional. A committed symlink re-resolves on its own as soon as `npm install` recreates
`node_modules`, so most repos need nothing.

**If your repo gitignores `.claude/` or `.cursor/`,** un-ignore at least the skills directory — an
ignored link reaches nobody else. `init-agent` warns when it spots this.

Where symlinks are unavailable (Windows without Developer Mode), `init-agent` writes a small pointer
`SKILL.md` instead and says so. Yarn Plug'n'Play is not supported: with no `node_modules`, there is
nothing to link to.

The references the kit points at are all generated from the design system itself:

| Reference | Generated from |
| --- | --- |
| `core-rules.md` | `src/agent/guidelines/core-rules.md` — the global contract |
| `components.md` | every component's `*.meta.ts` (`rules`, `antiPatterns`, `accessibility`, `llmSafe`) |
| `choosing.md` | `src/agent/guidelines/choosing/*.md` — deciding between similar-looking components |
| `tokens.md` | `src/tokens` — surface and severity tokens, so no hex can drift |

Everything also renders for humans in Storybook under **Guidelines → Agent Guide** and on each
component's docs page.

## LLM / generative-UI registry

`npm run build:registry` aggregates each component's `ComponentMeta` into:

| File | Purpose |
| --- | --- |
| `src/registry/component-registry.json` | Full entries (props, states, tokens, a11y, anti-patterns, `llmSafe`) |
| `src/registry/mui-mapping.json` | Octopus UI → MUI mapping |
| `src/registry/a2ui-mapping.json` | Octopus UI → A2UI / generative-UI mapping |
| `src/registry/llm-components.json` | Compact LLM index (`rules`, `antiPatterns`, `accessibility`; reference example pages excluded) |

The same JSON ships in the package for runtime use:

```ts
import registry from '@aistrike-dev/ui/registry';
```

A unit test fails if the committed JSON is stale, so run `npm run build:registry` and commit the
result whenever you touch a `*.meta.ts`.

## Contributing

1. Branch from `main` using a descriptive name (`feat/...`, `fix/...`, `chore/...`, `docs/...`).
2. Follow the 4-file component pattern and Atomic Design structure.
3. Write commits in [Conventional Commits](https://www.conventionalcommits.org/) format. Note that every commit type currently publishes a patch and `feat:` does not yet bump the minor — see [RELEASING.md](RELEASING.md#versioning-conventional-commits).
4. Open a pull request into `main` with a clear summary of user-facing impact.
5. Keep React, MUI, Emotion, and material-react-table as **peerDependencies**; runtime dependencies are `dayjs` and `rsuite`.

One logical change per PR. Never push directly to `main`.

## Releases

Releases are **agent-prepared, PR-gated, and CI-published**:

1. Changes land on a feature branch with Conventional Commits and open a PR into `main`.
2. A reviewer approves and merges the PR (branch protection is the release gate).
3. GitHub Actions runs semantic-release: publishes `@aistrike-dev/ui` to the public npm registry, tags the commit, creates a GitHub Release with the notes, and notifies Slack. (npm provenance is disabled while the repo is internal.)

See **[RELEASING.md](RELEASING.md)** for the full runbook, required secrets, and DevOps prerequisites.

## Related docs

| Document | Audience |
| --- | --- |
| [RELEASING.md](RELEASING.md) | CI/CD pipelines, npm trusted publishing, required secrets |
| [AGENTS.md](AGENTS.md) | Cursor Cloud agent guide and project map |
| [docs/foundation-ui-design-system-brief.md](docs/foundation-ui-design-system-brief.md) | Planning brief and migration context |

## License

Proprietary — **UNLICENSED**. Internal use within AiStrike only.
