mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-05 19:24:39 +00:00
docs: improve plugin documentation
This commit is contained in:
+3
-13
@@ -12,24 +12,14 @@ Welcome to the BetterSEQTA+ documentation! This documentation will help you unde
|
||||
- [Contributing Guide](../CONTRIBUTING.md) - How to contribute to BetterSEQTA+
|
||||
|
||||
### Plugin System
|
||||
- [Plugin System Overview](./plugins/README.md) - Overview of the plugin system
|
||||
- [Creating Your First Plugin](./plugins/creating-plugins.md) - Guide to creating a simple plugin
|
||||
|
||||
### Settings System
|
||||
- [Settings System Overview](./settings/README.md) - How the type-safe settings system works
|
||||
- [Creating Plugins with Settings](./settings/creating-plugins.md) - How to use the decorator-based settings in plugins
|
||||
- [Creating Custom UI Components](./settings/custom-ui-components.md) - How to create custom UI components for settings
|
||||
|
||||
### Advanced Topics
|
||||
- [TypeScript Type System](./advanced/typescript.md) - How BetterSEQTA+ leverages TypeScript for type safety
|
||||
- [Plugin API Reference](./advanced/plugin-api.md) - Detailed reference for the Plugin API
|
||||
- [Storage API Reference](./advanced/storage-api.md) - Detailed reference for the Storage API
|
||||
- [Creating Your First Plugin](./plugins/README.md) - A comprehensive, beginner-friendly guide to creating plugins
|
||||
- [Plugin API Reference](./plugins/api-reference.md) - Detailed technical documentation of the plugin APIs
|
||||
|
||||
## Core Concepts
|
||||
|
||||
BetterSEQTA+ is built around several core concepts:
|
||||
|
||||
1. **Plugin System**: BetterSEQTA+ uses a plugin system to extend SEQTA with new features. Plugins are self-contained pieces of code that can be enabled or disabled by the user.
|
||||
1. **Plugin System**: BetterSEQTA+ uses a plugin system to extend SEQTA with new features. Plugins are self-contained pieces of code that can be enabled or disabled by the user. Check out our [plugin guide](./plugins/README.md) to learn how to create your own!
|
||||
|
||||
2. **Type-Safe Settings**: Each plugin can define settings that are type-safe and automatically rendered in the settings UI. The settings system uses TypeScript decorators to make it easy to define settings with proper typing.
|
||||
|
||||
|
||||
@@ -176,6 +176,5 @@ bun run dev
|
||||
|
||||
Now that you have BetterSEQTA+ installed, you can:
|
||||
|
||||
- [Configure your settings](./settings/README.md)
|
||||
- [Create your own plugins](./plugins/creating-plugins.md)
|
||||
- [Getting Started with Plugins](./plugins/getting-started.md)
|
||||
- [Contribute to the project](../CONTRIBUTING.md)
|
||||
+223
-121
@@ -1,155 +1,257 @@
|
||||
# BetterSEQTA+ Plugin System
|
||||
# Creating Plugins for BetterSEQTA+
|
||||
|
||||
BetterSEQTA+ features a powerful plugin system that allows developers to extend and customize the functionality of SEQTA Learn. This document provides an overview of how the plugin system works and how to get started with creating your own plugins.
|
||||
Hey there! 👋 So you want to create a plugin for BetterSEQTA+? That's awesome! This guide will walk you through everything you need to know, from the very basics to more advanced features. Don't worry if you're new to this - we'll explain everything step by step.
|
||||
|
||||
## What is a Plugin?
|
||||
|
||||
A plugin is a self-contained piece of code that adds functionality to BetterSEQTA+. Plugins can:
|
||||
In BetterSEQTA+, a plugin is like a mini-app that adds new features to SEQTA. Think of it as a piece of LEGO that you can snap onto SEQTA to make it do new things. For example, you could create a plugin that:
|
||||
- Changes how SEQTA looks
|
||||
- Adds new buttons or features
|
||||
- Shows extra information on your timetable
|
||||
- Collects notifications in a better way
|
||||
- Really, anything you can imagine!
|
||||
|
||||
- Add new UI elements to SEQTA Learn
|
||||
- Modify existing UI elements
|
||||
- Add new features to SEQTA Learn
|
||||
- Modify or extend existing features
|
||||
- Store and retrieve user data
|
||||
- Respond to events in SEQTA Learn
|
||||
## Your First Plugin
|
||||
|
||||
Each plugin is isolated from other plugins, with its own settings, storage, and lifecycle. This ensures that plugins can be enabled, disabled, or removed without affecting other parts of the system.
|
||||
|
||||
## Plugin Architecture
|
||||
|
||||
The BetterSEQTA+ plugin system consists of several key components:
|
||||
|
||||
### 1. Plugin Interface
|
||||
|
||||
All plugins implement the `Plugin` interface, which defines the structure and lifecycle methods of a plugin:
|
||||
Let's create a super simple plugin together. We'll make one that adds a friendly message to the SEQTA homepage. Here's what we'll need:
|
||||
|
||||
```typescript
|
||||
export interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
settings: T;
|
||||
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
|
||||
}
|
||||
import type { Plugin } from '@/plugins/core/types';
|
||||
|
||||
const myFirstPlugin: Plugin = {
|
||||
// Every plugin needs these basic details
|
||||
id: 'my-first-plugin',
|
||||
name: 'My First Plugin',
|
||||
description: 'Adds a friendly message to SEQTA',
|
||||
version: '1.0.0',
|
||||
|
||||
// This tells BetterSEQTA+ that users can turn our plugin on/off
|
||||
disableToggle: true,
|
||||
|
||||
// This is where the magic happens!
|
||||
run: async (api) => {
|
||||
// Wait for the homepage to load
|
||||
api.seqta.onMount('.home-page', (homePage) => {
|
||||
// Create our message
|
||||
const message = document.createElement('div');
|
||||
message.textContent = 'Hello from my first plugin! 🎉';
|
||||
message.style.padding = '20px';
|
||||
message.style.backgroundColor = '#e9f5ff';
|
||||
message.style.borderRadius = '8px';
|
||||
message.style.margin = '20px';
|
||||
|
||||
// Add it to the page
|
||||
homePage.prepend(message);
|
||||
});
|
||||
|
||||
// Return a cleanup function that removes our message when the plugin is disabled
|
||||
return () => {
|
||||
const message = document.querySelector('.home-page > div');
|
||||
message?.remove();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default myFirstPlugin;
|
||||
```
|
||||
|
||||
### 2. Plugin API
|
||||
Let's break down what's happening here:
|
||||
|
||||
When a plugin is run, it receives an instance of the `PluginAPI`, which provides access to various services and utilities:
|
||||
1. First, we import the `Plugin` type that tells TypeScript what a plugin should look like
|
||||
2. We create our plugin object with some basic information:
|
||||
- `id`: A unique name for your plugin (use lowercase and dashes)
|
||||
- `name`: A friendly name that users will see
|
||||
- `description`: Explain what your plugin does
|
||||
- `version`: Your plugin's version number
|
||||
3. We set `disableToggle: true` so users can turn our plugin on/off in settings
|
||||
4. The `run` function is where we put our plugin's code
|
||||
5. We use `api.seqta.onMount` to wait for the homepage to load
|
||||
6. We create and style a message element
|
||||
7. We return a cleanup function that removes our changes when the plugin is disabled
|
||||
|
||||
## The Plugin API
|
||||
|
||||
When your plugin runs, it gets access to a powerful API that lets you do all sorts of things. Let's look at what you can do:
|
||||
|
||||
### SEQTA API (`api.seqta`)
|
||||
|
||||
This helps you interact with SEQTA's pages:
|
||||
|
||||
```typescript
|
||||
export interface PluginAPI<T extends PluginSettings, S = any> {
|
||||
seqta: SEQTAAPI;
|
||||
settings: SettingsAPI<T>;
|
||||
storage: TypedStorageAPI<S>;
|
||||
events: EventsAPI;
|
||||
}
|
||||
// Wait for an element to appear on the page
|
||||
api.seqta.onMount('.some-class', (element) => {
|
||||
// Do something with the element
|
||||
});
|
||||
|
||||
// Know when the user changes pages
|
||||
api.seqta.onPageChange((page) => {
|
||||
console.log('User went to:', page);
|
||||
});
|
||||
|
||||
// Get the current page
|
||||
const currentPage = api.seqta.getCurrentPage();
|
||||
```
|
||||
|
||||
- **SEQTA API**: Provides methods for interacting with the SEQTA Learn UI
|
||||
- **Settings API**: Provides type-safe access to plugin settings
|
||||
- **Storage API**: Provides type-safe persistent storage for plugin data
|
||||
- **Events API**: Allows plugins to emit and listen for events
|
||||
### Settings API (`api.settings`)
|
||||
|
||||
### 3. Plugin Manager
|
||||
|
||||
The Plugin Manager is responsible for loading, starting, stopping, and managing plugins. It handles the lifecycle of each plugin and ensures that plugins have access to the resources they need.
|
||||
|
||||
### 4. Plugin Registry
|
||||
|
||||
The Plugin Registry is a central repository of all available plugins. Built-in plugins are automatically registered, and additional plugins can be registered dynamically.
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
Plugins follow a simple lifecycle:
|
||||
|
||||
1. **Registration**: The plugin is registered with the Plugin Manager
|
||||
2. **Loading**: The plugin's settings and storage are loaded
|
||||
3. **Running**: The plugin's `run` method is called with the Plugin API
|
||||
4. **Cleanup**: If the plugin returns a cleanup function, it is called when the plugin is stopped
|
||||
|
||||
## Creating a Plugin
|
||||
|
||||
Creating a plugin for BetterSEQTA+ involves a few simple steps:
|
||||
|
||||
1. Define your plugin's interface
|
||||
2. Implement the Plugin interface
|
||||
3. Register your plugin with the Plugin Manager
|
||||
|
||||
For a detailed guide on creating plugins, see [Creating Your First Plugin](./creating-plugins.md).
|
||||
|
||||
## Built-in Plugins
|
||||
|
||||
BetterSEQTA+ comes with several built-in plugins that provide core functionality:
|
||||
|
||||
- **Timetable**: Enhances the SEQTA timetable view
|
||||
- **Notification Collector**: Improves the notification system
|
||||
- **Theme Customizer**: Allows customization of the SEQTA theme
|
||||
- **Assessment Enhancer**: Adds features to the assessment view
|
||||
|
||||
These plugins serve as good examples of how to use the plugin system effectively.
|
||||
|
||||
## Type-Safe Settings and Storage
|
||||
|
||||
One of the key features of the BetterSEQTA+ plugin system is its type-safe settings and storage. Using TypeScript generics, plugins can define the structure of their settings and storage, ensuring that they are used correctly throughout the codebase.
|
||||
|
||||
### Settings Example
|
||||
Want to let users customize your plugin? Use settings!
|
||||
|
||||
```typescript
|
||||
interface MyPluginSettings extends PluginSettings {
|
||||
enabled: {
|
||||
type: 'boolean';
|
||||
default: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
refreshInterval: {
|
||||
type: 'number';
|
||||
default: number;
|
||||
title: string;
|
||||
description: string;
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
import { BasePlugin } from '@/plugins/core/settings';
|
||||
import { booleanSetting, defineSettings, Setting } from '@/plugins/core/settingsHelpers';
|
||||
|
||||
// Define your settings
|
||||
const settings = defineSettings({
|
||||
showMessage: booleanSetting({
|
||||
default: true,
|
||||
title: "Show Welcome Message",
|
||||
description: "Show a friendly message on the homepage",
|
||||
})
|
||||
});
|
||||
|
||||
// Create a class for your plugin
|
||||
class MyPluginClass extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.showMessage)
|
||||
showMessage!: boolean;
|
||||
}
|
||||
|
||||
// Create your plugin
|
||||
const settingsInstance = new MyPluginClass();
|
||||
|
||||
const myPlugin: Plugin<typeof settings> = {
|
||||
// ... other plugin details ...
|
||||
settings: settingsInstance.settings,
|
||||
|
||||
run: async (api) => {
|
||||
// Use the setting
|
||||
if (api.settings.showMessage) {
|
||||
// Show the message
|
||||
}
|
||||
|
||||
// Listen for setting changes
|
||||
api.settings.onChange('showMessage', (newValue) => {
|
||||
if (newValue) {
|
||||
// Show the message
|
||||
} else {
|
||||
// Hide the message
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Storage Example
|
||||
### Storage API (`api.storage`)
|
||||
|
||||
Need to save some data? The storage API has got you covered:
|
||||
|
||||
```typescript
|
||||
interface MyPluginStorage {
|
||||
lastRefresh: string;
|
||||
savedItems: string[];
|
||||
userPreferences: {
|
||||
theme: 'light' | 'dark';
|
||||
fontSize: number;
|
||||
};
|
||||
}
|
||||
// Save some data
|
||||
await api.storage.set('lastVisit', new Date().toISOString());
|
||||
|
||||
// Get it back later
|
||||
const lastVisit = await api.storage.get('lastVisit');
|
||||
|
||||
// Listen for changes
|
||||
api.storage.onChange('lastVisit', (newValue) => {
|
||||
console.log('Last visit updated:', newValue);
|
||||
});
|
||||
```
|
||||
|
||||
## Decorator-Based Settings
|
||||
### Events API (`api.events`)
|
||||
|
||||
BetterSEQTA+ also offers a more modern, decorator-based approach to defining settings. For more information, see [Creating Plugins with Settings](../settings/creating-plugins.md).
|
||||
Want your plugin to be able to interface with other plugins? Then use events!
|
||||
|
||||
## Plugin API Reference
|
||||
```typescript
|
||||
// Listen for an event
|
||||
api.events.on('myCustomEvent', (data) => {
|
||||
console.log('Got event:', data);
|
||||
});
|
||||
|
||||
The Plugin API provides a rich set of features for interacting with SEQTA Learn. For a complete reference, see [Plugin API Reference](../advanced/plugin-api.md).
|
||||
// Send an event
|
||||
api.events.emit('myCustomEvent', { some: 'data' });
|
||||
```
|
||||
|
||||
## Adding Styles
|
||||
|
||||
Want to make your plugin look pretty? You can add CSS styles:
|
||||
|
||||
```typescript
|
||||
const myPlugin: Plugin = {
|
||||
// ... other plugin details ...
|
||||
|
||||
// Add your CSS here
|
||||
styles: `
|
||||
.my-plugin-message {
|
||||
background: linear-gradient(135deg, #6e8efb, #a777e3);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin: 20px;
|
||||
animation: slide-in 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
from { transform: translateY(-20px); opacity: 0; }
|
||||
to { transform: translateY(0); opacity: 1; }
|
||||
}
|
||||
`,
|
||||
|
||||
run: async (api) => {
|
||||
// Your plugin code here
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
When creating plugins for BetterSEQTA+, consider these best practices:
|
||||
Here are some tips to make your plugin awesome:
|
||||
|
||||
1. **Use TypeScript**: Take advantage of TypeScript's type system to ensure type safety in your plugins.
|
||||
2. **Keep Plugins Focused**: Each plugin should do one thing well.
|
||||
3. **Handle Cleanup**: Always return a cleanup function from your plugin's `run` method to ensure proper resource management.
|
||||
4. **Document Your Code**: Add clear documentation to your code, especially for public APIs.
|
||||
5. **Test Thoroughly**: Test your plugins in different environments and with different configurations.
|
||||
6. **Follow UI Guidelines**: When adding UI elements, follow the SEQTA Learn UI guidelines to maintain a consistent experience.
|
||||
7. **Optimize Performance**: Be mindful of performance impact, especially for plugins that run on every page.
|
||||
1. **Always Clean Up**: When your plugin is disabled, clean up any changes you made:
|
||||
```typescript
|
||||
run: async (api) => {
|
||||
// Add stuff to the page
|
||||
const element = document.createElement('div');
|
||||
document.body.appendChild(element);
|
||||
|
||||
// Return a cleanup function
|
||||
return () => {
|
||||
element.remove();
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
2. **Use TypeScript**: It helps catch errors before they happen and makes your code easier to understand.
|
||||
|
||||
- [Creating Your First Plugin](./creating-plugins.md)
|
||||
- [Plugin API Reference](../advanced/plugin-api.md)
|
||||
- [Typed Storage API](../advanced/storage-api.md)
|
||||
3. **Test Your Plugin**: Make sure it works in different situations:
|
||||
- When SEQTA is loading
|
||||
- When the user switches pages
|
||||
- When the plugin is enabled/disabled
|
||||
- When settings are changed
|
||||
|
||||
4. **Keep It Fast**: Don't slow down SEQTA:
|
||||
- Use `onMount` instead of intervals or timeouts
|
||||
- Clean up event listeners when they're not needed
|
||||
- Don't do heavy calculations on the main thread
|
||||
|
||||
5. **Make It User-Friendly**:
|
||||
- Add clear settings with good descriptions
|
||||
- Use `disableToggle: true` so users can turn it off if needed
|
||||
- Add helpful error messages if something goes wrong
|
||||
|
||||
## Examples
|
||||
|
||||
Want to see more examples? Check out our built-in plugins:
|
||||
- [themes](../../src/plugins/built-in/themes/index.ts): Shows how to change SEQTA's appearance
|
||||
- [notificationCollector](../../src/plugins/built-in/notificationCollector/index.ts): Shows how to work with SEQTA's notifications
|
||||
- [timetable](../../src/plugins/built-in/timetable/index.ts): Shows how to modify SEQTA's timetable view
|
||||
- [assessmentsAverage](../../src/plugins/built-in/assessmentsAverage/index.ts): Shows how to add new features to existing pages
|
||||
|
||||
## Need Help?
|
||||
|
||||
Got stuck? No worries! Here's where you can get help:
|
||||
- Join our [Discord server](https://discord.gg/YzmbnCDkat)
|
||||
- Check out the built-in plugins in the `src/plugins/built-in` folder
|
||||
- Open an issue on our [GitHub page](https://github.com/betterseqta/betterseqta-plus/issues)
|
||||
|
||||
Happy coding and feel free to checkout the api reference [here](./api-reference.md)
|
||||
@@ -0,0 +1,288 @@
|
||||
# Plugin API Reference
|
||||
|
||||
This document provides detailed technical information about BetterSEQTA+'s plugin APIs. For a beginner-friendly introduction, see [Creating Your First Plugin](./README.md).
|
||||
|
||||
## Plugin Interface
|
||||
|
||||
The core `Plugin` interface that all plugins must implement:
|
||||
|
||||
```typescript
|
||||
interface Plugin<T extends PluginSettings = PluginSettings, S = any> {
|
||||
id: string; // Unique identifier for the plugin
|
||||
name: string; // Display name
|
||||
description: string; // Plugin description
|
||||
version: string; // Semantic version (e.g. "1.0.0")
|
||||
settings: T; // Plugin settings (type-safe)
|
||||
styles?: string; // Optional CSS styles
|
||||
disableToggle?: boolean; // Whether to show enable/disable toggle
|
||||
run: (api: PluginAPI<T, S>) => void | Promise<void> | (() => void) | Promise<(() => void)>;
|
||||
}
|
||||
```
|
||||
|
||||
## SEQTA API
|
||||
|
||||
The `SEQTAAPI` interface provides methods for interacting with SEQTA's UI:
|
||||
|
||||
```typescript
|
||||
interface SEQTAAPI {
|
||||
// Wait for an element to appear in the DOM
|
||||
onMount(
|
||||
selector: string, // CSS selector
|
||||
callback: (el: Element) => void
|
||||
): { unregister: () => void };
|
||||
|
||||
// Get React fiber for debugging/advanced usage
|
||||
getFiber(selector: string): ReactFiber;
|
||||
|
||||
// Get current SEQTA page
|
||||
getCurrentPage(): string;
|
||||
|
||||
// Listen for page changes
|
||||
onPageChange(
|
||||
callback: (page: string) => void
|
||||
): { unregister: () => void };
|
||||
}
|
||||
```
|
||||
|
||||
## Settings API
|
||||
|
||||
The settings system provides type-safe plugin configuration:
|
||||
|
||||
```typescript
|
||||
interface SettingsAPI<T extends PluginSettings> {
|
||||
// Access setting values
|
||||
[K in keyof T]: SettingValue<T[K]>;
|
||||
|
||||
// Listen for setting changes
|
||||
onChange<K extends keyof T>(
|
||||
key: K,
|
||||
callback: (value: SettingValue<T[K]>) => void
|
||||
): { unregister: () => void };
|
||||
|
||||
// Remove change listener
|
||||
offChange<K extends keyof T>(
|
||||
key: K,
|
||||
callback: (value: SettingValue<T[K]>) => void
|
||||
): void;
|
||||
|
||||
// Promise that resolves when settings are loaded
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Types
|
||||
|
||||
Available setting types:
|
||||
|
||||
```typescript
|
||||
// Boolean toggle
|
||||
booleanSetting({
|
||||
default: boolean;
|
||||
title: string;
|
||||
description: string;
|
||||
});
|
||||
|
||||
// Text input
|
||||
stringSetting({
|
||||
default: string;
|
||||
title: string;
|
||||
description: string;
|
||||
placeholder?: string;
|
||||
});
|
||||
|
||||
// Number input
|
||||
numberSetting({
|
||||
default: number;
|
||||
title: string;
|
||||
description: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
});
|
||||
|
||||
// Dropdown select
|
||||
selectSetting<T extends string>({
|
||||
default: T;
|
||||
title: string;
|
||||
description: string;
|
||||
options: Array<{
|
||||
value: T;
|
||||
label: string;
|
||||
}>;
|
||||
});
|
||||
```
|
||||
|
||||
### Using Settings
|
||||
|
||||
Two ways to define settings:
|
||||
|
||||
1. Using the BasePlugin class (recommended):
|
||||
```typescript
|
||||
const settings = defineSettings({
|
||||
mySetting: booleanSetting({...})
|
||||
});
|
||||
|
||||
class MyPlugin extends BasePlugin<typeof settings> {
|
||||
@Setting(settings.mySetting)
|
||||
mySetting!: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
2. Direct object (simpler but less type-safe):
|
||||
```typescript
|
||||
const settings = {
|
||||
mySetting: booleanSetting({...})
|
||||
};
|
||||
```
|
||||
|
||||
## Storage API
|
||||
|
||||
Persistent storage for plugin data:
|
||||
|
||||
```typescript
|
||||
interface StorageAPI<T = any> {
|
||||
// Get a stored value
|
||||
get<K extends keyof T>(key: K): Promise<T[K] | undefined>;
|
||||
|
||||
// Set a value
|
||||
set<K extends keyof T>(key: K, value: T[K]): Promise<void>;
|
||||
|
||||
// Delete a value
|
||||
delete<K extends keyof T>(key: K): Promise<void>;
|
||||
|
||||
// Listen for changes
|
||||
onChange<K extends keyof T>(
|
||||
key: K,
|
||||
callback: (value: T[K]) => void
|
||||
): { unregister: () => void };
|
||||
|
||||
// Promise that resolves when storage is loaded
|
||||
loaded: Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
Storage is:
|
||||
- Persistent across page reloads
|
||||
- Isolated per plugin (plugins can't access each other's storage)
|
||||
- Type-safe when using TypeScript
|
||||
- Automatically synchronized across tabs
|
||||
|
||||
## Events API
|
||||
|
||||
Inter-plugin communication system:
|
||||
|
||||
```typescript
|
||||
interface EventsAPI {
|
||||
// Listen for an event
|
||||
on(
|
||||
event: string,
|
||||
callback: (...args: any[]) => void
|
||||
): { unregister: () => void };
|
||||
|
||||
// Emit an event
|
||||
emit(event: string, ...args: any[]): void;
|
||||
}
|
||||
```
|
||||
|
||||
Event naming conventions:
|
||||
- Use `plugin.{pluginId}.{eventName}` for plugin-specific events
|
||||
- Use `seqta.{eventName}` for SEQTA-related events
|
||||
- Use `global.{eventName}` for system-wide events
|
||||
|
||||
## Plugin Lifecycle
|
||||
|
||||
1. **Registration**:
|
||||
```typescript
|
||||
PluginManager.getInstance().registerPlugin(myPlugin);
|
||||
```
|
||||
|
||||
2. **Initialization**:
|
||||
- Plugin's `run` function is called
|
||||
- Settings and storage are loaded
|
||||
- CSS styles are injected (if any)
|
||||
|
||||
3. **Running**:
|
||||
- Plugin can use all APIs
|
||||
- Can listen for events and changes
|
||||
- Can modify SEQTA's UI
|
||||
|
||||
4. **Cleanup**:
|
||||
- When plugin is disabled or unloaded
|
||||
- Cleanup function from `run` is called
|
||||
- CSS styles are removed
|
||||
- Event listeners are cleaned up
|
||||
|
||||
## Type Safety
|
||||
|
||||
TypeScript types for type-safe plugins:
|
||||
|
||||
```typescript
|
||||
// Plugin with settings and storage types
|
||||
interface MyPluginSettings {
|
||||
theme: string;
|
||||
notifications: boolean;
|
||||
}
|
||||
|
||||
interface MyPluginStorage {
|
||||
lastVisit: string;
|
||||
userData: { name: string; id: number };
|
||||
}
|
||||
|
||||
const myPlugin: Plugin<MyPluginSettings, MyPluginStorage> = {
|
||||
// TypeScript will ensure type safety for:
|
||||
// - Settings access and changes
|
||||
// - Storage operations
|
||||
// - Event payloads (when typed)
|
||||
};
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Best practices for plugin error handling:
|
||||
|
||||
```typescript
|
||||
run: async (api) => {
|
||||
try {
|
||||
// Initialization
|
||||
await someAsyncOperation();
|
||||
|
||||
// Return cleanup
|
||||
return () => {
|
||||
try {
|
||||
// Cleanup code
|
||||
} catch (error) {
|
||||
console.error('Plugin cleanup failed:', error);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// Log error but don't crash
|
||||
console.error('Plugin initialization failed:', error);
|
||||
|
||||
// Still return cleanup to ensure proper shutdown
|
||||
return () => {};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **DOM Operations**:
|
||||
- Use `onMount` instead of polling
|
||||
- Batch DOM updates
|
||||
- Use CSS classes instead of inline styles
|
||||
- Remove listeners when not needed
|
||||
|
||||
2. **Storage**:
|
||||
- Cache frequently accessed values
|
||||
- Batch storage operations
|
||||
- Don't store large objects
|
||||
|
||||
3. **Events**:
|
||||
- Clean up listeners
|
||||
- Use typed events
|
||||
- Don't emit events too frequently
|
||||
|
||||
4. **Settings**:
|
||||
- Use appropriate setting types
|
||||
- Provide good defaults
|
||||
- Handle setting changes efficiently
|
||||
@@ -1,269 +0,0 @@
|
||||
# Creating Your First Plugin
|
||||
|
||||
This guide will walk you through the process of creating a plugin for BetterSEQTA+, from setup to implementation to testing.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before you start creating a plugin, make sure you have:
|
||||
|
||||
- Basic knowledge of TypeScript
|
||||
- Familiarity with the BetterSEQTA+ codebase
|
||||
- A development environment set up according to the [Installation Guide](../installation.md)
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
A typical BetterSEQTA+ plugin consists of:
|
||||
|
||||
1. **Plugin Definition**: A TypeScript file that defines the plugin's metadata and functionality
|
||||
2. **Settings Interface**: (Optional) A TypeScript interface that defines the plugin's settings
|
||||
3. **Storage Interface**: (Optional) A TypeScript interface that defines the plugin's storage structure
|
||||
|
||||
## Step 1: Planning Your Plugin
|
||||
|
||||
Before you start coding, take some time to plan your plugin:
|
||||
|
||||
1. **Identify the Problem**: What issue or need does your plugin address?
|
||||
2. **Define the Scope**: What specific features will your plugin include?
|
||||
3. **Consider the User Experience**: How will users interact with your plugin?
|
||||
|
||||
## Step 2: Creating the Plugin File
|
||||
|
||||
Create a new TypeScript file for your plugin. The convention is to place it in the `src/plugins/` directory, either in the `built-in` folder or a new folder if it's a third-party plugin.
|
||||
|
||||
```typescript
|
||||
// src/plugins/my-plugin/index.ts
|
||||
|
||||
import { Plugin, PluginAPI, PluginSettings } from '../../core/types';
|
||||
|
||||
export interface MyPluginSettings extends PluginSettings {
|
||||
enabled: {
|
||||
type: 'boolean';
|
||||
default: true;
|
||||
title: 'Enable My Plugin';
|
||||
description: 'Turn my plugin on or off';
|
||||
};
|
||||
// Add more settings as needed
|
||||
}
|
||||
|
||||
export interface MyPluginStorage {
|
||||
lastRun: string;
|
||||
// Add more storage fields as needed
|
||||
}
|
||||
|
||||
const myPlugin: Plugin<MyPluginSettings, MyPluginStorage> = {
|
||||
id: 'my-plugin',
|
||||
name: 'My Plugin',
|
||||
description: 'A simple plugin for BetterSEQTA+',
|
||||
version: '1.0.0',
|
||||
settings: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
title: 'Enable My Plugin',
|
||||
description: 'Turn my plugin on or off',
|
||||
},
|
||||
// Initialize your settings here
|
||||
},
|
||||
run: (api) => {
|
||||
if (!api.settings.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize storage with default values if needed
|
||||
if (api.storage.lastRun === undefined) {
|
||||
api.storage.lastRun = new Date().toISOString();
|
||||
}
|
||||
|
||||
// Your plugin logic goes here
|
||||
console.log('My Plugin is running!');
|
||||
|
||||
// Access the SEQTA API
|
||||
api.seqta.onPageLoad('/timetable', () => {
|
||||
// Code to run when the timetable page loads
|
||||
});
|
||||
|
||||
// Return a cleanup function (optional but recommended)
|
||||
return () => {
|
||||
console.log('My Plugin is cleaning up!');
|
||||
// Cleanup logic goes here
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default myPlugin;
|
||||
```
|
||||
|
||||
## Step 3: Registering Your Plugin
|
||||
|
||||
To make your plugin available to BetterSEQTA+, you need to register it with the Plugin Manager. For built-in plugins, you can add your plugin to the `src/plugins/built-in/index.ts` file:
|
||||
|
||||
```typescript
|
||||
// src/plugins/built-in/index.ts
|
||||
|
||||
import myPlugin from './my-plugin';
|
||||
// Other imports...
|
||||
|
||||
export const builtInPlugins = [
|
||||
myPlugin,
|
||||
// Other plugins...
|
||||
];
|
||||
```
|
||||
|
||||
For third-party plugins, you'll need to follow a different approach, as detailed in [Third-Party Plugins](../advanced/third-party-plugins.md).
|
||||
|
||||
## Step 4: Implementing Your Plugin Logic
|
||||
|
||||
The main functionality of your plugin goes in the `run` method. Here are some common patterns:
|
||||
|
||||
### Responding to Page Loads
|
||||
|
||||
```typescript
|
||||
api.seqta.onPageLoad('/timetable', () => {
|
||||
// Code to run when the timetable page loads
|
||||
});
|
||||
```
|
||||
|
||||
### Modifying the UI
|
||||
|
||||
```typescript
|
||||
api.seqta.onPageLoad('/timetable', () => {
|
||||
const timetableElement = document.querySelector('.timetable');
|
||||
if (timetableElement) {
|
||||
// Modify the timetable element
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.className = 'my-plugin-controls';
|
||||
controlsDiv.innerHTML = '<button>Zoom In</button><button>Zoom Out</button>';
|
||||
timetableElement.appendChild(controlsDiv);
|
||||
|
||||
// Add event listeners
|
||||
controlsDiv.querySelector('button:first-child').addEventListener('click', () => {
|
||||
// Zoom in logic
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Working with Settings
|
||||
|
||||
```typescript
|
||||
// Get a setting value
|
||||
const isEnabled = api.settings.enabled;
|
||||
|
||||
// Listen for settings changes
|
||||
api.settings.onChange('enabled', (newValue) => {
|
||||
if (newValue) {
|
||||
// Enable functionality
|
||||
} else {
|
||||
// Disable functionality
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Working with Storage
|
||||
|
||||
```typescript
|
||||
// Get a stored value
|
||||
const lastRun = api.storage.lastRun;
|
||||
|
||||
// Set a stored value
|
||||
api.storage.lastRun = new Date().toISOString();
|
||||
|
||||
// Listen for storage changes
|
||||
api.storage.onChange('lastRun', (newValue) => {
|
||||
console.log(`Last run updated to: ${newValue}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Working with Events
|
||||
|
||||
```typescript
|
||||
// Listen for events
|
||||
api.events.on('assessmentLoaded', (data) => {
|
||||
console.log(`Assessment loaded: ${data.id}`);
|
||||
});
|
||||
|
||||
// Emit an event
|
||||
api.events.emit('myPluginEvent', { message: 'Hello from My Plugin!' });
|
||||
```
|
||||
|
||||
## Step 5: Testing Your Plugin
|
||||
|
||||
To test your plugin:
|
||||
|
||||
1. Run the development server:
|
||||
```
|
||||
npm run dev
|
||||
```
|
||||
|
||||
2. Open SEQTA Learn in your browser with BetterSEQTA+ enabled.
|
||||
|
||||
3. Check the console for any error messages.
|
||||
|
||||
4. Verify that your plugin works as expected.
|
||||
|
||||
## Step 6: Adding Plugin Settings UI
|
||||
|
||||
If your plugin has settings, they will automatically appear in the BetterSEQTA+ settings panel. The UI is generated based on the settings interface you defined.
|
||||
|
||||
For more control over the settings UI, you can use the decorator-based settings system. See [Creating Plugins with Settings](../settings/creating-plugins.md) for more information.
|
||||
|
||||
## Best Practices for Plugin Development
|
||||
|
||||
1. **Follow TypeScript Best Practices**: Use proper typing for all variables and functions.
|
||||
|
||||
2. **Handle Errors Gracefully**: Wrap your code in try-catch blocks to prevent crashes.
|
||||
```typescript
|
||||
try {
|
||||
// Your code
|
||||
} catch (error) {
|
||||
console.error('My Plugin Error:', error);
|
||||
}
|
||||
```
|
||||
|
||||
3. **Clean Up After Yourself**: Always return a cleanup function from your plugin's `run` method.
|
||||
```typescript
|
||||
const cleanup = () => {
|
||||
// Remove event listeners, DOM elements, etc.
|
||||
};
|
||||
return cleanup;
|
||||
```
|
||||
|
||||
4. **Document Your Code**: Add comments to explain complex logic or unusual patterns.
|
||||
|
||||
5. **Keep It Simple**: Start with a simple plugin and add features incrementally.
|
||||
|
||||
## Example Plugins
|
||||
|
||||
For inspiration, check out these example plugins in the BetterSEQTA+ codebase:
|
||||
|
||||
1. **Timetable Plugin**: Enhances the SEQTA timetable view with zoom controls and filtering options.
|
||||
- Location: `src/plugins/built-in/timetable/index.ts`
|
||||
|
||||
2. **Notification Collector**: Improves the notification system in SEQTA Learn.
|
||||
- Location: `src/plugins/built-in/notification-collector/index.ts`
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin Not Loading
|
||||
|
||||
- Check that your plugin is properly registered
|
||||
- Verify that there are no TypeScript errors
|
||||
- Look for error messages in the console
|
||||
|
||||
### Plugin Not Working as Expected
|
||||
|
||||
- Ensure that your plugin's `enabled` setting is true
|
||||
- Check that your selectors match the SEQTA DOM structure
|
||||
- Use `console.log` statements to debug your code
|
||||
|
||||
### TypeScript Errors
|
||||
|
||||
- Make sure your interfaces are properly defined
|
||||
- Check that you're using the correct types for the plugin API
|
||||
- Verify that your plugin implements the `Plugin` interface correctly
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Learn About Type-Safe Settings](../settings/creating-plugins.md)
|
||||
- [Explore the Plugin API](../advanced/plugin-api.md)
|
||||
- [Contribute to BetterSEQTA+](../contributing.md)
|
||||
@@ -1,301 +0,0 @@
|
||||
# BetterSEQTA+ Settings System
|
||||
|
||||
BetterSEQTA+ includes a powerful, type-safe settings system that uses TypeScript decorators to create a seamless API for plugin developers. This document explains how the settings system works and how to extend it.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Overview](#overview)
|
||||
- [Existing Setting Types](#existing-setting-types)
|
||||
- [Using Settings in Plugins](#using-settings-in-plugins)
|
||||
- [Adding New Setting Types](#adding-new-setting-types)
|
||||
- [Rendering in the UI](#rendering-in-the-ui)
|
||||
|
||||
## Overview
|
||||
|
||||
The settings system is built around TypeScript decorators and uses TypeScript's type system to provide type safety for plugin settings. The system consists of a few key components:
|
||||
|
||||
1. **Setting Type Interfaces** in `src/plugins/core/types.ts` - Define the structure of the setting
|
||||
2. **Setting Decorator Options** in `src/plugins/core/settings.ts` - Define the options for the decorator
|
||||
3. **Setting Decorators** in `src/plugins/core/settings.ts` - Register the setting in the plugin
|
||||
4. **BasePlugin Class** in `src/plugins/core/settings.ts` - Base class that handles the settings
|
||||
|
||||
## Existing Setting Types
|
||||
|
||||
BetterSEQTA+ currently supports the following setting types:
|
||||
|
||||
- **Boolean Settings** - Simple on/off toggle
|
||||
- **String Settings** - Text input with optional validation
|
||||
- **Number Settings** - Numeric input with optional min/max/step
|
||||
- **Select Settings** - Dropdown selection from predefined options
|
||||
|
||||
Each setting type has a corresponding interface, options interface, and decorator.
|
||||
|
||||
## Using Settings in Plugins
|
||||
|
||||
Here's how to use the settings system in a plugin:
|
||||
|
||||
```typescript
|
||||
import { BasePlugin, BooleanSetting, StringSetting } from '../../core/settings';
|
||||
|
||||
// Define the plugin settings class
|
||||
class MyPluginClass extends BasePlugin {
|
||||
@BooleanSetting({
|
||||
default: true,
|
||||
title: "Enable Feature",
|
||||
description: "Enables the awesome feature."
|
||||
})
|
||||
enabled!: boolean;
|
||||
|
||||
@StringSetting({
|
||||
default: "Default Value",
|
||||
title: "Custom Text",
|
||||
description: "Enter your custom text here.",
|
||||
maxLength: 100
|
||||
})
|
||||
customText!: string;
|
||||
}
|
||||
|
||||
// Create an instance to extract settings
|
||||
const settingsInstance = new MyPluginClass();
|
||||
|
||||
// Use in plugin definition
|
||||
const myPlugin = {
|
||||
id: 'my-plugin',
|
||||
name: 'My Plugin',
|
||||
description: 'Does awesome things',
|
||||
version: '1.0.0',
|
||||
settings: settingsInstance.settings,
|
||||
run: async (api) => {
|
||||
// Access settings via api.settings
|
||||
if (api.settings.enabled) {
|
||||
console.log(api.settings.customText);
|
||||
}
|
||||
|
||||
// Listen for settings changes
|
||||
api.settings.onChange('enabled', (value) => {
|
||||
console.log(`Enabled changed to: ${value}`);
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Adding New Setting Types
|
||||
|
||||
To add a new setting type, you need to follow these steps:
|
||||
|
||||
### 1. Define the Setting Interface in `src/plugins/core/types.ts`
|
||||
|
||||
```typescript
|
||||
export interface ColorSetting {
|
||||
type: 'color';
|
||||
default: string; // HEX color code
|
||||
title: string;
|
||||
description?: string;
|
||||
presets?: string[]; // Optional color presets
|
||||
}
|
||||
|
||||
// Update the PluginSetting type to include the new setting type
|
||||
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting |
|
||||
SelectSetting<string> | ColorSetting;
|
||||
|
||||
// Update the SettingValue type helper
|
||||
type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
|
||||
T extends StringSetting ? string :
|
||||
T extends NumberSetting ? number :
|
||||
T extends SelectSetting<infer O> ? O :
|
||||
T extends ColorSetting ? string : // Add this line
|
||||
never;
|
||||
```
|
||||
|
||||
### 2. Define the Options Interface in `src/plugins/core/settings.ts`
|
||||
|
||||
```typescript
|
||||
interface ColorSettingOptions extends BaseSettingOptions {
|
||||
default: string; // HEX color
|
||||
presets?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create the Decorator Function in `src/plugins/core/settings.ts`
|
||||
|
||||
```typescript
|
||||
export function ColorSetting(options: ColorSettingOptions): PropertyDecorator {
|
||||
return (target: Object, propertyKey: string | symbol) => {
|
||||
// Ensure the settings property exists on the constructor's prototype
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
// Add the setting to the prototype's settings object
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'color',
|
||||
...options
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create a Corresponding UI Component (if needed)
|
||||
|
||||
If your setting type needs a custom UI component, create one in the `src/interface/components` directory.
|
||||
|
||||
For example, you might create a `ColorPicker.svelte` component.
|
||||
|
||||
### 5. Update the Settings UI in `src/interface/pages/settings/general.svelte`
|
||||
|
||||
Update the `getPluginSettingEntries` function to handle your new setting type:
|
||||
|
||||
```javascript
|
||||
entries.push({
|
||||
title: setting.title || key,
|
||||
description: setting.description || '',
|
||||
id,
|
||||
Component: setting.type === 'boolean' ? Switch :
|
||||
setting.type === 'select' ? Select :
|
||||
setting.type === 'number' ? Slider :
|
||||
setting.type === 'color' ? ColorPicker : // Add this line
|
||||
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
|
||||
props: {
|
||||
state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default,
|
||||
onChange: (value: any) => {
|
||||
updatePluginSetting(plugin.pluginId, key, value);
|
||||
},
|
||||
options: setting.options,
|
||||
presets: setting.presets // Add this line if needed for your component
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Rendering in the UI
|
||||
|
||||
The settings UI is handled in `src/interface/pages/settings/general.svelte`. This file does a few key things:
|
||||
|
||||
1. Loads settings for all plugins from storage
|
||||
2. Maps setting types to UI components
|
||||
3. Handles updating settings when users interact with the UI
|
||||
|
||||
For most setting types, you'll need to ensure there's a corresponding Svelte component in the `src/interface/components` directory that can render and edit the setting value.
|
||||
|
||||
## Example: Adding a Color Setting
|
||||
|
||||
Here's a complete example of adding a color setting type:
|
||||
|
||||
1. Define the setting interface in `types.ts`:
|
||||
|
||||
```typescript
|
||||
export interface ColorSetting {
|
||||
type: 'color';
|
||||
default: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
presets?: string[];
|
||||
}
|
||||
|
||||
export type PluginSetting = BooleanSetting | StringSetting | NumberSetting |
|
||||
SelectSetting<string> | ColorSetting;
|
||||
|
||||
type SettingValue<T extends PluginSetting> = T extends BooleanSetting ? boolean :
|
||||
T extends StringSetting ? string :
|
||||
T extends NumberSetting ? number :
|
||||
T extends SelectSetting<infer O> ? O :
|
||||
T extends ColorSetting ? string :
|
||||
never;
|
||||
```
|
||||
|
||||
2. Create the options interface and decorator in `settings.ts`:
|
||||
|
||||
```typescript
|
||||
interface ColorSettingOptions extends BaseSettingOptions {
|
||||
default: string;
|
||||
presets?: string[];
|
||||
}
|
||||
|
||||
export function ColorSetting(options: ColorSettingOptions): PropertyDecorator {
|
||||
return (target: Object, propertyKey: string | symbol) => {
|
||||
const proto = target.constructor.prototype;
|
||||
if (!proto.hasOwnProperty('settings')) {
|
||||
proto.settings = {};
|
||||
}
|
||||
|
||||
proto.settings[propertyKey] = {
|
||||
type: 'color',
|
||||
...options
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
3. Create a ColorPicker component in `src/interface/components/ColorPicker.svelte`:
|
||||
|
||||
```html
|
||||
<script lang="ts">
|
||||
export let state = "#000000";
|
||||
export let onChange = (value: string) => {};
|
||||
export let presets: string[] = ["#ff0000", "#00ff00", "#0000ff"];
|
||||
</script>
|
||||
|
||||
<div class="color-picker">
|
||||
<input
|
||||
type="color"
|
||||
value={state}
|
||||
on:change={(e) => onChange(e.currentTarget.value)}
|
||||
/>
|
||||
<div class="presets">
|
||||
{#each presets as preset}
|
||||
<button
|
||||
class="preset"
|
||||
style="background-color: {preset}"
|
||||
on:click={() => onChange(preset)}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.color-picker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.preset {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
4. Update the UI renderer in `general.svelte`:
|
||||
|
||||
```javascript
|
||||
Component: setting.type === 'boolean' ? Switch :
|
||||
setting.type === 'select' ? Select :
|
||||
setting.type === 'number' ? Slider :
|
||||
setting.type === 'color' ? ColorPicker :
|
||||
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
|
||||
```
|
||||
|
||||
5. Use the new setting type in a plugin:
|
||||
|
||||
```typescript
|
||||
class ThemePlugin extends BasePlugin {
|
||||
@ColorSetting({
|
||||
default: "#4285f4",
|
||||
title: "Primary Color",
|
||||
description: "The main color for the theme",
|
||||
presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"]
|
||||
})
|
||||
primaryColor!: string;
|
||||
}
|
||||
```
|
||||
|
||||
With these steps, you've added a completely new setting type to the BetterSEQTA+ plugin system!
|
||||
@@ -1,335 +0,0 @@
|
||||
# Creating Plugins with Decorator-Based Settings
|
||||
|
||||
This guide will walk you through creating a BetterSEQTA+ plugin using the new decorator-based settings system.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Understand basic TypeScript concepts (classes, interfaces, decorators)
|
||||
- Familiarity with the BetterSEQTA+ plugin system
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
A typical plugin consists of:
|
||||
|
||||
1. A settings class that defines the plugin's settings using decorators
|
||||
2. The plugin definition object
|
||||
3. The actual plugin functionality
|
||||
|
||||
## Step by Step Guide
|
||||
|
||||
### 1. Create a Plugin File
|
||||
|
||||
Start by creating a new file in the `src/plugins/built-in` directory. For example, `myFeature/index.ts`.
|
||||
|
||||
### 2. Define Storage Type (Optional)
|
||||
|
||||
If your plugin needs to store data, define a storage interface:
|
||||
|
||||
```typescript
|
||||
interface MyFeatureStorage {
|
||||
lastUsed: string;
|
||||
favoriteItems: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Create a Settings Class
|
||||
|
||||
Create a class that extends `BasePlugin` and use decorators to define settings:
|
||||
|
||||
```typescript
|
||||
import { BasePlugin, BooleanSetting, StringSetting, NumberSetting, SelectSetting } from '../../core/settings';
|
||||
|
||||
class MyFeaturePluginClass extends BasePlugin {
|
||||
@BooleanSetting({
|
||||
default: true,
|
||||
title: "Enable My Feature",
|
||||
description: "Enables the awesome new feature."
|
||||
})
|
||||
enabled!: boolean;
|
||||
|
||||
@StringSetting({
|
||||
default: "Default text",
|
||||
title: "Custom Message",
|
||||
description: "Sets a custom message for the feature",
|
||||
maxLength: 100
|
||||
})
|
||||
message!: string;
|
||||
|
||||
@NumberSetting({
|
||||
default: 5,
|
||||
title: "Refresh Interval",
|
||||
description: "How often to refresh the data (in seconds)",
|
||||
min: 1,
|
||||
max: 60,
|
||||
step: 1
|
||||
})
|
||||
refreshInterval!: number;
|
||||
|
||||
@SelectSetting({
|
||||
default: "small",
|
||||
options: ["small", "medium", "large"] as const,
|
||||
title: "Display Size",
|
||||
description: "Control how large the feature appears"
|
||||
})
|
||||
displaySize!: "small" | "medium" | "large";
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Create a Plugin Instance
|
||||
|
||||
Create an instance of your settings class and define the plugin object:
|
||||
|
||||
```typescript
|
||||
// Create an instance to extract settings
|
||||
const settingsInstance = new MyFeaturePluginClass();
|
||||
|
||||
const myFeaturePlugin: Plugin<typeof settingsInstance.settings, MyFeatureStorage> = {
|
||||
id: 'myFeature',
|
||||
name: 'My Awesome Feature',
|
||||
description: 'Adds an awesome new feature to SEQTA',
|
||||
version: '1.0.0',
|
||||
settings: settingsInstance.settings,
|
||||
run: async (api) => {
|
||||
// Plugin implementation goes here
|
||||
}
|
||||
};
|
||||
|
||||
export default myFeaturePlugin;
|
||||
```
|
||||
|
||||
### 5. Implement Plugin Functionality
|
||||
|
||||
Implement your plugin's functionality in the `run` function:
|
||||
|
||||
```typescript
|
||||
run: async (api) => {
|
||||
// Initialize storage with defaults if needed
|
||||
if (api.storage.lastUsed === undefined) {
|
||||
api.storage.lastUsed = new Date().toISOString();
|
||||
}
|
||||
|
||||
if (api.storage.favoriteItems === undefined) {
|
||||
api.storage.favoriteItems = [];
|
||||
}
|
||||
|
||||
// Only run if enabled
|
||||
if (!api.settings.enabled) return;
|
||||
|
||||
// Main plugin logic
|
||||
const initializeFeature = () => {
|
||||
console.log(`Initializing feature with message: ${api.settings.message}`);
|
||||
console.log(`Using display size: ${api.settings.displaySize}`);
|
||||
|
||||
// Set up refreshing
|
||||
const intervalId = setInterval(() => {
|
||||
refreshData();
|
||||
}, api.settings.refreshInterval * 1000);
|
||||
|
||||
// Clean up function returned here
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
console.log('Feature cleaned up');
|
||||
};
|
||||
};
|
||||
|
||||
const refreshData = () => {
|
||||
console.log('Refreshing data...');
|
||||
api.storage.lastUsed = new Date().toISOString();
|
||||
};
|
||||
|
||||
// Listen for elements we need
|
||||
api.seqta.onMount('.some-element', (element) => {
|
||||
// Do something when element appears
|
||||
});
|
||||
|
||||
// Listen for settings changes
|
||||
api.settings.onChange('refreshInterval', (newValue) => {
|
||||
console.log(`Refresh interval changed to ${newValue} seconds`);
|
||||
});
|
||||
|
||||
// Return cleanup function
|
||||
return initializeFeature();
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Register the Plugin
|
||||
|
||||
Make sure your plugin is registered in the plugin system. In the `src/plugins/index.ts` file, add your plugin to the list of built-in plugins:
|
||||
|
||||
```typescript
|
||||
import myFeaturePlugin from './built-in/myFeature';
|
||||
|
||||
// Add your plugin to this array
|
||||
const builtInPlugins = [
|
||||
// ... other plugins
|
||||
myFeaturePlugin,
|
||||
];
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
|
||||
### Reacting to Settings Changes
|
||||
|
||||
You can listen for settings changes with the `onChange` method:
|
||||
|
||||
```typescript
|
||||
api.settings.onChange('enabled', (value) => {
|
||||
if (value) {
|
||||
// Setting was turned on
|
||||
initialize();
|
||||
} else {
|
||||
// Setting was turned off
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Using Storage
|
||||
|
||||
The storage API lets you persist data between sessions:
|
||||
|
||||
```typescript
|
||||
// Read from storage
|
||||
const favorites = api.storage.favoriteItems;
|
||||
|
||||
// Write to storage
|
||||
api.storage.favoriteItems = [...favorites, 'new item'];
|
||||
|
||||
// Listen for storage changes
|
||||
api.storage.onChange('favoriteItems', (newValue) => {
|
||||
console.log('Favorites updated:', newValue);
|
||||
});
|
||||
```
|
||||
|
||||
### Cleaning Up
|
||||
|
||||
Always return a cleanup function from your plugin's `run` method if you have any resources to clean up:
|
||||
|
||||
```typescript
|
||||
run: async (api) => {
|
||||
// Set up resources
|
||||
const intervalId = setInterval(() => {
|
||||
// Do something
|
||||
}, 1000);
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
// Clean up any other resources
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Initialize Storage Values**: Always check if storage values are undefined and set defaults
|
||||
2. **Handle Enabled State**: Check if your plugin is enabled before running main functionality
|
||||
3. **Use TypeScript**: Take advantage of TypeScript's type system to ensure type safety
|
||||
4. **Clean Up Resources**: Always clean up resources when a plugin is disabled
|
||||
5. **Document Settings**: Use clear titles and descriptions for your settings
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example of a simple plugin that changes the color of elements:
|
||||
|
||||
```typescript
|
||||
import { BasePlugin, BooleanSetting, ColorSetting } from '../../core/settings';
|
||||
import type { Plugin } from '../../core/types';
|
||||
|
||||
interface ColorChangerStorage {
|
||||
lastApplied: string;
|
||||
}
|
||||
|
||||
class ColorChangerPluginClass extends BasePlugin {
|
||||
@BooleanSetting({
|
||||
default: true,
|
||||
title: "Enable Color Changer",
|
||||
description: "Applies custom colors to elements on the page."
|
||||
})
|
||||
enabled!: boolean;
|
||||
|
||||
@ColorSetting({
|
||||
default: "#4285f4",
|
||||
title: "Heading Color",
|
||||
description: "Color for headings on the page",
|
||||
presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"]
|
||||
})
|
||||
headingColor!: string;
|
||||
|
||||
@ColorSetting({
|
||||
default: "#34a853",
|
||||
title: "Button Color",
|
||||
description: "Color for buttons on the page",
|
||||
presets: ["#4285f4", "#ea4335", "#fbbc05", "#34a853"]
|
||||
})
|
||||
buttonColor!: string;
|
||||
}
|
||||
|
||||
const settingsInstance = new ColorChangerPluginClass();
|
||||
|
||||
const colorChangerPlugin: Plugin<typeof settingsInstance.settings, ColorChangerStorage> = {
|
||||
id: 'colorChanger',
|
||||
name: 'Color Changer',
|
||||
description: 'Changes colors of various elements on the page',
|
||||
version: '1.0.0',
|
||||
settings: settingsInstance.settings,
|
||||
run: async (api) => {
|
||||
if (api.storage.lastApplied === undefined) {
|
||||
api.storage.lastApplied = new Date().toISOString();
|
||||
}
|
||||
|
||||
const applyColors = () => {
|
||||
if (!api.settings.enabled) return;
|
||||
|
||||
// Apply heading color
|
||||
document.querySelectorAll('h1, h2, h3').forEach(heading => {
|
||||
(heading as HTMLElement).style.color = api.settings.headingColor;
|
||||
});
|
||||
|
||||
// Apply button color
|
||||
document.querySelectorAll('button').forEach(button => {
|
||||
(button as HTMLElement).style.backgroundColor = api.settings.buttonColor;
|
||||
});
|
||||
|
||||
api.storage.lastApplied = new Date().toISOString();
|
||||
};
|
||||
|
||||
// Apply colors initially
|
||||
applyColors();
|
||||
|
||||
// Apply colors when DOM changes
|
||||
api.seqta.onMount('h1, h2, h3, button', applyColors);
|
||||
|
||||
// Listen for color changes
|
||||
api.settings.onChange('headingColor', applyColors);
|
||||
api.settings.onChange('buttonColor', applyColors);
|
||||
api.settings.onChange('enabled', (enabled) => {
|
||||
if (enabled) {
|
||||
applyColors();
|
||||
} else {
|
||||
// Reset colors
|
||||
document.querySelectorAll('h1, h2, h3').forEach(heading => {
|
||||
(heading as HTMLElement).style.color = '';
|
||||
});
|
||||
|
||||
document.querySelectorAll('button').forEach(button => {
|
||||
(button as HTMLElement).style.backgroundColor = '';
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// No cleanup needed for this plugin
|
||||
return () => {};
|
||||
}
|
||||
};
|
||||
|
||||
export default colorChangerPlugin;
|
||||
```
|
||||
|
||||
This plugin demonstrates:
|
||||
- Using multiple setting types including a custom color setting
|
||||
- Handling the enabled state
|
||||
- Initializing storage
|
||||
- Listening for setting changes
|
||||
- Applying and resetting styles based on settings
|
||||
- Proper cleanup when disabled
|
||||
@@ -1,541 +0,0 @@
|
||||
# Creating Custom UI Components for Settings
|
||||
|
||||
When adding new setting types to BetterSEQTA+, you'll often need to create custom UI components to render and edit these settings. This guide covers how to create Svelte components for the settings UI and how to integrate them with the settings system.
|
||||
|
||||
## Understanding the Settings UI
|
||||
|
||||
Settings in BetterSEQTA+ are rendered by the `src/interface/pages/settings/general.svelte` component. This component:
|
||||
|
||||
1. Loads settings from all plugins
|
||||
2. Maps setting types to appropriate UI components
|
||||
3. Renders the settings UI
|
||||
4. Handles updates when settings are changed
|
||||
|
||||
## Basic Component Requirements
|
||||
|
||||
Every setting UI component should follow these conventions:
|
||||
|
||||
1. **Accept a `state` prop** for the current value
|
||||
2. **Accept an `onChange` prop** for updating the value
|
||||
3. **Accept any additional props** specific to the setting type (e.g., `options`, `min`, `max`)
|
||||
4. **Handle user input** and call `onChange` with the new value
|
||||
|
||||
## Creating a Basic Component
|
||||
|
||||
Here's an example of a basic Svelte component for a custom setting type:
|
||||
|
||||
```svelte
|
||||
<!-- src/interface/components/MyCustomSetting.svelte -->
|
||||
<script lang="ts">
|
||||
// Current value
|
||||
export let state: any = null;
|
||||
|
||||
// Callback for updates
|
||||
export let onChange = (newValue: any) => {};
|
||||
|
||||
// Other props specific to your setting type
|
||||
export let customOption: string = "default";
|
||||
|
||||
// Local state or methods if needed
|
||||
function handleChange(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
onChange(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="my-custom-setting">
|
||||
<input
|
||||
type="text"
|
||||
value={state}
|
||||
on:input={handleChange}
|
||||
data-option={customOption}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.my-custom-setting {
|
||||
/* Your component styles */
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Example: Slider Component
|
||||
|
||||
BetterSEQTA+ includes a Slider component for number settings:
|
||||
|
||||
```svelte
|
||||
<!-- src/interface/components/Slider.svelte -->
|
||||
<script lang="ts">
|
||||
export let state: number | string = 0;
|
||||
export let onChange = (value: number) => {};
|
||||
export let min = 0;
|
||||
export let max = 100;
|
||||
export let step = 1;
|
||||
|
||||
let stringValue = typeof state === "string" ? state : state.toString();
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const newValue = parseFloat(input.value);
|
||||
stringValue = input.value;
|
||||
onChange(newValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative flex items-center">
|
||||
<input
|
||||
type="range"
|
||||
class="w-24 accent-indigo-500"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={state}
|
||||
on:input={handleChange}
|
||||
/>
|
||||
<span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400 w-8">{stringValue}</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Example: Color Picker Component
|
||||
|
||||
Here's a more complex example of a color picker component:
|
||||
|
||||
```svelte
|
||||
<!-- src/interface/components/ColorPicker.svelte -->
|
||||
<script lang="ts">
|
||||
export let state = "#000000";
|
||||
export let onChange = (value: string) => {};
|
||||
export let presets: string[] = ["#ff0000", "#00ff00", "#0000ff"];
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
function handleColorChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
onChange(input.value);
|
||||
}
|
||||
|
||||
function selectPreset(color: string) {
|
||||
onChange(color);
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function togglePicker() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="color-picker relative">
|
||||
<button
|
||||
class="color-swatch"
|
||||
style="background-color: {state}"
|
||||
on:click={togglePicker}
|
||||
aria-label="Open color picker"
|
||||
></button>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="picker-popup">
|
||||
<input
|
||||
type="color"
|
||||
value={state}
|
||||
on:input={handleColorChange}
|
||||
/>
|
||||
|
||||
<div class="presets">
|
||||
{#each presets as preset}
|
||||
<button
|
||||
class="preset-swatch"
|
||||
style="background-color: {preset}"
|
||||
on:click={() => selectPreset(preset)}
|
||||
aria-label={`Select color ${preset}`}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.color-picker {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.color-swatch {
|
||||
width: 2rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.picker-popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.presets {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.preset-swatch {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #ccc;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Integrating with the Settings System
|
||||
|
||||
Once you've created your component, you need to update `general.svelte` to use it for your custom setting type.
|
||||
|
||||
### 1. Import Your Component
|
||||
|
||||
At the top of `src/interface/pages/settings/general.svelte`, add an import for your component:
|
||||
|
||||
```typescript
|
||||
import ColorPicker from "../../components/ColorPicker.svelte"
|
||||
```
|
||||
|
||||
### 2. Update Component Mapping
|
||||
|
||||
Find the `getPluginSettingEntries` function in `general.svelte` and update the component mapping:
|
||||
|
||||
```typescript
|
||||
function getPluginSettingEntries() {
|
||||
const entries: any[] = [];
|
||||
|
||||
pluginSettings.forEach(plugin => {
|
||||
if (Object.keys(plugin.settings).length === 0) return;
|
||||
|
||||
Object.entries(plugin.settings).forEach(([key, setting]) => {
|
||||
const id = getPluginSettingId(plugin.pluginId, key);
|
||||
|
||||
entries.push({
|
||||
title: setting.title || key,
|
||||
description: setting.description || '',
|
||||
id,
|
||||
Component: setting.type === 'boolean' ? Switch :
|
||||
setting.type === 'select' ? Select :
|
||||
setting.type === 'number' ? Slider :
|
||||
setting.type === 'color' ? ColorPicker : // Add your component here
|
||||
setting.type === 'string' ? (setting.options ? Select : null) : Switch,
|
||||
props: {
|
||||
state: pluginSettingsValues[plugin.pluginId]?.[key] ?? setting.default,
|
||||
onChange: (value: any) => {
|
||||
updatePluginSetting(plugin.pluginId, key, value);
|
||||
},
|
||||
options: setting.options,
|
||||
// Add any additional props your component needs
|
||||
presets: setting.presets,
|
||||
min: setting.min,
|
||||
max: setting.max,
|
||||
step: setting.step
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
```
|
||||
|
||||
## Handling Different UI Needs
|
||||
|
||||
Different setting types may have different UI needs:
|
||||
|
||||
### Toggle Switches
|
||||
|
||||
For boolean settings, a toggle switch is usually appropriate:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
export let state = false;
|
||||
export let onChange = (value: boolean) => {};
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="switch"
|
||||
class:active={state}
|
||||
on:click={() => onChange(!state)}
|
||||
>
|
||||
<div class="toggle"></div>
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.switch {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 24px;
|
||||
background-color: #ccc;
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch.active {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.toggle {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.switch.active .toggle {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Text Inputs
|
||||
|
||||
For string settings, a text input with validation:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
export let state = "";
|
||||
export let onChange = (value: string) => {};
|
||||
export let maxLength: number | undefined = undefined;
|
||||
export let pattern: string | undefined = undefined;
|
||||
|
||||
let error = "";
|
||||
|
||||
function validate(value: string) {
|
||||
if (maxLength && value.length > maxLength) {
|
||||
error = `Value must be under ${maxLength} characters`;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pattern && !new RegExp(pattern).test(value)) {
|
||||
error = "Value doesn't match the required pattern";
|
||||
return false;
|
||||
}
|
||||
|
||||
error = "";
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleInput(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const newValue = input.value;
|
||||
|
||||
if (validate(newValue)) {
|
||||
onChange(newValue);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="text-input">
|
||||
<input
|
||||
type="text"
|
||||
value={state}
|
||||
on:input={handleInput}
|
||||
maxlength={maxLength}
|
||||
pattern={pattern}
|
||||
/>
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.text-input {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
input {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
### Complex Interactive Components
|
||||
|
||||
For more complex settings, you may need more interactive components with dropdowns, modals, or other features. Consider using additional Svelte features like:
|
||||
|
||||
- `{#if}...{/if}` blocks for conditional rendering
|
||||
- Svelte transitions for animations
|
||||
- Svelte actions for DOM interactions
|
||||
- Svelte stores for shared state
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Keep Components Focused**: Each component should do one thing well
|
||||
2. **Use TypeScript**: Define proper types for your props
|
||||
3. **Handle Errors**: Validate input and show meaningful error messages
|
||||
4. **Use Clear UI**: Make it obvious how to interact with the component
|
||||
5. **Add Accessibility**: Include proper ARIA attributes and keyboard handling
|
||||
6. **Support Theming**: Use CSS variables or design system tokens for consistent styling
|
||||
7. **Test Edge Cases**: Ensure your component handles all possible inputs
|
||||
|
||||
## Complete Example
|
||||
|
||||
Here's a complete example of a custom file picker component:
|
||||
|
||||
```svelte
|
||||
<!-- src/interface/components/FilePicker.svelte -->
|
||||
<script lang="ts">
|
||||
export let state: string | null = null;
|
||||
export let onChange = (value: string | null) => {};
|
||||
export let accept = ".txt,.pdf,.doc,.docx";
|
||||
export let maxSize = 1024 * 1024 * 5; // 5MB
|
||||
|
||||
let error = "";
|
||||
let fileName = state ? state.split('/').pop() : "No file selected";
|
||||
let inputEl: HTMLInputElement;
|
||||
|
||||
function handleFileChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
const files = input.files;
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
onChange(null);
|
||||
fileName = "No file selected";
|
||||
error = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const file = files[0];
|
||||
|
||||
// Validate file size
|
||||
if (file.size > maxSize) {
|
||||
error = `File too large. Maximum size is ${maxSize / (1024 * 1024)}MB.`;
|
||||
input.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
error = "";
|
||||
fileName = file.name;
|
||||
|
||||
// Read file as data URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (e.target && typeof e.target.result === 'string') {
|
||||
onChange(e.target.result);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
function clearFile() {
|
||||
if (inputEl) inputEl.value = "";
|
||||
onChange(null);
|
||||
fileName = "No file selected";
|
||||
error = "";
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="file-picker">
|
||||
<div class="file-input">
|
||||
<button class="browse-btn" on:click={() => inputEl.click()}>
|
||||
Browse...
|
||||
</button>
|
||||
<span class="file-name">{fileName}</span>
|
||||
{#if state}
|
||||
<button class="clear-btn" on:click={clearFile}>
|
||||
✕
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
bind:this={inputEl}
|
||||
on:change={handleFileChange}
|
||||
{accept}
|
||||
class="hidden"
|
||||
/>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.file-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.browse-btn {
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
color: #999;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
To use this in the settings system, you would:
|
||||
|
||||
1. Define a `FileSetting` interface in `types.ts`
|
||||
2. Create a `FileSetting` decorator in `settings.ts`
|
||||
3. Update the `getPluginSettingEntries` function in `general.svelte`
|
||||
|
||||
This component demonstrates:
|
||||
- Handling file input (a more complex input type)
|
||||
- Input validation
|
||||
- Error handling
|
||||
- Multiple interactive elements
|
||||
- Binding to DOM elements
|
||||
- Clean UI that follows platform conventions
|
||||
Reference in New Issue
Block a user