|
@@ -0,0 +1,627 @@
|
|
|
|
|
+# AGENTS.md - Logseq Plugin Development Guide
|
|
|
|
|
+
|
|
|
|
|
+## Overview
|
|
|
|
|
+
|
|
|
|
|
+This document provides guidance for AI agents (Copilot, coding assistants, etc.) to help develop Logseq plugins using
|
|
|
|
|
+the `@logseq/libs` SDK.
|
|
|
|
|
+
|
|
|
|
|
+Logseq is a privacy-first, open-source knowledge management and note-taking application built on top of local plain-text
|
|
|
|
|
+Markdown and Org-mode files. The plugin system extends Logseq's functionality through a JavaScript/TypeScript SDK.
|
|
|
|
|
+
|
|
|
|
|
+## Repository Structure
|
|
|
|
|
+
|
|
|
|
|
+### Core SDK (`logseq/logseq` - `libs/` folder)
|
|
|
|
|
+
|
|
|
|
|
+The official SDK source code is located in the `libs/` directory of the main Logseq repository:
|
|
|
|
|
+
|
|
|
|
|
+```
|
|
|
|
|
+libs/
|
|
|
|
|
+├── src/
|
|
|
|
|
+│ ├── LSPlugin.ts # Main type definitions & interfaces
|
|
|
|
|
+│ ├── LSPlugin.user.ts # User-facing plugin API implementation
|
|
|
|
|
+│ ├── LSPlugin.core.ts # Core plugin system
|
|
|
|
|
+│ ├── LSPlugin. caller.ts # IPC message caller
|
|
|
|
|
+│ ├── LSPlugin.shadow.ts # Shadow DOM support
|
|
|
|
|
+│ ├── helpers.ts # Utility functions
|
|
|
|
|
+│ ├── modules/ # Additional modules (Experiments, Storage, Request)
|
|
|
|
|
+│ └── postmate/ # Cross-frame communication
|
|
|
|
|
+├── package.json # NPM package config (@logseq/libs)
|
|
|
|
|
+├── README.md # SDK documentation
|
|
|
|
|
+└── CHANGELOG.md # Version history
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Plugin Samples (`logseq/logseq-plugin-samples`)
|
|
|
|
|
+
|
|
|
|
|
+Official sample plugins demonstrating various SDK features:
|
|
|
|
|
+
|
|
|
|
|
+| Sample | Description | Key APIs |
|
|
|
|
|
+|-------------------------------|----------------------------------|-------------------------------------------------|
|
|
|
|
|
+| `logseq-slash-commands` | Basic slash command registration | `Editor.registerSlashCommand` |
|
|
|
|
|
+| `logseq-pomodoro-timer` | Macro renderer & slot UI | `App.onMacroRendererSlotted`, `provideUI` |
|
|
|
|
|
+| `logseq-a-translator` | Selection hooks & float UI | `Editor.onInputSelectionEnd`, `provideUI` |
|
|
|
|
|
+| `logseq-awesome-fonts` | Custom styles & settings | `provideStyle`, `useSettingsSchema` |
|
|
|
|
|
+| `logseq-bujo-themes` | Custom theme development | `provideTheme` |
|
|
|
|
|
+| `logseq-journals-calendar` | Calendar integration | `App.getCurrentGraph`, `Editor.getPage` |
|
|
|
|
|
+| `logseq-emoji-picker` | UI overlay with 3rd party libs | `showMainUI`, `hideMainUI` |
|
|
|
|
|
+| `logseq-reddit-hot-news` | Batch block insertion | `Editor.insertBatchBlock`, `App.registerUIItem` |
|
|
|
|
|
+| `logseq-imdb-top250-importer` | DB graph data import | DB-specific APIs |
|
|
|
|
|
+
|
|
|
|
|
+## Plugin Architecture
|
|
|
|
|
+
|
|
|
|
|
+### Entry Point Pattern
|
|
|
|
|
+
|
|
|
|
|
+Every Logseq plugin follows this basic pattern:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+import '@logseq/libs'
|
|
|
|
|
+
|
|
|
|
|
+async function main() {
|
|
|
|
|
+ // Plugin initialization code
|
|
|
|
|
+ console.log('Plugin loaded!')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Bootstrap
|
|
|
|
|
+logseq.ready(main).catch(console.error)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Plugin Modes
|
|
|
|
|
+
|
|
|
|
|
+Plugins can run in two modes defined in `package.json`:
|
|
|
|
|
+
|
|
|
|
|
+- **`iframe`** (default): Plugin runs in an isolated iframe sandbox
|
|
|
|
|
+- **`shadow`** (still draft): Plugin runs in a Shadow DOM container (faster, less isolation)
|
|
|
|
|
+
|
|
|
|
|
+> **⚠️ Sandbox Environment:** All Logseq plugins run in an isolated iframe sandbox.
|
|
|
|
|
+> This provides security isolation between plugins and the
|
|
|
|
|
+> main application. Communication between the plugin iframe and
|
|
|
|
|
+> Logseq happens through a postMessage-based RPC mechanism.
|
|
|
|
|
+
|
|
|
|
|
+### package.json Configuration
|
|
|
|
|
+
|
|
|
|
|
+## Package. json Configuration
|
|
|
|
|
+
|
|
|
|
|
+### Basic Structure
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "name": "logseq-my-plugin",
|
|
|
|
|
+ "version": "0.1.0",
|
|
|
|
|
+ "description": "A brief description of your plugin",
|
|
|
|
|
+ "author": "Your Name",
|
|
|
|
|
+ "license": "MIT",
|
|
|
|
|
+ "main": "dist/index.html",
|
|
|
|
|
+ "scripts": {
|
|
|
|
|
+ "dev": "parcel ./index.html --public-url ./",
|
|
|
|
|
+ "build": "parcel build --public-url . --no-source-maps index.html"
|
|
|
|
|
+ },
|
|
|
|
|
+ "devDependencies": {
|
|
|
|
|
+ "@logseq/libs": "^0.0.17",
|
|
|
|
|
+ "parcel": "^2.0.0"
|
|
|
|
|
+ },
|
|
|
|
|
+ "logseq": {
|
|
|
|
|
+ "id": "my-unique-plugin-id",
|
|
|
|
|
+ "main": "dist/index.html",
|
|
|
|
|
+ "icon": "./icon.png"
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Logseq Configuration Block
|
|
|
|
|
+
|
|
|
|
|
+The `logseq` field in `package.json` defines plugin metadata and behavior:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "logseq": {
|
|
|
|
|
+ "id": "my-unique-plugin-id",
|
|
|
|
|
+ "main": "dist/index.html",
|
|
|
|
|
+ "icon": "./icon. png",
|
|
|
|
|
+ "title": "My Plugin Display Name",
|
|
|
|
|
+ "effect": true,
|
|
|
|
|
+ "themes": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "My Dark Theme",
|
|
|
|
|
+ "url": "./css/dark.css",
|
|
|
|
|
+ "mode": "dark",
|
|
|
|
|
+ "description": "A beautiful dark theme"
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "My Light Theme",
|
|
|
|
|
+ "url": "./css/light.css",
|
|
|
|
|
+ "mode": "light",
|
|
|
|
|
+ "description": "A clean light theme"
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Configuration Fields
|
|
|
|
|
+
|
|
|
|
|
+| Field | Type | Required | Description |
|
|
|
|
|
+|------------|-----------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
|
|
|
+| `id` | `string` | Yes | Unique plugin identifier. Must be unique across all plugins. Use lowercase with hyphens. |
|
|
|
|
|
+| `main` | `string` | Yes | Entry point HTML file path (relative to package.json). |
|
|
|
|
|
+| `entry` | `string` | No | Alias for `main`. |
|
|
|
|
|
+| `icon` | `string` | No | Plugin icon path. Displayed in plugin marketplace and settings. |
|
|
|
|
|
+| `title` | `string` | No | Display name shown in UI. Falls back to package `name` if not set. |
|
|
|
|
|
+| `effect` | `boolean` | No | When `true`, the plugin runs as a background effect without user interaction. Useful for plugins that only inject styles/themes or run automated tasks. Default: `false`. |
|
|
|
|
|
+| `themes` | `Theme[]` | No | Array of theme definitions this plugin provides. |
|
|
|
|
|
+| `devEntry` | `string` | No | Alternative entry point for development (e.g., with HMR). |
|
|
|
|
|
+
|
|
|
|
|
+### Theme Configuration
|
|
|
|
|
+
|
|
|
|
|
+For theme-only plugins, define themes in the `themes` array:
|
|
|
|
|
+
|
|
|
|
|
+```json
|
|
|
|
|
+{
|
|
|
|
|
+ "logseq": {
|
|
|
|
|
+ "id": "my-theme-pack",
|
|
|
|
|
+ "main": "dist/index.html",
|
|
|
|
|
+ "effect": true,
|
|
|
|
|
+ "themes": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "Nord Dark",
|
|
|
|
|
+ "url": "./themes/nord-dark.css",
|
|
|
|
|
+ "mode": "dark",
|
|
|
|
|
+ "description": "Nord-inspired dark color scheme"
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "Nord Light",
|
|
|
|
|
+ "url": "./themes/nord-light.css",
|
|
|
|
|
+ "mode": "light",
|
|
|
|
|
+ "description": "Nord-inspired light color scheme"
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Theme Object Fields:**
|
|
|
|
|
+
|
|
|
|
|
+| Field | Type | Required | Description |
|
|
|
|
|
+|---------------|----------|----------|--------------------------------------------------|
|
|
|
|
|
+| `name` | `string` | Yes | Theme display name shown in theme selector. |
|
|
|
|
|
+| `url` | `string` | Yes | Path to the CSS file (relative to package.json). |
|
|
|
|
|
+| `mode` | `light` | `dark` | Yes | Theme mode. Determines when theme appears in selector. |
|
|
|
|
|
+| `description` | `string` | No | Brief description of the theme. |
|
|
|
|
|
+
|
|
|
|
|
+## Core API Namespaces
|
|
|
|
|
+
|
|
|
|
|
+The SDK exposes the following primary namespaces through the global `logseq` object:
|
|
|
|
|
+
|
|
|
|
|
+### `logseq. App` - Application APIs
|
|
|
|
|
+
|
|
|
|
|
+Application-level operations and hooks:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Get app/user information
|
|
|
|
|
+await logseq.App.getInfo()
|
|
|
|
|
+await logseq.App.getUserConfigs()
|
|
|
|
|
+await logseq.App.getCurrentGraph()
|
|
|
|
|
+
|
|
|
|
|
+// Graph operations
|
|
|
|
|
+await logseq.App.getCurrentGraphFavorites()
|
|
|
|
|
+await logseq.App.getCurrentGraphRecent()
|
|
|
|
|
+
|
|
|
|
|
+// Navigation
|
|
|
|
|
+logseq.App.pushState('page', { name: 'my-page' })
|
|
|
|
|
+
|
|
|
|
|
+// UI Registration
|
|
|
|
|
+logseq.App.registerUIItem('toolbar', {
|
|
|
|
|
+ key: 'my-button',
|
|
|
|
|
+ template: `<a data-on-click="myHandler">Click</a>`
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Command Registration
|
|
|
|
|
+logseq.App.registerCommandPalette({
|
|
|
|
|
+ key: 'my-command',
|
|
|
|
|
+ label: 'My Command',
|
|
|
|
|
+ keybinding: { binding: 'mod+shift+m' }
|
|
|
|
|
+}, () => { /* action */ })
|
|
|
|
|
+
|
|
|
|
|
+// Event Hooks
|
|
|
|
|
+logseq.App.onCurrentGraphChanged(() => {})
|
|
|
|
|
+logseq.App.onThemeModeChanged(({ mode }) => {})
|
|
|
|
|
+logseq.App.onRouteChanged(({ path }) => {})
|
|
|
|
|
+logseq.App.onMacroRendererSlotted(({ slot, payload }) => {})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### `logseq.Editor` - Editor APIs
|
|
|
|
|
+
|
|
|
|
|
+Block and page manipulation:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Slash Commands
|
|
|
|
|
+logseq.Editor.registerSlashCommand('My Command', async ({ uuid }) => {
|
|
|
|
|
+ await logseq.Editor.insertAtEditingCursor('Hello!')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Block Context Menu
|
|
|
|
|
+logseq.Editor.registerBlockContextMenuItem('My Action', async ({ uuid }) => {
|
|
|
|
|
+ const block = await logseq.Editor.getBlock(uuid)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Block Operations
|
|
|
|
|
+await logseq.Editor.getBlock(uuid)
|
|
|
|
|
+await logseq.Editor.insertBlock(uuid, 'content', { sibling: true })
|
|
|
|
|
+await logseq.Editor.updateBlock(uuid, 'new content')
|
|
|
|
|
+await logseq.Editor.removeBlock(uuid)
|
|
|
|
|
+await logseq.Editor.insertBatchBlock(uuid, [
|
|
|
|
|
+ { content: 'Block 1', children: [{ content: 'Child' }] }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// Page Operations
|
|
|
|
|
+await logseq.Editor.getPage('page-name')
|
|
|
|
|
+await logseq.Editor.createPage('new-page', { prop: 'value' })
|
|
|
|
|
+await logseq.Editor.deletePage('page-name')
|
|
|
|
|
+await logseq.Editor.getCurrentPageBlocksTree()
|
|
|
|
|
+
|
|
|
|
|
+// Cursor & Selection
|
|
|
|
|
+await logseq.Editor.checkEditing()
|
|
|
|
|
+await logseq.Editor.getEditingCursorPosition()
|
|
|
|
|
+await logseq.Editor.insertAtEditingCursor('text')
|
|
|
|
|
+await logseq.Editor.exitEditingMode()
|
|
|
|
|
+
|
|
|
|
|
+// Properties (DB graphs)
|
|
|
|
|
+await logseq.Editor.upsertBlockProperty(uuid, 'key', 'value')
|
|
|
|
|
+await logseq.Editor.getBlockProperties(uuid)
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### `logseq.DB` - Database APIs
|
|
|
|
|
+
|
|
|
|
|
+Query and data operations:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// DSL Query (Logseq query language)
|
|
|
|
|
+const results = await logseq.DB.q('[[my-page]]')
|
|
|
|
|
+
|
|
|
|
|
+// Datascript Query
|
|
|
|
|
+const results1 = await logseq.DB.datascriptQuery(`
|
|
|
|
|
+ [: find (pull ?b [*])
|
|
|
|
|
+ :where [?b :block/marker "TODO"]]
|
|
|
|
|
+`)
|
|
|
|
|
+
|
|
|
|
|
+// Watch for changes
|
|
|
|
|
+logseq.DB.onChanged(({ blocks, txData }) => {
|
|
|
|
|
+ console.log('Database changed:', blocks)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Watch specific block
|
|
|
|
|
+logseq.DB.onBlockChanged(uuid, (block, txData) => {
|
|
|
|
|
+ console.log('Block changed:', block)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// File operations
|
|
|
|
|
+await logseq.DB.getFileContent('logseq/custom.css')
|
|
|
|
|
+await logseq.DB.setFileContent('logseq/custom.js', 'console.log("hi")')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### `logseq.UI` - UI Utilities
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Toast messages
|
|
|
|
|
+await logseq.UI.showMsg('Success! ', 'success')
|
|
|
|
|
+await logseq.UI.showMsg('Warning!', 'warning', { timeout: 5000 })
|
|
|
|
|
+logseq.UI.closeMsg(key)
|
|
|
|
|
+
|
|
|
|
|
+// DOM queries
|
|
|
|
|
+const rect = await logseq.UI.queryElementRect('.my-selector')
|
|
|
|
|
+const exists = await logseq.UI.queryElementById('my-id')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### `logseq.Git` - Git Operations
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const result = await logseq.Git.execCommand(['status'])
|
|
|
|
|
+const ignoreContent = await logseq.Git.loadIgnoreFile()
|
|
|
|
|
+await logseq.Git.saveIgnoreFile('. DS_Store\nnode_modules')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### `logseq.Assets` - Asset Management
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const files = await logseq.Assets.listFilesOfCurrentGraph(['png', 'jpg'])
|
|
|
|
|
+const storage = logseq.Assets.makeSandboxStorage()
|
|
|
|
|
+const url = await logseq.Assets.makeUrl('path/to/file')
|
|
|
|
|
+await logseq.Assets.builtInOpen('path/to/asset. pdf')
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## UI Injection Patterns
|
|
|
|
|
+
|
|
|
|
|
+### provideUI - Inject Custom UI
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+logseq.provideUI({
|
|
|
|
|
+ key: 'my-ui',
|
|
|
|
|
+ path: '#my-target', // or use `slot` for macro renderers
|
|
|
|
|
+ template: `<div data-on-click="myHandler">Click me</div>`,
|
|
|
|
|
+ style: { backgroundColor: 'white', padding: '10px' },
|
|
|
|
|
+ attrs: { title: 'My UI' },
|
|
|
|
|
+ close: 'outside', // close when clicking outside
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### provideStyle - Inject CSS
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+logseq.provideStyle(`
|
|
|
|
|
+ .my-class {
|
|
|
|
|
+ color: var(--ls-primary-text-color);
|
|
|
|
|
+ background: var(--ls-primary-background-color);
|
|
|
|
|
+ }
|
|
|
|
|
+`)
|
|
|
|
|
+
|
|
|
|
|
+// Or with a key for updates
|
|
|
|
|
+logseq.provideStyle({
|
|
|
|
|
+ key: 'my-styles',
|
|
|
|
|
+ style: `.my-class { color: red; }`
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### provideModel - Event Handlers
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+logseq.provideModel({
|
|
|
|
|
+ myHandler(e) {
|
|
|
|
|
+ console.log('Clicked!', e.dataset)
|
|
|
|
|
+ },
|
|
|
|
|
+ async asyncHandler(e) {
|
|
|
|
|
+ const block = await logseq.Editor.getBlock(e.dataset.blockUuid)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Main UI (Full-screen Overlay)
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// Show plugin's main UI
|
|
|
|
|
+logseq.showMainUI({ autoFocus: true })
|
|
|
|
|
+
|
|
|
|
|
+// Hide it
|
|
|
|
|
+logseq.hideMainUI({ restoreEditingCursor: true })
|
|
|
|
|
+
|
|
|
|
|
+// Toggle
|
|
|
|
|
+logseq.toggleMainUI()
|
|
|
|
|
+
|
|
|
|
|
+// Style the main UI container
|
|
|
|
|
+logseq.setMainUIInlineStyle({
|
|
|
|
|
+ position: 'fixed',
|
|
|
|
|
+ top: '100px',
|
|
|
|
|
+ left: '50%',
|
|
|
|
|
+ zIndex: 11,
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Settings Schema
|
|
|
|
|
+
|
|
|
|
|
+Define user-configurable settings:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+logseq.useSettingsSchema([
|
|
|
|
|
+ {
|
|
|
|
|
+ key: 'apiKey',
|
|
|
|
|
+ type: 'string',
|
|
|
|
|
+ default: '',
|
|
|
|
|
+ title: 'API Key',
|
|
|
|
|
+ description: 'Enter your API key'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ key: 'enableFeature',
|
|
|
|
|
+ type: 'boolean',
|
|
|
|
|
+ default: true,
|
|
|
|
|
+ title: 'Enable Feature',
|
|
|
|
|
+ description: 'Toggle this feature on/off'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ key: 'theme',
|
|
|
|
|
+ type: 'enum',
|
|
|
|
|
+ enumChoices: ['light', 'dark', 'auto'],
|
|
|
|
|
+ enumPicker: 'select',
|
|
|
|
|
+ default: 'auto',
|
|
|
|
|
+ title: 'Theme',
|
|
|
|
|
+ description: 'Select theme mode'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ key: 'fontSize',
|
|
|
|
|
+ type: 'number',
|
|
|
|
|
+ default: 14,
|
|
|
|
|
+ title: 'Font Size',
|
|
|
|
|
+ description: 'Customize font size',
|
|
|
|
|
+ inputAs: 'range'
|
|
|
|
|
+ }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// Access settings
|
|
|
|
|
+const apiKey = logseq.settings?.apiKey
|
|
|
|
|
+
|
|
|
|
|
+// Update settings
|
|
|
|
|
+logseq.updateSettings({ apiKey: 'new-key' })
|
|
|
|
|
+
|
|
|
|
|
+// Listen for changes
|
|
|
|
|
+logseq.onSettingsChanged((newSettings, oldSettings) => {
|
|
|
|
|
+ console.log('Settings changed:', newSettings)
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Macro Renderer Pattern
|
|
|
|
|
+
|
|
|
|
|
+Create custom block renderers using `{{renderer : type, arg1, arg2}}`:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+logseq.App.onMacroRendererSlotted(({ slot, payload }) => {
|
|
|
|
|
+ const [type, ...args] = payload.arguments
|
|
|
|
|
+
|
|
|
|
|
+ if (type !== ': my-renderer') return
|
|
|
|
|
+
|
|
|
|
|
+ logseq.provideUI({
|
|
|
|
|
+ key: `my-renderer-${payload.uuid}`,
|
|
|
|
|
+ slot,
|
|
|
|
|
+ template: `
|
|
|
|
|
+ <div class="my-renderer">
|
|
|
|
|
+ <span>${args.join(', ')}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ `,
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// Register slash command to insert the macro
|
|
|
|
|
+logseq.Editor.registerSlashCommand('Insert My Renderer', async () => {
|
|
|
|
|
+ await logseq.Editor.insertAtEditingCursor(
|
|
|
|
|
+ `{{renderer :my-renderer, arg1, arg2}}`
|
|
|
|
|
+ )
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Key Types Reference
|
|
|
|
|
+
|
|
|
|
|
+### Block Entity
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+interface BlockEntity {
|
|
|
|
|
+ id: number // Database entity ID
|
|
|
|
|
+ uuid: string // Block UUID
|
|
|
|
|
+ title: string // Block content
|
|
|
|
|
+ content?: string // @deprecated, use title
|
|
|
|
|
+ format: 'markdown' | 'org'
|
|
|
|
|
+ parent: { id: number }
|
|
|
|
|
+ page: { id: number }
|
|
|
|
|
+ properties?: Record<string, any>
|
|
|
|
|
+ children?: BlockEntity[]
|
|
|
|
|
+ 'collapsed? ': boolean
|
|
|
|
|
+ createdAt: number
|
|
|
|
|
+ updatedAt: number
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Page Entity
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+interface PageEntity {
|
|
|
|
|
+ id: number
|
|
|
|
|
+ uuid: string
|
|
|
|
|
+ name: string
|
|
|
|
|
+ type: 'page' | 'journal' | 'whiteboard' | 'class' | 'property' | 'hidden'
|
|
|
|
|
+ format: 'markdown' | 'org'
|
|
|
|
|
+ 'journal? ': boolean
|
|
|
|
|
+ journalDay?: number // YYYYMMDD format for journals
|
|
|
|
|
+ properties?: Record<string, any>
|
|
|
|
|
+ createdAt: number
|
|
|
|
|
+ updatedAt: number
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Development Workflow
|
|
|
|
|
+
|
|
|
|
|
+### Setup
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# Clone sample repository
|
|
|
|
|
+git clone https://github.com/logseq/logseq-plugin-samples
|
|
|
|
|
+cd logseq-plugin-samples/logseq-slash-commands
|
|
|
|
|
+
|
|
|
|
|
+# Install dependencies
|
|
|
|
|
+npm install # or yarn
|
|
|
|
|
+
|
|
|
|
|
+# Build
|
|
|
|
|
+npm run build
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Loading in Logseq
|
|
|
|
|
+
|
|
|
|
|
+1. Open Logseq Desktop
|
|
|
|
|
+2. Enable **Developer mode** in Settings
|
|
|
|
|
+3. Press `t p` to open Plugins dashboard
|
|
|
|
|
+4. Click **Load unpacked plugin**
|
|
|
|
|
+5. Select your plugin directory
|
|
|
|
|
+
|
|
|
|
|
+### Debugging
|
|
|
|
|
+
|
|
|
|
|
+- Use browser DevTools (Ctrl/Cmd + Shift + I)
|
|
|
|
|
+- Check console for `@logseq/libs` debug output
|
|
|
|
|
+- Use `logseq.UI.showMsg()` for quick debugging
|
|
|
|
|
+
|
|
|
|
|
+## Best Practices
|
|
|
|
|
+
|
|
|
|
|
+### Performance
|
|
|
|
|
+
|
|
|
|
|
+1. **Debounce frequent operations** - especially DB queries and UI updates
|
|
|
|
|
+2. **Use `onBlockChanged` sparingly** - it fires on every edit
|
|
|
|
|
+3. **Batch block operations** - use `insertBatchBlock` instead of multiple `insertBlock` calls
|
|
|
|
|
+4. **Clean up listeners** - store and call off-hooks in `beforeunload`
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const offHooks: (() => void)[] = []
|
|
|
|
|
+
|
|
|
|
|
+offHooks.push(
|
|
|
|
|
+ logseq.DB.onChanged(() => { /* ... */ })
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+logseq.beforeunload(async () => {
|
|
|
|
|
+ offHooks.forEach(off => off())
|
|
|
|
|
+})
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### Error Handling
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+try {
|
|
|
|
|
+ const block = await logseq.Editor.getBlock(uuid)
|
|
|
|
|
+ if (!block) {
|
|
|
|
|
+ await logseq.UI.showMsg('Block not found', 'warning')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ // ... proceed
|
|
|
|
|
+} catch (error) {
|
|
|
|
|
+ console.error('Plugin error:', error)
|
|
|
|
|
+ await logseq.UI.showMsg('An error occurred', 'error')
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### CSS Variables
|
|
|
|
|
+
|
|
|
|
|
+Use Logseq's CSS custom properties for consistent theming:
|
|
|
|
|
+
|
|
|
|
|
+```css
|
|
|
|
|
+.my-plugin-ui {
|
|
|
|
|
+ color: var(--ls-primary-text-color);
|
|
|
|
|
+ background: var(--ls-primary-background-color);
|
|
|
|
|
+ border: 1px solid var(--ls-border-color);
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### DB vs File Graphs
|
|
|
|
|
+
|
|
|
|
|
+Check graph type before using DB-specific features:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const isDbGraph = await logseq.App.checkCurrentIsDbGraph()
|
|
|
|
|
+if (isDbGraph) {
|
|
|
|
|
+ // Use DB-specific APIs
|
|
|
|
|
+ await logseq.Editor.upsertProperty('my-prop', { type: 'string' })
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## Resources
|
|
|
|
|
+
|
|
|
|
|
+- **API Documentation**: https://plugins-doc.logseq.com
|
|
|
|
|
+- **Plugin Samples**: https://github.com/logseq/logseq-plugin-samples
|
|
|
|
|
+- **SDK Source**: https://github.com/logseq/logseq/tree/master/libs
|
|
|
|
|
+- **NPM Package**: https://www.npmjs.com/package/@logseq/libs
|
|
|
|
|
+- **Discord Community**: https://discord.gg/KpN4eHY
|
|
|
|
|
+- **Community Templates**:
|
|
|
|
|
+ - ClojureScript: https://github.com/logseq/cljs-plugin-example
|
|
|
|
|
+ - TypeScript: https://github.com/YU000jp/logseq-plugin-sample-kit-typescript
|
|
|
|
|
+
|
|
|
|
|
+## Version Compatibility
|
|
|
|
|
+
|
|
|
|
|
+Always check the minimum Logseq version required for specific APIs. Keep `@logseq/libs` updated to the latest version
|
|
|
|
|
+for best compatibility and performance.
|
|
|
|
|
+
|
|
|
|
|
+```bash
|
|
|
|
|
+# Check current version
|
|
|
|
|
+npm info @logseq/libs version
|
|
|
|
|
+
|
|
|
|
|
+# Update
|
|
|
|
|
+npm update @logseq/libs
|
|
|
|
|
+```
|