@aistrike-dev/ui
v5.0.1
Published
Octopus UI - AiStrike design system (Atomic Design over MUI).
Keywords
Readme
Octopus UI
@aistrike-dev/ui is the AiStrike design system: a dark-first component library built as a theme layer over MUI and organized with Atomic Design. Design tokens are sourced from the AiStrike Complete Design System 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.
Table of contents
- Features
- Install
- Quick start
- Develop
- Scripts
- Architecture
- Component catalog
- Code editor (Monaco)
- Theming
- Migrating to 2.0
- Agent guidance kit
- LLM / generative-UI registry
- Contributing
- Releases
- Related docs
Features
- Dark-first theming —
darkThemeis the default;lightThemeis 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 —
ComponentMetaper component for LLM and generative-UI tooling. - Agent guidance kit —
npx aistrike-ui init-agentinstalls 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:
npm install @aistrike-dev/uiPeer 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:
import 'rsuite/dist/rsuite-no-reset.min.css';Quick start
Wrap your app with the Octopus theme and import components from the package entry:
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
git clone git@github-aistrike:aistrike-dev/octopus-ui.git
cd octopus-ui
npm ci
npm run storybook # http://localhost:6006npm run dev starts a minimal Vite landing page; the component library and documentation live in Storybook.
Quality checks
Before opening a pull request, run:
npm run typecheck
npm run lint
npm run build
npm run build-storybookIf 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, dayjsComponent 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 exportClassification
| 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.
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:
import { CodeEditor } from '@aistrike-dev/ui/monaco';Install the peer — it is optional, but pinned:
npm install monaco-editor@^0.56.0The 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
<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
<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:
// @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']] },
});
}<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:
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:
// 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 ofprimary/secondary/ghost/destructivevariants (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:
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:
npm i @aistrike-dev/ui
npx --package=@aistrike-dev/ui aistrike-ui init-agentThat 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:
"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:
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
- Branch from
mainusing a descriptive name (feat/...,fix/...,chore/...,docs/...). - Follow the 4-file component pattern and Atomic Design structure.
- Write commits in Conventional Commits format. Note that every commit type currently publishes a patch and
feat:does not yet bump the minor — see RELEASING.md. - Open a pull request into
mainwith a clear summary of user-facing impact. - Keep React, MUI, Emotion, and material-react-table as peerDependencies; runtime dependencies are
dayjsandrsuite.
One logical change per PR. Never push directly to main.
Releases
Releases are agent-prepared, PR-gated, and CI-published:
- Changes land on a feature branch with Conventional Commits and open a PR into
main. - A reviewer approves and merges the PR (branch protection is the release gate).
- GitHub Actions runs semantic-release: publishes
@aistrike-dev/uito 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 for the full runbook, required secrets, and DevOps prerequisites.
Related docs
| Document | Audience | | --- | --- | | RELEASING.md | CI/CD pipelines, npm trusted publishing, required secrets | | AGENTS.md | Cursor Cloud agent guide and project map | | docs/foundation-ui-design-system-brief.md | Planning brief and migration context |
License
Proprietary — UNLICENSED. Internal use within AiStrike only.
