` — Adds hover lift effect and primary border highlight
**Utility classes:**
- `.text-muted` — Muted text color (`var(--text-color-muted)`)
- `.text-primary` — Primary theme color (`var(--c-primary)`)
- `.page-fade` — Fade-in animation (0.3s ease)
**Key CSS variables:**
- `--bg`, `--bg-darker` — Background colors
- `--text-color`, `--text-color-muted` — Text colors
- `--c-primary`, `--c-accent`, `--c-warn` — Theme colors
- `--card-bg`, `--card-shadow`, `--card-border-radius` — Card styling
- `--divider-color` — Border/divider color
- `--s`, `--s2`, `--s3`, `--s4`, `--s-half`, `--s-quarter` — Spacing scale
- `--transition-standard` — Standard transition
- `--font-primary-stack` — App font stack
- `--whiteframe-shadow-1dp` through `--whiteframe-shadow-24dp` — Elevation shadows
- `--is-dark-theme` — `1` if dark theme, `0` if light
## Available API Methods
### Data Operations
#### Tasks
- `getTasks()` - Get all active tasks
- `getArchivedTasks()` - Get archived tasks
- `getCurrentContextTasks()` - Get tasks in current context
- `addTask(task)` - Create a new task
- `updateTask(taskId, updates)` - Update existing task
#### Projects
- `getAllProjects()` - Get all projects
- `addProject(project)` - Create new project
- `updateProject(projectId, updates)` - Update project
#### Tags
- `getAllTags()` - Get all tags
- `addTag(tag)` - Create new tag
- `updateTag(tagId, updates)` - Update tag
#### Simple Counters
Simple counters let you track lightweight metrics (e.g., daily clicks or habits) that persist and sync with your data. There are two levels: **basic** (key-value pairs for today's count) and **full model** (full CRUD on `SimpleCounter` entities with date-specific values).
##### Basic Counters
These treat counters as a simple `{ [id: string]: number }` map for today's values (auto-upserts via NgRx).
| Method | Description | Example |
| --------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `getAllCounters()` | Get all counters as `{ [id: string]: number }` | `const counters = await PluginAPI.getAllCounters(); console.log(counters['my-key']);` |
| `getCounter(id)` | Get today's value for a counter (returns `null` if unset) | `const val = await PluginAPI.getCounter('daily-commits');` |
| `setCounter(id, value)` | Set today's value (non-negative number; validates id regex `/^[A-Za-z0-9_-]+$/`) | `await PluginAPI.setCounter('daily-commits', 5);` |
| `incrementCounter(id, incrementBy = 1)` | Increment and return new value (floors at 0) | `const newVal = await PluginAPI.incrementCounter('daily-commits', 2);` |
| `decrementCounter(id, decrementBy = 1)` | Decrement and return new value (floors at 0) | `const newVal = await PluginAPI.decrementCounter('daily-commits');` |
| `deleteCounter(id)` | Delete the counter | `await PluginAPI.deleteCounter('daily-commits');` |
**Example:**
```javascript
// Track daily commits
let commits = (await PluginAPI.getCounter('daily-commits')) ?? 0;
await PluginAPI.incrementCounter('daily-commits');
PluginAPI.showSnack({
msg: `Commits today: ${await PluginAPI.getCounter('daily-commits')}`,
type: 'INFO',
});
```
##### Full SimpleCounter Model
For advanced use: Full CRUD on counters with metadata (title, enabled state, date-specific values via `countOnDay: { [date: string]: number }`).
| Method | Description | Example |
| ---------------------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `getAllSimpleCounters()` | Get all as `SimpleCounter[]` | `const all = await PluginAPI.getAllSimpleCounters();` |
| `getSimpleCounter(id)` | Get one by id (returns `undefined` if not found) | `const counter = await PluginAPI.getSimpleCounter('my-id');` |
| `updateSimpleCounter(id, updates)` | Partial update (e.g., `{ title: 'New Title', countOnDay: { '2025-11-17': 10 } }`) | `await PluginAPI.updateSimpleCounter('my-id', { isEnabled: false });` |
| `toggleSimpleCounter(id)` | Toggle `isOn` state (throws if not found) | `await PluginAPI.toggleSimpleCounter('my-id');` |
| `setSimpleCounterEnabled(id, isEnabled)` | Set enabled state | `await PluginAPI.setSimpleCounterEnabled('my-id', true);` |
| `deleteSimpleCounter(id)` | Delete by id | `await PluginAPI.deleteSimpleCounter('my-id');` |
| `setSimpleCounterToday(id, value)` | Set today's value (YYYY-MM-DD) | `await PluginAPI.setSimpleCounterToday('my-id', 10);` |
| `setSimpleCounterDate(id, date, value)` | Set value for specific date (validates YYYY-MM-DD) | `await PluginAPI.setSimpleCounterDate('my-id', '2025-11-16', 5);` |
**Example:**
```javascript
// Create/update a habit counter
await PluginAPI.updateSimpleCounter('habit-streak', {
title: 'Daily Streak',
type: 'ClickCounter',
isEnabled: true,
countOnDay: { '2025-11-17': 1 }, // Today's count
});
await PluginAPI.toggleSimpleCounter('habit-streak');
const counter = await PluginAPI.getSimpleCounter('habit-streak');
console.log(`Streak on: ${counter.isOn}`);
```
### UI Operations
#### Notifications
```javascript
// Show snackbar notification
PluginAPI.showSnack({
msg: 'Operation completed!',
type: 'SUCCESS', // SUCCESS, ERROR, INFO, WARNING
ico: 'check', // Optional Material icon
actionStr: 'Undo', // Optional action button
actionFn: () => console.log('Undo clicked'),
});
// System notification
PluginAPI.notify({
title: 'Task Complete',
body: 'Great job!',
ico: 'done',
});
```
#### Dialogs
```javascript
// Open a dialog
const result = await PluginAPI.openDialog({
title: 'Confirm Action',
content: 'Are you sure?',
okBtnLabel: 'Yes',
cancelBtnLabel: 'No',
});
```
### Registration Methods (plugin.js only)
#### Header Button
```javascript
PluginAPI.registerHeaderButton({
id: 'my-header-btn', // Optional unique ID
label: 'Click Me',
icon: 'star', // Material icon name
onClick: () => {
console.log('Header button clicked');
},
});
```
#### Menu Entry
```javascript
PluginAPI.registerMenuEntry({
label: 'My Plugin Action',
icon: 'extension',
onClick: () => {
console.log('Menu item clicked');
},
});
```
#### Side Panel Button
```javascript
PluginAPI.registerSidePanelButton({
label: 'My Panel',
icon: 'dashboard',
onClick: () => {
PluginAPI.showIndexHtmlAsView();
},
});
```
#### Keyboard Shortcut
```javascript
PluginAPI.registerShortcut({
keys: 'ctrl+shift+p',
label: 'My Plugin Shortcut',
action: () => {
console.log('Shortcut triggered');
},
});
```
#### Hooks
```javascript
// Available hooks
const hooks = {
TASK_COMPLETE: 'taskComplete',
TASK_UPDATE: 'taskUpdate',
TASK_DELETE: 'taskDelete',
CURRENT_TASK_CHANGE: 'currentTaskChange',
FINISH_DAY: 'finishDay',
LANGUAGE_CHANGE: 'languageChange',
PERSISTED_DATA_UPDATE: 'persistedDataUpdate',
ACTION: 'action',
};
// Register hook listener
PluginAPI.registerHook(PluginAPI.Hooks.TASK_COMPLETE, (taskId) => {
console.log(`Task ${taskId} completed!`);
});
// Listen to Redux actions
PluginAPI.registerHook(PluginAPI.Hooks.ACTION, (action) => {
if (action.type === 'ADD_TASK_SUCCESS') {
console.log('New task added:', action.payload);
// Bonus: Increment a counter on task add
PluginAPI.incrementCounter('tasks-added-today');
}
});
```
### Data Persistence
You can persist data that will also be synced vai the `persistDataSynced` and `loadSyncedData` APIs. For local storage I recommend using `localStorage`.
```javascript
// Save plugin data
await PluginAPI.persistDataSynced(JSON.stringify({ count: 42 }));
// Load saved data
const data = await PluginAPI.loadSyncedData();
console.log(data); // '{ count: 42 }'
```
## Best Practices
### 1. Performance
- **Lazy load resources**: Don't load everything on plugin initialization
- **Be responsive with using resources**: Avoid heavy operations and don't save excessive amounts of data.
- **Keep it lightweight**: Super Productivity is not the only app on the users system and your plugin is not the only plugin.
### 2. User Experience
- **Provide feedback**: Show loading states and confirmations
- **Be non-intrusive**: Don't spam notifications
- **Follow the app's design**: Use the injected theme variables and try to keep styles minimal.
- **Respect user preferences**: Check dark mode, and language settings (if possible or stick to english if not)
### 3. Security
- **Request minimal permissions**: Only what you need
### Node.js Script Execution
Plugins with `"permissions": ["nodeExecution"]` can run Node.js scripts in the Electron
desktop app.
```javascript
const result = await plugin.executeNodeScript({
script: `
const os = require('os');
return os.hostname();
`,
timeout: 5000,
});
if (result.success) {
console.log('Hostname:', result.result);
}
```
**Important — use `plugin.onReady()` for startup calls:**
`executeNodeScript` requires the Electron IPC bridge to be available. On cold boot this
bridge may not be ready when `plugin.js` first runs. Always put `executeNodeScript` calls
(and any other startup init code) inside `plugin.onReady()`:
```javascript
// ❌ May fail on cold boot
const result = await plugin.executeNodeScript({ script: 'return true' });
// ✅ Correct — fires after the bridge is confirmed available
plugin.onReady(async () => {
const result = await plugin.executeNodeScript({ script: 'return true' });
});
```
`plugin.onReady(fn)` fires after `plugin.js` has fully evaluated **and** the app has
confirmed the Node.js IPC bridge is responding (with automatic retry). If the bridge is
unavailable after retries, an error is shown in the plugin management UI and `onReady` does
not fire.
You can also use `onReady` for any other startup work that should run after the plugin
script has finished setting up its hooks and registrations — not just for `nodeExecution`.
**Iframe plugins:** `plugin.onReady()` is also available inside iframe plugins, but it
fires on the next microtask after `plugin.js` finishes evaluating — without an IPC bridge
ping. This is fine in practice because iframe plugins are rendered on user navigation
(well after host startup, when the bridge is already up). If your iframe plugin needs the
bridge from `onReady`, it will be available; cold-boot races affect host-side plugin code
only.
### 4. Don't spam the logs
`console.logs` should be kept to a minimum.
### 5. Iframe plugins: inline everything
1. **Inline everything**: CSS and JavaScript must be in the HTML file
```html
```
## Security Considerations
### Sandboxing
- JavaScript plugins run in isolated VM contexts
- Iframe plugins run in sandboxed iframes with restricted permissions
- No access to file system unless through API
### API Restrictions
In iframe context, these methods are NOT available:
- `registerHeaderButton()`
- `registerMenuEntry()`
- `registerSidePanelButton()`
- `registerShortcut()`
- `registerHook()`
- `execNodeScript()`
### Content Security Policy
- External scripts/styles are blocked in iframes
- Only same-origin resources are allowed
- Inline scripts must be within the HTML file
## Testing Your Plugin
### 1. Local Development
1. Use "Load Plugin from Folder" to test your plugin
2. Open DevTools (F12 or Ctrl+Shift+i) to see console logs
3. Use the API Test Plugin as reference
### 2. Debugging Tips
```javascript
// Add debug logging
const DEBUG = true;
function log(...args) {
if (DEBUG) {
console.log('[MyPlugin]', ...args);
}
}
// Test API methods
async function testAPI() {
log('Testing getTasks...');
const tasks = await PluginAPI.getTasks();
log('Tasks:', tasks);
log('Testing showSnack...');
PluginAPI.showSnack({
msg: 'API test successful!',
type: 'SUCCESS',
});
}
```
### 3. Common Issues
**Plugin not loading:**
- Check manifest.json syntax
- Verify minSupVersion compatibility
- Look for errors in console
**API methods failing:**
- Check if method is available in current context
- Verify permissions in manifest
- If `executeNodeScript` fails on startup or cold boot, wrap your init code in
`plugin.onReady(async () => { ... })` — this ensures the Node.js bridge is ready before
your code runs
**Iframe not displaying:**
- Check that all resources are inlined
- Verify no external dependencies
- Look for CSP violations in console
## Resources
- **Plugin API Types**: [@super-productivity/plugin-api](https://www.npmjs.com/package/@super-productivity/plugin-api)
- **Plugin Boilerplate**: [boilerplate-solid-js](../packages/plugin-dev/boilerplate-solid-js)
- **Example Plugins**: [plugin-dev](../packages/plugin-dev)
- **Community Plugins**:
- [counter-tester-plugin](https://github.com/Mustache-Games/counter-tester-plugin) by [Mustache Dev](https://github.com/Mustache-Games)
- [sp-reporter](https://github.com/dougcooper/sp-reporter) by [dougcooper](https://github.com/dougcooper)
## Contributing
If you create a useful plugin, consider:
1. Posting on reddit or GitHub discussions about it
2. Submitting a PR to add it to the community plugins list (coming soon)
Happy plugin development! 🚀
## Bonus: Vibe Coding your Plugins
### Tips
- Don't test on your real world data! Use a test instance! (you can use https://test-app.super-productivity.com/ if you don't know how get one)
- Be as specific as possible
- Outline what APIs your plugin should use
- Test for errors (`Ctrl+Shift+i` opens the console) and iterate until it works. Don't expect that everything works on your first try.
- Read the code! Don't trust it blindly.
### Example
```md
Can you you write me a plugin for Super Productivity that plays a beep sound every time i click on a header button (You need to add a header button via PluginAPI.registerHeaderButton).
Here are the docs: https://github.com/super-productivity/super-productivity/blob/master/docs/plugin-development.md
Don't use any PluginAPI methods that are not listed in the guide.
Please give me the output as flat zip file to download.
```