Das System-Entity Framework ermΓΆglicht es, neue System-Entities (wie Settings, Marketplace, ZeitplΓ€ne) universell zu erstellen, ohne hardcoded Checks in UI-Komponenten hinzufΓΌgen zu mΓΌssen.
Vorher (β Hardcoded):
// DetailView.jsx
if (item.domain === 'settings' ||
item.domain === 'marketplace' ||
item.domain === 'all_schedules' ||
item.domain === 'pluginstore') {
// ...
}
// DeviceCard.jsx
if (device.domain === 'settings' ||
device.domain === 'marketplace' ||
device.domain === 'all_schedules' ||
device.domain === 'pluginstore') {
// ...
}Nachher (β Universal):
// DetailView.jsx
if (systemRegistry.isSystemEntity(item.domain)) {
// Automatisch!
}
// DeviceCard.jsx
if (systemRegistry.isSystemEntity(device.domain)) {
// Automatisch!
}Jede System-Entity hat folgende UI Behavior Properties:
| Property | Typ | Default | Beschreibung |
|---|---|---|---|
hasTabs |
boolean | false |
Hat die Entity Tabs in der DetailView? |
hasCustomView |
boolean | true |
Rendert die Entity eine eigene View-Komponente? |
showInDetailView |
boolean | true |
Soll die Entity in der DetailView geΓΆffnet werden? |
Die systemRegistry bietet folgende Methoden:
// PrΓΌfe ob Domain eine System-Entity ist
systemRegistry.isSystemEntity(domain: string): boolean
// PrΓΌfe ob System-Entity Tabs hat
systemRegistry.hasTabs(domain: string): boolean
// PrΓΌfe ob System-Entity Custom View hat
systemRegistry.hasCustomView(domain: string): boolean
// PrΓΌfe ob in DetailView geΓΆffnet werden soll
systemRegistry.shouldShowInDetailView(domain: string): booleanDatei: src/system-entities/entities/[name]/index.js
import { SystemEntity } from '../../base/SystemEntity.js';
class MyCustomEntity extends SystemEntity {
constructor() {
super({
// Core Properties
id: 'my_custom',
domain: 'my_custom',
name: 'Meine Entity',
icon: 'mdi:star',
category: 'system',
description: 'Beschreibung der Entity',
relevance: 90,
// π UI Behavior Properties
hasTabs: false, // KEINE Tabs (eigene View)
hasCustomView: true, // Rendert eigene View
showInDetailView: true, // In DetailView ΓΆffnen
// View Component (Lazy Load)
viewComponent: () => import('./MyCustomView.jsx')
});
}
}
export default new MyCustomEntity();Datei: src/system-entities/entities/[name]/MyCustomView.jsx
import { h } from 'preact';
export default function MyCustomView({
entity,
hass,
lang,
onBack,
onNavigate,
onServiceCall
}) {
return (
<div className="my-custom-view">
<h1>{entity.name}</h1>
<p>{entity.description}</p>
{/* Deine UI hier */}
</div>
);
}Datei: src/system-entities/registry.js
FΓΌge den Import in autoDiscover() hinzu:
async autoDiscover() {
console.log('π Auto-discovering system entities...');
const knownEntities = [
() => import('./entities/settings/index.js'),
() => import('./entities/marketplace/index.js'),
() => import('./entities/pluginstore/index.js'),
() => import('./entities/all-schedules/index.js'),
() => import('./entities/my-custom/index.js'), // π HIER HINZUFΓGEN!
];
// ...
}Die Entity ist jetzt automatisch integriert in:
- β DetailView - erkennt automatisch System-Entity
- β DeviceCard - zeigt korrektes Icon und Layout
- β SearchField - erscheint in Suchergebnissen
- β DataProvider - wird in Entity-Liste geladen
KEINE weiteren Γnderungen nΓΆtig! π
// z.B. ZeitplΓ€ne-Γbersicht, Marketplace, PluginStore
super({
id: 'all_schedules',
domain: 'all_schedules',
name: 'ZeitplΓ€ne Γbersicht',
icon: 'mdi:calendar-clock',
category: 'system',
relevance: 95,
hasTabs: false, // β
KEINE Tabs
hasCustomView: true, // β
Eigene View
showInDetailView: true, // β
In DetailView ΓΆffnen
viewComponent: () => import('./AllSchedulesView.jsx')
});Ergebnis:
- DetailView rendert die
AllSchedulesViewdirekt - Keine Tab-Leiste wird angezeigt
- View hat volle Kontrolle ΓΌber Layout
// z.B. Settings
super({
id: 'settings',
domain: 'settings',
name: 'Einstellungen',
icon: 'mdi:cog',
category: 'system',
relevance: 100,
hasTabs: true, // β
HAT Tabs
hasCustomView: false, // β
Nutzt alte SettingsTab
showInDetailView: true, // β
In DetailView ΓΆffnen
viewComponent: () => import('./SettingsView.jsx')
});Ergebnis:
- DetailView zeigt Tab-Leiste (About, Appearance, etc.)
- Settings-Komponente verwaltet Tabs intern
- Tab-Clicks Γ€ndern Content korrekt
// z.B. Quick-Action Entity
super({
id: 'quick_action',
domain: 'quick_action',
name: 'Quick Action',
icon: 'mdi:lightning-bolt',
category: 'tools',
relevance: 85,
hasTabs: false,
hasCustomView: true,
showInDetailView: false, // β NICHT in DetailView ΓΆffnen
// Keine viewComponent - fΓΌhrt direkt Action aus
actions: {
execute: async function() {
// Action-Code hier
}
}
});Ergebnis:
- Click auf Entity ΓΆffnet KEINE DetailView
- FΓΌhrt stattdessen direkt eine Action aus
- NΓΌtzlich fΓΌr Shortcuts/Quick-Actions
Datei: src/components/DetailView.jsx
const renderTabContent = () => {
// Special Case: Settings wird separat behandelt
if (item.domain === 'settings') {
return (
<SettingsTab
settingsRef={settingsTabRef}
lang={lang}
onTabChange={(newTab) => {}}
/>
);
}
// Universal: System-Entity Check via Registry
const isSystemEntity = item.is_system ||
item.is_plugin ||
systemRegistry.isSystemEntity(item.domain);
if (isSystemEntity) {
const SystemViewComponent = systemRegistry.getViewComponent(item.domain);
if (SystemViewComponent) {
if (typeof SystemViewComponent === 'function') {
return (
<SystemEntityLazyView
viewLoader={SystemViewComponent}
entity={item}
hass={hass}
lang={lang}
onBack={handleBackClick}
onNavigate={onActionNavigate}
onServiceCall={onServiceCall}
/>
);
}
}
}
// Normal Entities (Lights, Sensors, etc.)
// ...
};const getFilteredTabIcons = () => {
// Settings behΓ€lt Tabs
if (item.domain === 'settings') {
return settingsTabIcons;
}
// Universal: PrΓΌfe via Registry ob Entity Tabs haben soll
if (systemRegistry.isSystemEntity(item.domain)) {
const hasTabs = systemRegistry.hasTabs(item.domain);
if (!hasTabs) {
return []; // System-Entity ohne Tabs
}
}
// Sensors haben 3 Tabs
if (item.domain === 'sensor' || item.domain === 'binary_sensor') {
return [tabIcons[0], tabIcons[2], tabIcons[3]];
}
// Standard: 4 Tabs
return tabIcons;
};Datei: src/components/DeviceCard.jsx
const getDisplayState = () => {
// Universal: System-Entities haben keine Status-Anzeige
if (device.is_system || systemRegistry.isSystemEntity(device.domain)) {
return '';
}
// Normal Entities zeigen Status
// ...
};| Entity | Domain | hasTabs | hasCustomView | Beschreibung |
|---|---|---|---|---|
| Settings | settings |
β
true |
β false |
System-Einstellungen mit Tabs (About, Appearance, etc.) |
| Marketplace | marketplace |
β false |
β
true |
Add-ons und Integrationen verwalten |
| PluginStore | pluginstore |
β false |
β
true |
Plugins installieren und verwalten |
| AllSchedules | all_schedules |
β false |
β
true |
Γbersicht aller Timer und ZeitplΓ€ne |
- Nutze Registry-Methoden statt hardcoded domain-Checks
- Setze
hasTabs: falsefΓΌr neue Entities (auΓer es gibt einen guten Grund) - Lazy-Load View-Komponenten via
() => import('./MyView.jsx') - Dokumentiere neue Properties in Entity-Kommentaren
- KEINE hardcoded domain-Checks in UI-Komponenten
- NICHT
hasCustomView: falseverwenden (auΓer fΓΌr Legacy wie Settings) - KEINE synchronen Imports fΓΌr View-Komponenten
- NICHT direkt
systemRegistry.entitieszugreifen - nutze Helper-Methoden
LΓΆsung:
- PrΓΌfe ob Entity in
registry.jsβautoDiscover()importiert ist - PrΓΌfe Browser-Console auf Fehler beim Import
- PrΓΌfe ob Entity korrekt exportiert wird:
export default new MyEntity();
LΓΆsung:
- PrΓΌfe ob
viewComponentkorrekt gesetzt ist - PrΓΌfe Browser-Console auf Import-Fehler
- PrΓΌfe ob View-Komponente korrekt exportiert:
export default function MyView() { ... }
LΓΆsung:
- PrΓΌfe
hasTabs: truein Entity-Definition - PrΓΌfe ob
getFilteredTabIcons()die richtigen Tabs zurΓΌckgibt - FΓΌr Settings: PrΓΌfe ob
settingsRefkorrekt ΓΌbergeben wird
LΓΆsung:
- PrΓΌfe ob
systemRegistry.isInitialized === true - PrΓΌfe ob Entity in Registry geladen:
systemRegistry.getEntity('my_custom') - PrΓΌfe Console-Log beim App-Start:
β Registered entity: my_custom
// DetailView.jsx
if (item.domain === 'my_custom' || item.domain === 'other_custom') {
// Custom handling
}
// DeviceCard.jsx
if (device.domain === 'my_custom' || device.domain === 'other_custom') {
return '';
}// DetailView.jsx
if (systemRegistry.isSystemEntity(item.domain)) {
// Automatisch via Registry!
}
// DeviceCard.jsx
if (systemRegistry.isSystemEntity(device.domain)) {
return '';
}Migration Steps:
- Erstelle Entity-Datei mit Properties
- Registriere in
autoDiscover() - Entferne alle hardcoded domain-Checks
- Teste alle betroffenen UI-Komponenten
// src/system-entities/entities/notifications/index.js
import { SystemEntity } from '../../base/SystemEntity.js';
class NotificationsEntity extends SystemEntity {
constructor() {
super({
// Core Properties
id: 'notifications',
domain: 'notifications',
name: 'Benachrichtigungen',
icon: 'mdi:bell',
category: 'system',
description: 'Verwalte deine Benachrichtigungen',
relevance: 90,
// Attributes
attributes: {
unread_count: 0,
last_notification: null
},
// Permissions
permissions: [
'notifications:read',
'notifications:write',
'notifications:clear'
],
// Routes (Deep Links)
routes: {
all: '/notifications',
unread: '/notifications/unread',
settings: '/notifications/settings'
},
// Actions
actions: {
markAsRead: async function(params) {
const { notificationId } = params;
// Mark notification as read
console.log(`Marked notification ${notificationId} as read`);
},
clearAll: async function() {
// Clear all notifications
console.log('Cleared all notifications');
}
},
// UI Behavior
hasTabs: false,
hasCustomView: true,
showInDetailView: true,
// View Component
viewComponent: () => import('./NotificationsView.jsx')
});
}
/**
* Lifecycle: onMount
*/
async onMount(context) {
await super.onMount(context);
console.log('π¬ Notifications Entity mounted');
// Subscribe to notification events
if (context.hass) {
context.hass.connection.subscribeEvents(
(event) => {
console.log('New notification:', event);
this.updateAttributes({
unread_count: this.attributes.unread_count + 1,
last_notification: event.data
});
},
'notification'
);
}
}
}
export default new NotificationsEntity();Mit dem Universal System-Entity Framework kΓΆnnen neue Entities mit minimalen Code-Γnderungen hinzugefΓΌgt werden:
- β 1 Entity-Datei erstellen
- β 1 View-Komponente erstellen
- β 1 Import in Registry hinzufΓΌgen
- β FERTIG!
Keine weiteren Γnderungen in DetailView, DeviceCard oder anderen UI-Komponenten nΓΆtig! π
| Feature | System-Entity | External Plugin |
|---|---|---|
| Location | Built-in (src/system-entities/entities/) |
External (URL, GitHub, ZIP) |
| Loading | Auto-discovered at startup | Dynamically loaded via Plugin Loader |
| Installation | Part of card build | User installs via Plugin Store |
| Updates | Card updates | Individual plugin updates |
| Security | Trusted code | Sandboxed, validated |
| Access | Full system access | Limited API access |
Every external plugin requires a manifest.json file:
{
"id": "my-awesome-plugin",
"name": "My Awesome Plugin",
"version": "1.0.0",
"author": "Your Name",
"description": "A plugin that does amazing things",
"homepage": "https://github.com/username/my-plugin",
"icon": "mdi:star",
"category": "tools",
"relevance": 85,
"main": "index.js",
"viewComponent": "MyPluginView.jsx",
"permissions": [
"entities:read",
"entities:write",
"storage:read",
"storage:write"
],
"dependencies": {
"preact": "^10.0.0",
"framer-motion": "^10.0.0"
},
"compatibility": {
"minVersion": "1.0.0",
"maxVersion": "2.0.0"
},
"settings": {
"apiKey": {
"type": "string",
"default": "",
"required": true,
"label": "API Key",
"description": "Your API key for the service"
},
"updateInterval": {
"type": "number",
"default": 300,
"min": 60,
"max": 3600,
"label": "Update Interval (seconds)"
}
}
}my-awesome-plugin/
βββ manifest.json # Plugin manifest (required)
βββ index.js # Main entry point (required)
βββ MyPluginView.jsx # View component (optional)
βββ styles.css # Styles (optional)
βββ icon.svg # Custom icon (optional)
βββ README.md # Documentation (recommended)
βββ LICENSE # License file (recommended)
import { SystemEntity } from '@fast-search-card/system-entities';
class MyAwesomePlugin extends SystemEntity {
constructor() {
// Load manifest
const manifest = require('./manifest.json');
super({
id: manifest.id,
domain: manifest.id,
name: manifest.name,
icon: manifest.icon,
category: manifest.category,
description: manifest.description,
relevance: manifest.relevance,
// Plugin-specific
isPlugin: true,
pluginManifest: manifest,
// UI Behavior
hasTabs: false,
hasCustomView: true,
showInDetailView: true,
// View Component
viewComponent: () => import('./MyPluginView.jsx'),
// Actions
actions: {
refresh: async () => {
await this.refreshData();
}
}
});
}
/**
* Initialize plugin
*/
async onMount(context) {
await super.onMount(context);
console.log('π Plugin mounted:', this.id);
// Load settings from storage
const settings = await this.getSettings();
// Initialize with settings
await this.initialize(settings);
}
/**
* Cleanup plugin
*/
async onUnmount() {
console.log('π Plugin unmounting:', this.id);
// Cleanup resources
await this.cleanup();
await super.onUnmount();
}
/**
* Custom plugin methods
*/
async initialize(settings) {
// Initialize plugin with settings
}
async refreshData() {
// Refresh plugin data
}
async cleanup() {
// Cleanup resources
}
}
export default new MyAwesomePlugin();import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { motion } from 'framer-motion';
export default function MyPluginView({
entity,
hass,
lang,
onBack,
onNavigate,
onServiceCall
}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
setLoading(true);
// Load data from API or Home Assistant
const result = await fetch('/api/data');
const json = await result.json();
setData(json);
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="plugin-loading">
<div className="spinner" />
<p>Loading...</p>
</div>
);
}
return (
<motion.div
className="my-plugin-view"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<header className="plugin-header">
<h1>{entity.name}</h1>
<button onClick={() => entity.actions.refresh()}>
Refresh
</button>
</header>
<div className="plugin-content">
{/* Your plugin UI */}
{data && (
<pre>{JSON.stringify(data, null, 2)}</pre>
)}
</div>
</motion.div>
);
}Plugins must declare required permissions in manifest.json:
{
"permissions": [
"entities:read", // Read entity states
"entities:write", // Control entities
"storage:read", // Read from storage
"storage:write", // Write to storage
"network:fetch", // Make HTTP requests
"notifications:send" // Send notifications
]
}| Permission | Description | Risk Level |
|---|---|---|
entities:read |
Read entity states and attributes | Low |
entities:write |
Control entities (turn on/off, etc.) | Medium |
storage:read |
Read plugin storage | Low |
storage:write |
Write to plugin storage | Low |
network:fetch |
Make HTTP requests | Medium |
network:websocket |
Open WebSocket connections | High |
notifications:send |
Send notifications to user | Low |
automation:create |
Create automations | High |
automation:delete |
Delete automations | High |
system:settings |
Access system settings | High |
DO β
- Request only required permissions
- Validate all user input
- Use HTTPS for external requests
- Handle errors gracefully
- Store sensitive data encrypted
- Follow least privilege principle
DON'T β
- Request unnecessary permissions
- Store passwords in plain text
- Make unvalidated API calls
- Bypass permission checks
- Access system internals directly
- Execute arbitrary code
All plugins are validated before loading:
- Manifest Validation - Schema validation
- Permission Check - User approval required
- Code Scanning - Basic security checks
- Dependency Check - Verify dependencies
- Signature Verification - Check plugin signature (future)
The context object passed to onMount() provides:
{
hass: {
connection, // WebSocket connection
callService, // Call HA service
callWS, // Call WebSocket API
states, // Entity states
config, // HA config
user // Current user
},
storage: {
get, // Get from storage
set, // Set in storage
delete, // Delete from storage
clear // Clear all storage
},
notifications: {
show, // Show notification
success, // Success toast
error, // Error toast
warning, // Warning toast
info // Info toast
},
ui: {
openEntity, // Open entity detail
closeDetail, // Close detail view
showModal, // Show modal dialog
hideModal // Hide modal
},
events: {
on, // Subscribe to event
off, // Unsubscribe
emit // Emit custom event
}
}// Get value
const value = await this.getStorage('key');
// Set value
await this.setStorage('key', 'value');
// Get all plugin settings
const settings = await this.getSettings();
// Update setting
await this.updateSetting('apiKey', 'new-key');// Get entity state
const state = await this.getEntityState('light.living_room');
// Control entity
await this.callService('light', 'turn_on', {
entity_id: 'light.living_room',
brightness: 255
});
// Subscribe to entity changes
this.subscribeEntityChanges('light.living_room', (newState) => {
console.log('State changed:', newState);
});// Show success message
this.showSuccess('Operation completed!');
// Show error
this.showError('Something went wrong');
// Show custom notification
this.showNotification({
title: 'Plugin Update',
message: 'New version available',
type: 'info',
duration: 5000
});// manifest.json
{
"id": "weather-plugin",
"name": "Weather Dashboard",
"version": "1.0.0",
"permissions": [
"entities:read",
"network:fetch",
"storage:read",
"storage:write"
],
"settings": {
"location": {
"type": "string",
"default": "Berlin",
"label": "Location"
},
"units": {
"type": "select",
"default": "metric",
"options": ["metric", "imperial"],
"label": "Units"
}
}
}
// index.js
class WeatherPlugin extends SystemEntity {
constructor() {
super({
id: 'weather-plugin',
domain: 'weather-plugin',
name: 'Weather Dashboard',
icon: 'mdi:weather-partly-cloudy',
category: 'apps',
relevance: 90,
isPlugin: true,
viewComponent: () => import('./WeatherView.jsx')
});
this.weatherData = null;
this.updateInterval = null;
}
async onMount(context) {
await super.onMount(context);
// Load settings
const settings = await this.getSettings();
// Start auto-update
this.startAutoUpdate(settings);
}
async onUnmount() {
// Stop auto-update
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
await super.onUnmount();
}
startAutoUpdate(settings) {
this.updateWeather(settings);
this.updateInterval = setInterval(() => {
this.updateWeather(settings);
}, 300000); // 5 minutes
}
async updateWeather(settings) {
try {
const response = await fetch(
`https://api.openweathermap.org/data/2.5/weather?q=${settings.location}&units=${settings.units}&appid=${settings.apiKey}`
);
this.weatherData = await response.json();
// Update attributes
this.updateAttributes({
temperature: this.weatherData.main.temp,
humidity: this.weatherData.main.humidity,
condition: this.weatherData.weather[0].main,
lastUpdate: new Date().toISOString()
});
} catch (error) {
console.error('Weather update failed:', error);
this.showError('Failed to update weather data');
}
}
}
export default new WeatherPlugin();class AutomationBuilderPlugin extends SystemEntity {
constructor() {
super({
id: 'automation-builder',
domain: 'automation-builder',
name: 'Automation Builder',
icon: 'mdi:robot',
category: 'tools',
relevance: 95,
isPlugin: true,
permissions: [
'entities:read',
'automation:create',
'automation:delete',
'storage:read',
'storage:write'
],
viewComponent: () => import('./AutomationBuilderView.jsx')
});
}
async createAutomation(config) {
try {
// Validate config
if (!this.validateAutomationConfig(config)) {
throw new Error('Invalid automation config');
}
// Create via Home Assistant API
await this._context.hass.callService('automation', 'create', {
alias: config.name,
trigger: config.triggers,
condition: config.conditions,
action: config.actions
});
this.showSuccess('Automation created!');
} catch (error) {
console.error('Failed to create automation:', error);
this.showError('Failed to create automation');
}
}
validateAutomationConfig(config) {
return config.name &&
config.triggers &&
config.triggers.length > 0 &&
config.actions &&
config.actions.length > 0;
}
}
export default new AutomationBuilderPlugin();Enable developer mode in Settings:
// Add to browser console
localStorage.setItem('fastSearchCard:devMode', 'true');// Test plugin loading
const plugin = await SystemEntityLoader.loadFromURL('http://localhost:3000/plugin.js');
// Validate manifest
const errors = validatePluginManifest(plugin.manifest);
if (errors.length > 0) {
console.error('Validation errors:', errors);
}
// Mount plugin
await plugin.onMount(testContext);
// Trigger action
await plugin.actions.refresh();
// Unmount
await plugin.onUnmount();class MyPlugin extends SystemEntity {
constructor() {
super({ /* config */ });
// Enable debug logging
this.debug = true;
}
log(message, ...args) {
if (this.debug) {
console.log(`[${this.id}]`, message, ...args);
}
}
async onMount(context) {
await super.onMount(context);
this.log('Plugin mounted', { context });
}
}Plugin doesn't appear:
- Check manifest.json syntax
- Verify plugin is registered in registry
- Check browser console for errors
Permissions denied:
- Request required permissions in manifest
- User must approve permissions
View doesn't render:
- Verify viewComponent path
- Check for JSX/component errors
- Ensure dependencies are available
Publish plugins to the Fast Search Card Plugin Store:
- Create plugin repository on GitHub
- Add
manifest.jsonin root - Create release with version tag
- Submit to Plugin Store:
https://plugins.fastcard.io/submit
# .github/workflows/release.yml
name: Release Plugin
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create Release
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: |
See CHANGELOG.md for details
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Users can install plugins via:
# GitHub
https://github.com/username/plugin-name
# Direct URL
https://cdn.example.com/my-plugin.js
# Plugin Store
fastcard://plugin/my-plugin-id
Use Fast Search Card design system:
/* Use CSS variables */
.my-plugin {
background: var(--glass-bg);
border: 1px solid var(--glass-border-color);
border-radius: var(--border-radius);
padding: var(--spacing-md);
color: var(--text-primary);
}
.my-plugin-button {
background: var(--accent);
color: var(--accent-contrast);
border-radius: var(--border-radius-sm);
padding: var(--spacing-sm) var(--spacing-md);
}Use Framer Motion for consistent animations:
import { motion } from 'framer-motion';
const fadeIn = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
<motion.div
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ duration: 0.3 }}
>
{/* Content */}
</motion.div>Support all screen sizes:
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = () => {
setIsMobile(window.innerWidth <= 768);
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
<Suspense fallback={<LoadingSpinner />}>
<HeavyComponent />
</Suspense>import { useMemo, useCallback } from 'preact/hooks';
// Memoize expensive calculations
const expensiveData = useMemo(() => {
return processLargeDataset(rawData);
}, [rawData]);
// Memoize callbacks
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);const debouncedSearch = useMemo(
() =>
debounce((query) => {
performSearch(query);
}, 300),
[]
);// translations.js
export const translations = {
en: {
title: 'My Plugin',
button: 'Click Me'
},
de: {
title: 'Mein Plugin',
button: 'Klick Mich'
}
};
// Component
const t = (key) => translations[lang][key] || key;
<h1>{t('title')}</h1>
<button>{t('button')}</button>Before publishing your plugin:
- Manifest is complete and valid
- All permissions are justified
- Code is tested and debugged
- Documentation (README.md) is included
- License file is included
- Version number follows semver
- Icon is provided
- Screenshots are included
- Responsive design is implemented
- Error handling is complete
- Performance is optimized
- Code is commented
- Security best practices followed
Version: 2.0.0 Updated: 2025-10-29 Author: Fast Search Card Team