feat: add docs and dev plugins

This commit is contained in:
SethBurkart123
2025-03-18 22:15:40 +11:00
parent 7a76d3f4eb
commit 1c63c06b72
18 changed files with 3855 additions and 54 deletions
+301
View File
@@ -0,0 +1,301 @@
# 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!
+335
View File
@@ -0,0 +1,335 @@
# 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
+541
View File
@@ -0,0 +1,541 @@
# 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