` — 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
### 4. Don't spam the logs
`console.logs` should be kept to a minimum.
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
**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.
```