Advanced Customization
Advanced techniques for customizing Lokus to match your workflow, from custom themes and editor extensions to plugin development and UI modifications.
Custom Themes
Creating a Custom Theme
Define custom colors and syntax highlighting:
{
"theme": "custom",
"customTheme": {
"name": "Ocean Theme",
"colors": {
"background": "#0c1e2a",
"foreground": "#e0e0e0",
"primary": "#00acc1",
"secondary": "#455a64",
"accent": "#ff6e40",
"border": "#1a3a4a",
"sidebar": "#0a1820",
"editor": "#0c1e2a",
"selection": "#00acc133",
"highlight": "#ff6e4033"
},
"syntax": {
"keyword": "#00acc1",
"string": "#4caf50",
"comment": "#607d8b",
"function": "#ffc107",
"variable": "#90caf9",
"number": "#ff9800",
"operator": "#ec407a",
"tag": "#7c4dff"
},
"editor": {
"lineHighlight": "#ffffff0a",
"cursor": "#00acc1",
"gutterBackground": "#0a1820",
"gutterForeground": "#546e7a"
}
}
}
Color Properties:
Main Colors:
background
- Application backgroundforeground
- Default text colorprimary
- Primary UI elements and linkssecondary
- Secondary UI elementsaccent
- Highlights and accentsborder
- Border and divider colorssidebar
- Sidebar backgroundeditor
- Editor pane background
Interactive States:
selection
- Selected text backgroundhighlight
- Highlighted text backgroundhover
- Hover state coloractive
- Active state color
Syntax Highlighting:
keyword
- Language keywords (if, for, function)string
- String literalscomment
- Commentsfunction
- Function namesvariable
- Variable namesnumber
- Numeric literalsoperator
- Operators (+, -, *, /)tag
- HTML/XML tags
Theme Variables
Use CSS custom properties for dynamic theming:
/* src/styles/themes/custom.css */
:root {
--color-bg: #0c1e2a;
--color-fg: #e0e0e0;
--color-primary: #00acc1;
--color-accent: #ff6e40;
/* Editor */
--editor-font-size: 16px;
--editor-line-height: 1.6;
--editor-padding: 2rem;
/* UI */
--sidebar-width: 250px;
--toolbar-height: 48px;
--border-radius: 4px;
--transition-speed: 0.2s;
}
.dark-theme {
--color-bg: #1e1e1e;
--color-fg: #d4d4d4;
}
Dynamic Theme Switching
import { updateConfig } from '@/core/config/store';
async function switchTheme(themeName: string) {
await updateConfig({ theme: themeName });
// Broadcast theme change
await invoke('theme_broadcast', { theme: themeName });
// Update CSS variables
updateCSSVariables(themeName);
}
function updateCSSVariables(theme: string) {
const root = document.documentElement;
const colors = getThemeColors(theme);
Object.entries(colors).forEach(([key, value]) => {
root.style.setProperty(`--color-${key}`, value);
});
}
Custom Editor Extensions
Creating a TipTap Extension
import { Node } from '@tiptap/core';
import { ReactNodeViewRenderer } from '@tiptap/react';
import CustomComponent from './CustomComponent';
export const CustomExtension = Node.create({
name: 'customNode',
group: 'block',
content: 'inline*',
parseHTML() {
return [
{
tag: 'div[data-type="custom"]'
}
];
},
renderHTML({ HTMLAttributes }) {
return ['div', {
...HTMLAttributes,
'data-type': 'custom',
class: 'custom-node'
}, 0];
},
addNodeView() {
return ReactNodeViewRenderer(CustomComponent);
},
addCommands() {
return {
setCustomNode: (attributes) => ({ commands }) => {
return commands.insertContent({
type: this.name,
attrs: attributes
});
}
};
},
addKeyboardShortcuts() {
return {
'Mod-Shift-C': () => this.editor.commands.setCustomNode({})
};
}
});
Custom React Component
import React from 'react';
import { NodeViewWrapper } from '@tiptap/react';
export default function CustomComponent({ node, updateAttributes }) {
return (
<NodeViewWrapper className="custom-component">
<div className="custom-content">
<input
value={node.attrs.value}
onChange={(e) => updateAttributes({ value: e.target.value })}
placeholder="Enter value..."
/>
</div>
</NodeViewWrapper>
);
}
Registering Extension via Plugin
// plugin.json
{
"name": "custom-extension-plugin",
"permissions": ["ui:editor"]
}
// index.js
import { CustomExtension } from './CustomExtension';
export default class CustomExtensionPlugin {
activate(context) {
context.addExtension(CustomExtension);
}
deactivate() {
// Cleanup
}
}
Custom UI Components
Custom Sidebar Panel
import React from 'react';
export default function CustomPanel({ context }) {
const [data, setData] = React.useState([]);
React.useEffect(() => {
// Load panel data
loadData().then(setData);
// Listen for updates
const handler = (newData) => setData(newData);
context.on('data:updated', handler);
return () => context.off('data:updated', handler);
}, []);
return (
<div className="custom-panel">
<div className="panel-header">
<h3>Custom Panel</h3>
<button onClick={() => refreshData()}>Refresh</button>
</div>
<div className="panel-content">
{data.map(item => (
<div key={item.id} className="panel-item">
{item.name}
</div>
))}
</div>
</div>
);
}
async function loadData() {
// Load data from API or filesystem
return [];
}
Register panel:
context.registerPanel({
name: 'custom-panel',
title: 'Custom Panel',
icon: 'box',
component: CustomPanel,
position: 'right'
});
Custom Toolbar
import React from 'react';
export default function CustomToolbar({ editor }) {
return (
<div className="custom-toolbar">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'active' : ''}
>
Bold
</button>
<button
onClick={() => insertCustomBlock(editor)}
>
Insert Custom
</button>
<select
onChange={(e) => setHeading(editor, e.target.value)}
value={getCurrentHeading(editor)}
>
<option value="paragraph">Paragraph</option>
<option value="heading1">Heading 1</option>
<option value="heading2">Heading 2</option>
<option value="heading3">Heading 3</option>
</select>
</div>
);
}
function insertCustomBlock(editor) {
editor.chain().focus().setCustomNode({
type: 'customBlock'
}).run();
}
function setHeading(editor, level) {
if (level === 'paragraph') {
editor.chain().focus().setParagraph().run();
} else {
const headingLevel = parseInt(level.replace('heading', ''));
editor.chain().focus().setHeading({ level: headingLevel }).run();
}
}
function getCurrentHeading(editor) {
for (let i = 1; i <= 6; i++) {
if (editor.isActive('heading', { level: i })) {
return `heading${i}`;
}
}
return 'paragraph';
}
Custom Commands
Adding Slash Commands
context.addSlashCommand({
name: 'current-date',
description: 'Insert current date',
icon: 'calendar',
keywords: ['date', 'today'],
handler: (editor) => {
const date = new Date().toISOString().split('T')[0];
editor.commands.insertContent(date);
}
});
context.addSlashCommand({
name: 'code-snippet',
description: 'Insert code snippet',
icon: 'code',
handler: async (editor) => {
const language = await promptForLanguage();
editor.commands.insertContent({
type: 'codeBlock',
attrs: { language }
});
}
});
Command Palette Integration
// Register custom command in palette
registerCommand({
id: 'custom:action',
name: 'Custom Action',
description: 'Performs custom action',
category: 'Custom',
shortcut: 'Cmd+Shift+X',
handler: async () => {
// Command implementation
await performCustomAction();
}
});
Workspace Customization
Custom File Templates
// Define templates
const templates = {
'meeting-notes': {
name: 'Meeting Notes',
content: `# Meeting Notes - {date}
## Attendees
-
## Agenda
1.
## Notes
## Action Items
- [ ]
## Next Meeting
- Date:
- Time: `
},
'project-plan': {
name: 'Project Plan',
content: `# {title}
**Created:** {date}
**Status:** Planning
## Overview
## Goals
## Timeline
## Resources
## Risks
`
}
};
// Use template
async function createFromTemplate(templateId, variables) {
const template = templates[templateId];
let content = template.content;
// Replace variables
Object.entries(variables).forEach(([key, value]) => {
content = content.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
});
// Create file
const filePath = await invoke('create_file_in_workspace', {
workspace_path: workspacePath,
name: `${variables.title}.md`
});
await invoke('write_file_content', {
path: filePath,
content
});
return filePath;
}
Custom Folder Structure
const workspaceStructure = {
'Inbox': {
description: 'Quick notes and captures'
},
'Projects': {
description: 'Project-specific notes',
subfolders: ['Active', 'Archive', 'Ideas']
},
'Resources': {
description: 'Reference materials',
subfolders: ['Articles', 'Books', 'Research']
},
'Daily': {
description: 'Daily notes',
pattern: '{year}/{month}'
}
};
async function initializeWorkspace(workspacePath: string) {
for (const [folder, config] of Object.entries(workspaceStructure)) {
await invoke('create_folder_in_workspace', {
workspace_path: workspacePath,
name: folder
});
if (config.subfolders) {
for (const subfolder of config.subfolders) {
await invoke('create_folder_in_workspace', {
workspace_path: `${workspacePath}/${folder}`,
name: subfolder
});
}
}
}
}
Advanced Plugin Development
Plugin with Multiple Features
export default class AdvancedPlugin {
constructor() {
this.state = {};
this.timers = [];
}
activate(context) {
this.context = context;
// Initialize features
this.initializeUI();
this.registerCommands();
this.setupEventListeners();
this.startBackgroundTasks();
}
initializeUI() {
// Add toolbar buttons
this.context.addToolbarButton({
name: 'plugin-action',
label: 'Plugin Action',
icon: 'star',
action: () => this.performAction()
});
// Register panel
this.context.registerPanel({
name: 'plugin-panel',
title: 'Plugin Panel',
component: this.createPanel()
});
}
registerCommands() {
this.context.addSlashCommand({
name: 'plugin-command',
description: 'Plugin command',
handler: (editor) => this.handleCommand(editor)
});
}
setupEventListeners() {
this.context.on('file:save', this.onFileSave.bind(this));
this.context.on('editor:selection', this.onSelection.bind(this));
}
startBackgroundTasks() {
// Auto-save state periodically
const timer = setInterval(() => {
this.saveState();
}, 60000);
this.timers.push(timer);
}
async performAction() {
try {
const content = this.context.getEditorContent();
const processed = await this.processContent(content);
this.context.setEditorContent(processed);
this.context.showNotification({
message: 'Action completed!',
type: 'success'
});
} catch (error) {
this.context.showNotification({
message: `Error: ${error.message}`,
type: 'error'
});
}
}
async processContent(content) {
// Custom processing logic
return content;
}
async saveState() {
await this.context.setSetting('state', this.state);
}
deactivate() {
// Clean up timers
this.timers.forEach(timer => clearInterval(timer));
// Remove event listeners
this.context.removeAllListeners();
// Save final state
this.saveState();
}
}
CSS Customization
Custom Styles
/* Custom editor styles */
.ProseMirror {
padding: 3rem;
max-width: 800px;
margin: 0 auto;
}
.ProseMirror h1 {
font-size: 2.5em;
font-weight: 700;
margin: 2em 0 0.5em;
color: var(--color-primary);
}
.ProseMirror p {
margin: 1em 0;
line-height: 1.8;
}
.ProseMirror code {
background: var(--color-code-bg);
padding: 0.2em 0.4em;
border-radius: 3px;
font-family: var(--font-mono);
}
/* Custom sidebar styles */
.sidebar {
background: var(--color-sidebar);
border-right: 1px solid var(--color-border);
}
.sidebar-item {
padding: 0.5em 1em;
cursor: pointer;
transition: background 0.2s;
}
.sidebar-item:hover {
background: var(--color-hover);
}
.sidebar-item.active {
background: var(--color-active);
color: var(--color-primary);
}
Integration Examples
Custom AI Integration
interface AIConfig {
apiKey: string;
model: string;
maxTokens: number;
}
class AIAssistant {
constructor(private context: PluginAPI) {}
async summarize(text: string): Promise<string> {
const config = await this.getConfig();
const response = await fetch('https://api.example.com/summarize', {
method: 'POST',
headers: {
'Authorization': `Bearer ${config.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
text,
model: config.model,
max_tokens: config.maxTokens
})
});
const data = await response.json();
return data.summary;
}
async getConfig(): Promise<AIConfig> {
return {
apiKey: await this.context.getSetting('aiApiKey', ''),
model: await this.context.getSetting('aiModel', 'gpt-4'),
maxTokens: await this.context.getSetting('maxTokens', 1000)
};
}
}
// Usage in plugin
export default class AIPlugin {
activate(context) {
const ai = new AIAssistant(context);
context.addSlashCommand({
name: 'summarize',
description: 'Summarize selected text',
handler: async (editor) => {
const selection = context.getSelection();
const summary = await ai.summarize(selection.text);
editor.commands.insertContent(`\n\n**Summary:**\n${summary}\n`);
}
});
}
}
Next Steps
- Performance Optimization - Optimize performance
- Security Features - Security best practices
- Troubleshooting - Common issues