| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 17 天前 | ||
| 12 天前 | ||
| 12 天前 | ||
| 12 天前 | ||
| 17 天前 |
LibreGene Plugins
Plugins extend LibreGene with new tools (dialogs, tracks, menu entries). They are compiled into the app — there is no runtime loading — so contributing a plugin means opening a pull request against this repository.
Directory layout
src/plugins/
├── index.js # static registry — every plugin must be listed here
├── README.md # this file
├── <id>/ # your plugin goes here, one directory per plugin
├── builtin/ # the editor's original built-in plugins
└── example-plugin-*/ # self-contained reference examples (dotplot, gel)
<id>/— the default location for a new plugin: a single camelCase directory at the top level ofsrc/plugins/.builtin/— where the pre-existing built-in plugins live. Don't add new plugins here.example-plugin-*— two fully self-contained example plugins. Treat them as documentation you can run: read them to see the plugin object, dialog contract and styling conventions in action.
The plugin object
Each plugin's index.js default-exports a plain object:
const myPlugin = {
id: 'myPlugin', // unique, camelCase; used for disable state & dialog keys
name: 'My Plugin',
description: 'One line shown in the Plugins settings window',
version: '1.0.0',
author: 'your-name', // optional; shown as "by <author>" in the Plugins window
// Fully declarative hooks — work with zero host-side changes:
dialogKey: 'myPlugin', // key into the pluginDialogs open-state map; null if
// no dialog. May differ from id (e.g. snapgeneHistory uses 'snapshots').
dialog: MyDialog, // React component, receives { open, onOpenChange, ... }
// Optional predicate gating every entry of the plugin; receives
// { projectId, moleculeType, isTauri }.
dialogVisible: ({ projectId, moleculeType, isTauri }) => true,
sidebarItems: [{ dialogKey, label, tooltip, icon }],
// Molecule-type gating (optional):
dnaOnly: false, // DNA projects only
rnaOnly: false, // RNA projects only
notForProtein: false, // hidden for protein projects
// View-menu visibility toggle (optional). State is persisted per plugin id
// in localStorage (key `pluginToggle:<id>`); defaultChecked defaults to
// false. Also gates the track hook's `enabled` flag.
featuresMenuItem: { label, defaultChecked },
// Numeric field in Settings (optional). Persisted like the toggle (key
// `pluginSetting:<id>`); defaultValue falls back to min. Values are rounded
// and clamped to [min, max].
settingsField: { label, description, min, max, unit, defaultValue },
// Custom track below the sequence (optional). useLane receives
// { cleanSeq, topology, moleculeType, enabled, settingValue } and returns
// { height, ... } | null; render(ctx, lane) draws it.
track: { useLane, render },
};
export default myPlugin;
Then register it in index.js:
import myPlugin from './myPlugin';
export const plugins = [
// ...
myPlugin,
];
Conventions
- Disable-ability is mandatory. Every plugin must disappear completely
when the user disables it (localStorage
disabledPlugins): sidebar entries, dialogs and nav-menu items are all derived from the registry, so register every entry point through the plugin object rather than hard-coding buttons. - Track rendering goes through the visual-cell stream. A
trackplugin implementsuseLane(ctx)+render(ctx, lane). Anything anchored to template columns must convert coordinates withctx'scolVis/colRuns/colFromVishelpers — never assume a linear column→x mapping (seebuiltin/gcContent/track.jsx). - Dialog props are the host contract. Every plugin dialog is rendered with
the same fixed prop set (see
src/workspace/WorkspaceDialogs.jsx):{ open, onOpenChange, projectId, sequence, fileName, features, enzymes, myEnzymes, enzymeFilter, topology, alignments, dialogState, onAddAlignment, onRemoveAlignment, onOpenSnapshot, onProjectChanged, watermark, onToggleWatermark }. Take what you need and ignore the rest. Close the dialog viaonOpenChange(false); do not unmount it yourself.dialogStatecarries whatever the opener stored inpluginDialogs[dialogKey](e.g.{ enzymes: [...] }to preselect gel enzymes). - Gating has a single source of truth. Disabled state, molecule-type flags
and
dialogVisibleare evaluated bypluginAvailable(plugin, ctx)fromsrc/plugins/index.js— every consumer (sidebar, dialogs, View menu, tracks, nav-menu openers) goes through it, so declare the flags on the plugin object and they apply everywhere. - JSON fields are camelCase, matching the rest of the codebase.
- Respect molecule type. If your tool is meaningless for RNA/protein, set
the gating flags instead of checking
moleculeTypeyourself. - Keep comments minimal and in English; commit messages are English
(
feat:/fix:/refactor:).
Host wiring
Everything except nav-menu entries works purely from the registry: dialog,
sidebarItems, molecule gating, disable-ability, featuresMenuItem (View-menu
toggle), settingsField (Settings field) and track lanes all function
without touching host code — toggle/field state is created and persisted
automatically per plugin id (src/hooks/useSettings.js).
The one exception is nav-menu entries (e.g. the Diagrams menu): the menu
item itself is wired in src/EditorNavMenu.jsx with an onOpen<Plugin>
callback threaded from src/ProjectWorkspace.jsx, gated there via
pluginGate('<id>') (a pluginAvailable wrapper). Follow the onOpenDotplot
/ onOpenGel examples.
Verify before submitting
npx vite build # frontend compiles
npm run lint # ESLint
npm test # vitest
Then open the app (npx tauri dev), confirm the plugin appears in the
Plugins window (sidebar → Plugins), and toggle it off/on to check that all
of its entry points vanish and return.
Distributable plugins
If you want your plugin to be reusable outside LibreGene, keep it
self-contained like the examples: no imports outside its own directory (React
excepted), its own minimal UI primitives (ui.jsx), and a manifest.json +
README.md documenting the host contract. Self-containment is a property of
the plugin, not a naming requirement — your directory is still
src/plugins/<id>/. The two example plugins are the reference implementation.