@particle-academy/fancy-code
v0.4.2
Published
Lightweight embedded code editor with syntax highlighting and extensible language/theme registries
Downloads
998
Readme
@particle-academy/fancy-code
Lightweight embedded code editor with syntax highlighting, custom toolbar buttons, and extensible language/theme registries. Part of the @particle-academy component ecosystem.
Installation
# npm
npm install @particle-academy/fancy-code
# pnpm
pnpm add @particle-academy/fancy-code
# yarn
yarn add @particle-academy/fancy-codePeer dependencies: react >= 18, react-dom >= 18, @particle-academy/react-fancy >= 1.5
Usage
Add the @source directive to your main CSS file so Tailwind v4 scans the component library:
@import "tailwindcss";
@import "@particle-academy/fancy-code/styles.css";
@source "../node_modules/@particle-academy/fancy-code/dist/**/*.js";Then import and use:
import { CodeEditor } from "@particle-academy/fancy-code";
import "@particle-academy/fancy-code/styles.css";
function App() {
const [code, setCode] = useState('console.log("Hello");');
return (
<CodeEditor
value={code}
onChange={setCode}
language="javascript"
className="rounded-xl border border-zinc-200 dark:border-zinc-700"
>
<CodeEditor.Toolbar />
<CodeEditor.Panel />
<CodeEditor.StatusBar />
</CodeEditor>
);
}The component has no default border or rounding — add your own via className for standalone use, or embed directly into IDE layouts without visual conflicts.
Commands
pnpm --filter @particle-academy/fancy-code build # Build with tsup (ESM + CJS + DTS)
pnpm --filter @particle-academy/fancy-code dev # Watch mode
pnpm --filter @particle-academy/fancy-code lint # Type-check (tsc --noEmit)
pnpm --filter @particle-academy/fancy-code clean # Remove dist/Component API
Compound Components
| Component | Description |
|-----------|-------------|
| CodeEditor | Root wrapper — context provider, state management |
| CodeEditor.Toolbar | Action bar with default buttons or custom children |
| CodeEditor.Toolbar.Separator | Vertical divider between toolbar groups |
| CodeEditor.Panel | Code editing surface |
| CodeEditor.StatusBar | Cursor position, language, tab size display |
CodeEditor Props
interface CodeEditorProps {
children: ReactNode;
className?: string;
value?: string; // Controlled value
defaultValue?: string; // Uncontrolled initial value
onChange?: (value: string) => void;
language?: string; // Language name or alias (default: "javascript")
onLanguageChange?: (lang: string) => void;
theme?: string; // "light" | "dark" | "auto" | custom name (default: "auto")
readOnly?: boolean; // Default: false
lineNumbers?: boolean; // Default: true
wordWrap?: boolean; // Default: false
tabSize?: number; // Default: 2
placeholder?: string;
minHeight?: number; // Minimum height in px
maxHeight?: number; // Max height before scrolling
}useCodeEditor Hook
Access the editor context from custom toolbar buttons:
import { useCodeEditor } from "@particle-academy/fancy-code";
import { Action } from "@particle-academy/react-fancy";
function RunButton() {
const { getValue } = useCodeEditor();
return <Action size="xs" onClick={() => run(getValue())}>Run</Action>;
}
<CodeEditor value={code} onChange={setCode} language="javascript">
<CodeEditor.Toolbar>
<RunButton />
<CodeEditor.Toolbar.Separator />
</CodeEditor.Toolbar>
<CodeEditor.Panel />
</CodeEditor>Context value:
| Method / Property | Description |
|-------------------|-------------|
| getValue() | Get current document text |
| getSelection() | Get currently selected text |
| setValue(text) | Replace entire document |
| replaceSelection(text) | Replace current selection |
| focus() | Focus the editor |
| language | Current language name |
| setLanguage(name) | Switch active language |
| theme | Current theme name |
| readOnly | Whether the editor is read-only |
| lineNumbers | Whether line numbers are shown |
| wordWrap | Whether word wrap is enabled |
| tabSize | Current tab size |
| toggleWordWrap() | Toggle word wrap on/off |
| toggleLineNumbers() | Toggle line numbers on/off |
| copyToClipboard() | Copy entire document to clipboard |
| cursorPosition | { line, col } — current cursor position |
| selectionLength | Length of current selection (0 if none) |
Examples
Basic Editor
<CodeEditor value={code} onChange={setCode} language="typescript">
<CodeEditor.Toolbar />
<CodeEditor.Panel />
<CodeEditor.StatusBar />
</CodeEditor>Read-Only Code Display
<CodeEditor value={snippet} language="php" readOnly>
<CodeEditor.Panel />
</CodeEditor>Custom Toolbar
<CodeEditor value={code} onChange={setCode} language="javascript">
<CodeEditor.Toolbar>
<RunButton />
<CodeEditor.Toolbar.Separator />
<FormatButton />
<CopyButton />
</CodeEditor.Toolbar>
<CodeEditor.Panel />
<CodeEditor.StatusBar />
</CodeEditor>Height Constraints
<CodeEditor value={longCode} language="javascript" maxHeight={400} minHeight={200}>
<CodeEditor.Toolbar />
<CodeEditor.Panel />
<CodeEditor.StatusBar />
</CodeEditor>Minimal (No Toolbar, No StatusBar)
<CodeEditor value={snippet} language="javascript" readOnly lineNumbers={false}>
<CodeEditor.Panel />
</CodeEditor>Built-in Languages
| Language | Aliases |
|----------|---------|
| JavaScript | js, javascript, jsx |
| TypeScript | ts, typescript, tsx |
| HTML | html, htm |
| PHP | php |
| Python | py, python |
| Go | go, golang |
Custom Language Registration
Add languages beyond the built-ins using registerLanguage:
import { registerLanguage } from "@particle-academy/fancy-code";
import type { Tokenizer } from "@particle-academy/fancy-code";
const tokenizeRuby: Tokenizer = (source) => {
const tokens = [];
// Your regex-based tokenizer logic here
// See src/engine/tokenizers/javascript.ts for a full example
return tokens;
};
registerLanguage({
name: "Ruby",
aliases: ["rb", "ruby"],
tokenize: tokenizeRuby,
});Then use it:
<CodeEditor language="python" ... />Built-in Color Schemes
| Theme | Description |
|-------|-------------|
| light | White background, blue/purple/green token colors |
| dark | Zinc-900 background, pastel token colors |
| auto | Automatically selects light/dark based on prefers-color-scheme (default) |
Custom Theme Registration
Create custom syntax highlighting color schemes using registerTheme:
import { registerTheme } from "@particle-academy/fancy-code";
registerTheme({
name: "monokai",
variant: "dark",
editorTheme: /* editor chrome styles (background, gutter, cursor, selection) */,
highlightStyle: /* token color definitions (keywords, strings, comments, etc.) */,
});Then use it:
<CodeEditor theme="monokai" ... />Theme definition:
interface ThemeDefinition {
name: string; // Unique theme name
variant: "light" | "dark"; // Light or dark base
editorTheme: Extension; // Editor chrome styles (gutter, cursor, selection)
highlightStyle: HighlightStyle; // Syntax token color definitions
}Built-in Features
- Syntax highlighting with language-aware tokenization
- Line numbers with active line gutter highlight
- Active line highlighting
- Bracket matching and auto-close
- Code folding with fold gutter
- Search and replace (Ctrl+F / Cmd+F)
- Autocompletion
- History (undo/redo)
- Indent on input
- Selection highlighting
- Tab key indentation
All features are reconfigurable at runtime — switching languages, themes, line numbers, word wrap, tab size, and read-only mode happens without recreating the editor.
Customization
All components render data-fancy-code-* attributes on their root elements for external CSS targeting:
| Attribute | Element |
|-----------|---------|
| data-fancy-code-editor | Root wrapper |
| data-fancy-code-toolbar | Toolbar bar |
| data-fancy-code-toolbar-separator | Toolbar separator |
| data-fancy-code-panel | Editing surface |
| data-fancy-code-statusbar | Status bar |
Architecture
Directory Layout
src/
├── components/
│ └── CodeEditor/
│ ├── CodeEditor.tsx # Root compound component + context
│ ├── CodeEditor.types.ts # All prop/context types
│ ├── CodeEditor.context.ts # React context + useCodeEditor hook
│ ├── CodeEditorPanel.tsx # Editing surface
│ ├── CodeEditorToolbar.tsx # Default toolbar + custom children
│ ├── CodeEditorToolbarSeparator.tsx
│ ├── CodeEditorStatusBar.tsx # Cursor/language/tab display
│ └── index.ts
├── hooks/
│ ├── use-editor-engine.ts # Core editor lifecycle
│ └── use-dark-mode.ts # Reactive prefers-color-scheme
├── languages/
│ ├── registry.ts # Global language registry
│ ├── builtin.ts # JS, TS, HTML, PHP registrations
│ ├── types.ts # LanguageDefinition type
│ └── index.ts
├── themes/
│ ├── registry.ts # Global theme registry
│ ├── light.ts # Built-in light color scheme
│ ├── dark.ts # Built-in dark color scheme
│ ├── types.ts # ThemeDefinition type
│ └── index.ts
├── styles.css # Base structural styles
└── index.ts # Public APIPublic Exports
// Components
export { CodeEditor, useCodeEditor };
export type { CodeEditorProps, CodeEditorContextValue, CodeEditorToolbarProps, CodeEditorPanelProps, CodeEditorStatusBarProps };
// Language registration
export { registerLanguage, getLanguage, getRegisteredLanguages };
export type { LanguageDefinition };
// Theme registration
export { registerTheme, getTheme, getRegisteredThemes };
export type { ThemeDefinition };Related: TreeNav from react-fancy
For IDE-style layouts, pair CodeEditor with the TreeNav component from @particle-academy/react-fancy. TreeNav provides hierarchical file/folder navigation with expand/collapse, selection, and extension-based file icons.
import { TreeNav } from "@particle-academy/react-fancy";
import { CodeEditor } from "@particle-academy/fancy-code";
<div className="flex" style={{ height: 600 }}>
<TreeNav
nodes={fileTree}
selectedId={activeFile}
onSelect={(id, node) => openFile(id, node)}
/>
<CodeEditor value={code} onChange={setCode} language={lang}>
<CodeEditor.Toolbar />
<CodeEditor.Panel />
<CodeEditor.StatusBar />
</CodeEditor>
</div>See the full IDE demo at /react-demos/ide in the monorepo.
Demo Pages
The demo page lives in the monorepo at resources/js/react-demos/pages/CodeEditorDemo.tsx and is accessible at /react-demos/code-editor. It demonstrates:
- Basic editor with default toolbar
- Read-only code display
- Custom toolbar buttons via
useCodeEditor() - Language switching (JS, TS, HTML, PHP)
- Light vs dark theme showcase
- Word wrap and configuration
- Minimal mode (no toolbar, no status bar)
Agent Guidelines
Guidelines for AI agents (Claude Code, Copilot, etc.) working on this package.
Component Pattern
- Uses the same compound component pattern as
@particle-academy/react-fancy's Editor:Object.assign(Root, { Toolbar, Panel, StatusBar }). - Context is provided via
CodeEditorContextand consumed withuseCodeEditor(). - All root elements have
data-fancy-code-*attributes for external CSS targeting.
Editor Engine
- The
useCodemirrorhook manages the editor lifecycle. - Uses compartments for hot-swapping language, theme, line numbers, word wrap, tab size, read-only, placeholder, and height constraints.
- External value changes are synced via
isExternalUpdateref to prevent onChange loops. - SSR-safe: the editor is only created inside
useEffect.
Extension System
- Languages and themes use the same global registry pattern as react-fancy's
registerExtension. - Built-in languages/themes are registered via side-effect imports in
languages/builtin.tsandthemes/light.ts/themes/dark.ts. - The
sideEffectsfield inpackage.jsonlists these files to prevent tree-shaking.
Build
- tsup handles the build — ESM, CJS, and
.d.tsgeneration. react,react-dom, and@particle-academy/react-fancyare external dependencies.- All editor engine packages are bundled (not external) so consumers don't need to install them.
- After any change, verify with
npm run buildfrom the monorepo root.
