Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

628
libs/guides/AGENTS.md Normal file
View File

@@ -0,0 +1,628 @@
# 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
Only supported in file-based graphs for desktop Logseq.
```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
pnpm install
# Build
pnpm 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
```

View File

@@ -0,0 +1,400 @@
# `logseq.Commands` API Guide
## Quick start
```ts
import '@logseq/libs'
async function main() {
const unregister = logseq.Commands.register(
'insert-hello',
{
title: 'Insert hello',
placement: 'slash',
},
async () => {
await logseq.Editor.insertAtEditingCursor('Hello from command!')
}
)
logseq.beforeunload(async () => {
unregister && unregister()
})
}
logseq.ready(main).catch(console.error)
```
## API surface
### `Commands.register(id, options, action?)`
Register a command and return an unregister handler.
```ts
const unregister = logseq.Commands.register(id, options, action)
```
The same command can also put its handler in `options.handler`:
```ts
const unregister = logseq.Commands.register('my-command', {
title: 'My command',
placement: 'palette',
handler: async () => {
await logseq.UI.showMsg('Run')
},
})
```
Return value:
- `() => void`: unregister handler.
- `false`: registration failed, usually because handler/action is missing or invalid.
The unregister handler is idempotent. Calling it multiple times is safe.
```ts
const unregister = logseq.Commands.register('temporary-command', {
title: 'Temporary command',
placement: 'palette',
handler: () => console.log('run'),
})
unregister && unregister()
unregister && unregister() // safe no-op
```
### `Commands.execute(id, ...args)`
Execute a built-in or plugin command.
```ts
await logseq.Commands.execute('logseq.go/home')
await logseq.Commands.execute('my-plugin/some-command')
await logseq.Commands.execute('my-plugin.commands.some-command')
await logseq.Commands.execute('local-command')
```
Supported id forms:
| Form | Meaning |
| ------------------------ | ----------------------------------------------------------- |
| `logseq.*` | Built-in command, forwarded to `App.invokeExternalCommand`. |
| `plugin-id/key` | Command registered by another plugin. |
| `plugin-id.commands.key` | Backward-compatible plugin command address. |
| `key` | Command registered by the current plugin. |
Plugin command keys are normalized to match host registration rules:
- trim whitespace;
- whitespace becomes `_`;
- lower-case;
- `:` becomes `-`;
- numeric first character becomes `_1`, `_2`, etc.
## Register options
```ts
type CommandRegisterOptions = {
key?: string
title?: string
label?: string
desc?: string
handler?: CommandCallback | BlockCommandCallback | Array<SlashCommandAction>
when?: string | string[]
placement?: CommandPlacement
placements?: CommandPlacement[]
keybinding?: SimpleCommandKeybinding | string
extras?: Record<string, any>
type?: string
palette?: boolean
}
```
Important fields:
- `key`: stable command key. Defaults to `id`.
- `title`: display title. `label` is also accepted for compatibility.
- `desc`: command palette description. Defaults to title/label in host palette rendering.
- `handler`: command callback. Can be passed here or as third argument.
- `placement`: single UI surface.
- `placements`: multiple UI surfaces for one logical command.
- `keybinding`: string or `{ mode, binding, mac }` shortcut descriptor.
- `when`: v1 stores this as metadata for future host-side condition evaluation.
- `extras`: placement-specific metadata, e.g. PDF highlight menu options.
## Placements
```ts
type CommandPlacement =
| 'palette'
| 'shortcut'
| 'slash'
| 'block-context-menu'
| 'highlight-context-menu'
| 'page-menu'
| 'simple'
```
### `palette`
Register a command palette item.
```ts
const unregister = logseq.Commands.register(
'open-dashboard',
{
title: 'Open dashboard',
placement: 'palette',
},
async () => {
logseq.showMainUI()
}
)
```
### `shortcut`
Register a keyboard shortcut without necessarily showing it in the command palette.
```ts
const unregister = logseq.Commands.register(
'toggle-panel',
{
title: 'Toggle panel',
placement: 'shortcut',
keybinding: {
mode: 'global',
binding: 'mod+shift+p',
mac: 'cmd+shift+p',
},
},
() => {
logseq.toggleMainUI()
}
)
```
Shortcut modes:
| Mode | Meaning |
| ------------- | -------------------------------- |
| `global` | Active globally. |
| `non-editing` | Active when not editing a block. |
| `editing` | Active when editing a block. |
### `slash`
Register a slash command.
```ts
const unregister = logseq.Commands.register(
'insert-current-date',
{
title: 'Insert current date',
placement: 'slash',
},
async () => {
await logseq.Editor.insertAtEditingCursor(
new Date().toISOString().slice(0, 10)
)
}
)
```
Slash commands also support the legacy action-step form:
```ts
const unregister = logseq.Commands.register(
'big-bang',
{ title: 'Big Bang', placement: 'slash' },
[['editor/input', 'Hello'], ['editor/clear-current-slash']]
)
```
### `block-context-menu`
Register a block context menu item.
```ts
const unregister = logseq.Commands.register(
'copy-block-uuid',
{
title: 'Copy block UUID',
placement: 'block-context-menu',
},
async ({ uuid }) => {
await navigator.clipboard.writeText(uuid)
}
)
```
### `page-menu`
Register a page menu item.
```ts
const unregister = logseq.Commands.register(
'analyze-page',
{
title: 'Analyze page',
placement: 'page-menu',
},
async ({ page }) => {
console.log('Analyze page', page)
}
)
```
### `highlight-context-menu`
Register a PDF highlight context menu item.
```ts
const unregister = logseq.Commands.register(
'send-highlight-to-inbox',
{
title: 'Send highlight to inbox',
placement: 'highlight-context-menu',
extras: { clearSelection: true },
},
async ({ content }) => {
await logseq.Editor.appendBlockInPage('Inbox', content)
}
)
```
### `simple`
Register a command without visible UI placement. This is useful when the command is intended to be executed by another plugin through `Commands.execute`.
```ts
const unregister = logseq.Commands.register(
'sync-now',
{
title: 'Sync now',
placement: 'simple',
},
async () => {
await syncNow()
}
)
```
## Multiple placements
Use `placements` when one logical command should appear in multiple places.
```ts
const unregister = logseq.Commands.register(
'toggle-focus-mode',
{
title: 'Toggle focus mode',
placements: ['palette', 'shortcut'],
keybinding: 'mod+shift+f',
},
() => {
toggleFocusMode()
}
)
// Removes both palette and shortcut registrations.
unregister && unregister()
```
## Dynamic registration patterns
### Enable or disable a command from settings
```ts
let unregisterExport: (() => void) | false | undefined
function registerExportCommand() {
unregisterExport = logseq.Commands.register(
'export-current-page',
{ title: 'Export current page', placement: 'palette' },
exportCurrentPage
)
}
function unregisterExportCommand() {
unregisterExport && unregisterExport()
unregisterExport = undefined
}
async function main() {
if (logseq.settings?.enableExport) registerExportCommand()
logseq.onSettingsChanged((next) => {
unregisterExportCommand()
if (next.enableExport) registerExportCommand()
})
logseq.beforeunload(async () => {
unregisterExportCommand()
})
}
```
### Register temporary commands
```ts
const unregister = logseq.Commands.register(
'temporary-review-action',
{ title: 'Approve review item', placement: 'palette' },
approveCurrentReviewItem
)
setTimeout(() => {
unregister && unregister()
}, 60_000)
```
## Compatibility with existing APIs
Old APIs remain supported. Internally, they share the same command registration path as v1 `logseq.Commands`.
| Existing API | Commands equivalent |
| -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `App.registerCommandPalette({ key, label }, action)` | `Commands.register(key, { title: label, placement: 'palette' }, action)` |
| `App.registerCommandShortcut(keybinding, action, opts)` | `Commands.register(opts.key, { ...opts, placement: 'shortcut', keybinding }, action)` |
| `Editor.registerSlashCommand(tag, action)` | `Commands.register(tag, { title: tag, placement: 'slash' }, action)` |
| `Editor.registerBlockContextMenuItem(label, action)` | `Commands.register(key, { title: label, placement: 'block-context-menu' }, action)` |
| `Editor.registerHighlightContextMenuItem(label, action, opts)` | `Commands.register(key, { title: label, placement: 'highlight-context-menu', extras: opts }, action)` |
| `App.registerPageMenuItem(tag, action)` | `Commands.register(key, { title: tag, placement: 'page-menu' }, action)` |
These old APIs also return unregister handlers after v1.
## Cleanup semantics
Commands are cleaned in two ways:
1. **Manual unregister**: call the function returned by `register`.
2. **Plugin lifecycle cleanup**: when the plugin is disabled, reloaded, unloaded, or uninstalled, the host removes all commands registered by that plugin.
Manual unregister removes:
- local SDK hook listener;
- simple command registry entry;
- command palette entry;
- shortcut registry entry;
- slash command entry, if any.
Plugin lifecycle cleanup removes all commands by plugin id, including palette-only commands without shortcuts.
## v1 limitations
- `when` is metadata only. The host does not yet evaluate VSCode-like context expressions such as `editor.inCodeBlock` or `graph.isDb`.
- `Commands.onDidExecute(listener)` is not implemented in v1.
- Command collision handling still follows existing host behavior. Prefer stable, plugin-scoped keys.
- `Commands.execute` works with registered plugin commands and built-in `logseq.*` commands; it is not a general RPC replacement for arbitrary plugin model methods.
## Best practices
- Use a stable `key` for commands that other plugins may call.
- Keep `title` user-facing and concise.
- Store unregister handlers for dynamic commands and call them before re-registering.
- Always clean up dynamic registrations in `logseq.beforeunload`.
- Prefer `placements` over registering the same handler several times manually.
- Use `placement: 'simple'` for plugin-to-plugin automation commands that should not appear in UI.

View File

@@ -0,0 +1,594 @@
# Custom Theme Plugin Guide
This guide summarizes Logseq's current UI theme architecture and gives a practical workflow for building a Logseq **theme plugin** with `@logseq/libs`.
It is based on the current repository structure and these theme-related entry points:
- CSS load order: `tailwind.all.css`
- Design tokens and theme variables: `src/main/frontend/shui/radix.css`, `src/main/frontend/shui/radix-hsl.css`, `src/main/frontend/shui/vars-classic.css`, `src/main/frontend/shui/colors.css`
- Shared shui/Base UI component CSS: `resources/css/shui.css`
- CodeMirror theme bridge: `resources/css/codemirror.lsradix.css`
- Frontend component CSS: `src/main/frontend/**/*.css`
- Theme runtime attributes: `src/main/frontend/components/theme.cljs`, `src/main/frontend/state.cljs`, `src/main/frontend/ui.cljs`
- Plugin theme registration/injection: `libs/src/LSPlugin.ts`, `libs/src/LSPlugin.core.ts`, `libs/src/LSPlugin.user.ts`, `libs/src/common.ts`
## Mental model
Logseq themes are mostly CSS-variable based. A theme plugin should first override the semantic variables that the app already consumes, and only then add targeted selector overrides for areas that are not fully tokenized.
### CSS loading order
The main stylesheet imports theme foundations before app component CSS:
1. `src/main/frontend/shui/radix.css` — color scales as `--rx-*` variables, for example `--rx-gray-01` through `--rx-gray-12` and alpha variants.
2. `src/main/frontend/shui/radix-hsl.css` — HSL forms such as `--rx-gray-01-hsl` for Tailwind/shui tokens.
3. `src/main/frontend/shui/vars-classic.css` — Logseq semantic variables, layout variables, default light/dark values for `data-color=logseq`.
4. `src/main/frontend/shui/colors.css` — accent palettes selected by `html[data-color=...]`, mapping `--lx-accent-*`, `--lx-gray-*`, shui tokens, and many `--ls-*` values.
5. `src/main/frontend/shui/index.css` — Tailwind base.
6. `resources/css/shui.css` — shared UI components built on shui/Base UI/Tailwind tokens.
7. Third-party CSS: Inter, PhotoSwipe, KaTeX, CodeMirror, PDF.js, Tabler, `codemirror.lsradix.css`.
8. `src/main/frontend/**/[!_]*.css` — component and extension CSS.
9. A selected plugin theme is injected later as a `<link rel="stylesheet">`, so it can override earlier rules when specificity is equal or higher.
### Runtime attributes and classes
Theme CSS should scope by attributes on `html` and compatibility classes on `body`:
| Hook | Set by | Meaning |
| --- | --- | --- |
| `html[data-theme="light"]` / `html[data-theme="dark"]` | `frontend.components.theme/container` | Current light/dark mode. Prefer this for theme CSS. |
| `html.dark` | same component | Tailwind dark-mode hook. |
| `body.light-theme`, `body.white-theme`, `body.dark-theme` | same component | Backward-compatible hooks for older custom CSS/themes. |
| `html[data-color="logseq"]` and other colors | same component | Accent color selection. A theme plugin can override or ignore this. |
| `html[data-font="serif"|"mono"]`, `html[data-font-global="true"]` | same component | Editor/global font preferences. Avoid fighting these unless your theme intentionally owns typography. |
| Platform classes such as `html.is-mobile`, `html.is-electron`, `html.is-mac`, `html.is-native-iphone` | `frontend.ui/inject-document-devices-envs!` | Useful for responsive/mobile-safe fixes. |
### Plugin theme lifecycle
A theme can be registered in either of two ways:
1. Declaratively in `package.json > logseq.themes`. During package preparation, Logseq resolves relative `url` values to plugin resources, registers each theme, and shows them in the theme selector.
2. Programmatically with `logseq.provideTheme(theme)`. This is useful for generated themes or settings-driven variants.
When a user selects a theme, Logseq:
- removes the previously injected custom theme link;
- injects the selected theme CSS link into `document.head`;
- persists the selected theme separately for `light` and `dark` modes;
- updates `:plugin/selected-theme` and emits plugin app hooks such as `:theme-changed`.
## Minimal theme plugin
### Directory layout
```text
logseq-my-theme/
├── package.json
├── index.html
└── themes/
├── my-theme-light.css
└── my-theme-dark.css
```
### `package.json`
Use a stable lowercase-kebab `logseq.id`. For a theme-only plugin, set `effect: true` and declare each CSS file in `themes`.
```json
{
"name": "logseq-my-theme",
"version": "0.1.0",
"description": "A custom Logseq theme pack",
"license": "MIT",
"main": "index.html",
"logseq": {
"id": "my-theme",
"main": "index.html",
"title": "My Theme",
"icon": "./icon.png",
"effect": true,
"themes": [
{
"name": "My Theme Light",
"url": "./themes/my-theme-light.css",
"mode": "light",
"description": "Light variant of My Theme"
},
{
"name": "My Theme Dark",
"url": "./themes/my-theme-dark.css",
"mode": "dark",
"description": "Dark variant of My Theme"
}
]
},
"devDependencies": {
"@logseq/libs": "^0.0.17"
}
}
```
### `index.html`
Declarative themes do not need runtime code, but keeping a tiny entry file makes the plugin package explicit and easy to extend later.
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Logseq Theme</title>
</head>
<body></body>
</html>
```
### Programmatic variant with `provideTheme`
If the theme URL or metadata is generated at runtime, use the SDK. Always wait for Logseq to be ready.
```ts
import '@logseq/libs'
async function main() {
logseq.provideTheme({
name: 'My Generated Dark Theme',
url: './themes/generated-dark.css',
mode: 'dark',
description: 'Generated from plugin settings',
})
}
logseq.ready(main).catch(console.error)
```
## CSS strategy
Start with variables. Add selector overrides only for UI that cannot be changed via variables.
### Dark theme starter
```css
/* themes/my-theme-dark.css */
html[data-theme="dark"] {
color-scheme: dark;
/* Core backgrounds */
--ls-primary-background-color: #111827;
--ls-secondary-background-color: #172033;
--ls-tertiary-background-color: #202b42;
--ls-quaternary-background-color: #2a3650;
--ls-table-tr-even-background-color: var(--ls-secondary-background-color);
--ls-slide-background-color: var(--ls-primary-background-color);
/* Text */
--ls-primary-text-color: #d6deeb;
--ls-secondary-text-color: #eef2ff;
--ls-title-text-color: #f8fafc;
--ls-left-sidebar-text-color: #cbd5e1;
/* Links and refs */
--ls-link-text-color: #7dd3fc;
--ls-link-text-hover-color: #bae6fd;
--ls-link-ref-text-color: var(--ls-link-text-color);
--ls-link-ref-text-hover-color: var(--ls-link-text-hover-color);
--ls-block-ref-link-text-color: #38bdf8;
--ls-tag-text-color: #93c5fd;
--ls-tag-text-hover-color: #bfdbfe;
/* Borders, guidelines, focus */
--ls-border-color: #334155;
--ls-secondary-border-color: #475569;
--ls-tertiary-border-color: rgb(148 163 184 / 0.18);
--ls-guideline-color: rgb(148 163 184 / 0.18);
--ls-focus-ring-color: rgb(56 189 248 / 0.45);
/* Blocks and properties */
--ls-block-properties-background-color: #1e293b;
--ls-page-properties-background-color: #1e293b;
--ls-block-bullet-color: #64748b;
--ls-block-bullet-border-color: #475569;
--ls-block-highlight-color: rgb(14 165 233 / 0.22);
--ls-a-chosen-bg: rgb(56 189 248 / 0.16);
--ls-menu-hover-color: var(--ls-a-chosen-bg);
/* Selection, checkbox, quote, mark, inline code */
--ls-selection-background-color: rgb(56 189 248 / 0.32);
--ls-selection-text-color: #f8fafc;
--ls-page-checkbox-color: #64748b;
--ls-page-checkbox-border-color: #475569;
--ls-page-blockquote-color: var(--ls-primary-text-color);
--ls-page-blockquote-bg-color: #172033;
--ls-page-blockquote-border-color: #38bdf8;
--ls-page-mark-color: #111827;
--ls-page-mark-bg-color: #fde68a;
--ls-page-inline-code-color: #e0f2fe;
--ls-page-inline-code-bg-color: #0f172a;
/* Scrollbars and notifications */
--ls-scrollbar-foreground-color: rgb(148 163 184 / 0.35);
--ls-scrollbar-background-color: rgb(15 23 42 / 0.35);
--ls-scrollbar-thumb-hover-color: rgb(148 163 184 / 0.55);
--ls-notification-background: #1e293b;
--ls-notification-text-color: #f8fafc;
/* shui/Tailwind HSL tokens: use space-separated HSL channels, not hsl(). */
--background: 222 47% 11%;
--foreground: 210 40% 96%;
--card: 222 47% 13%;
--card-foreground: 210 40% 96%;
--popover: 222 47% 10%;
--popover-foreground: 210 40% 96%;
--primary: 199 89% 48%;
--primary-foreground: 210 40% 98%;
--secondary: 217 33% 18%;
--secondary-foreground: 210 40% 96%;
--muted: 217 33% 16%;
--border: 217 33% 24%;
--input: 217 33% 24%;
--ring: 199 89% 48%;
--accent: 199 89% 48%;
--accent-foreground: 210 40% 98%;
}
html[data-theme="dark"] body {
background: var(--ls-primary-background-color);
color: var(--ls-primary-text-color);
}
html[data-theme="dark"] ::selection {
background: var(--ls-selection-background-color);
color: var(--ls-selection-text-color);
}
```
### Light theme starter
```css
/* themes/my-theme-light.css */
html[data-theme="light"] {
color-scheme: light;
--ls-primary-background-color: #ffffff;
--ls-secondary-background-color: #f8fafc;
--ls-tertiary-background-color: #eef2f7;
--ls-quaternary-background-color: #e2e8f0;
--ls-primary-text-color: #1e293b;
--ls-secondary-text-color: #0f172a;
--ls-title-text-color: #0f172a;
--ls-left-sidebar-text-color: #334155;
--ls-link-text-color: #0369a1;
--ls-link-text-hover-color: #075985;
--ls-link-ref-text-color: var(--ls-link-text-color);
--ls-link-ref-text-hover-color: var(--ls-link-text-hover-color);
--ls-block-ref-link-text-color: #0284c7;
--ls-tag-text-color: #0369a1;
--ls-tag-text-hover-color: #075985;
--ls-border-color: #cbd5e1;
--ls-secondary-border-color: #e2e8f0;
--ls-tertiary-border-color: rgb(15 23 42 / 0.08);
--ls-guideline-color: rgb(15 23 42 / 0.08);
--ls-focus-ring-color: rgb(14 165 233 / 0.35);
--ls-block-properties-background-color: #f1f5f9;
--ls-page-properties-background-color: #f1f5f9;
--ls-block-bullet-color: #94a3b8;
--ls-block-bullet-border-color: #cbd5e1;
--ls-block-highlight-color: #e0f2fe;
--ls-a-chosen-bg: #e0f2fe;
--ls-menu-hover-color: var(--ls-a-chosen-bg);
--ls-selection-background-color: #dbeafe;
--ls-selection-text-color: #0f172a;
--ls-page-checkbox-color: #94a3b8;
--ls-page-checkbox-border-color: #94a3b8;
--ls-page-blockquote-color: var(--ls-primary-text-color);
--ls-page-blockquote-bg-color: #f8fafc;
--ls-page-blockquote-border-color: #38bdf8;
--ls-page-mark-color: #0f172a;
--ls-page-mark-bg-color: #fef3c7;
--ls-page-inline-code-color: #0f172a;
--ls-page-inline-code-bg-color: #f1f5f9;
--ls-scrollbar-foreground-color: rgb(15 23 42 / 0.12);
--ls-scrollbar-background-color: rgb(15 23 42 / 0.05);
--ls-scrollbar-thumb-hover-color: rgb(15 23 42 / 0.22);
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 222 47% 11%;
--popover: 0 0% 100%;
--popover-foreground: 222 47% 11%;
--primary: 199 89% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 199 89% 40%;
--accent: 199 89% 40%;
--accent-foreground: 0 0% 100%;
}
```
## Important variable groups
Prefer these variables before reaching for selectors.
### Global layout and typography
| Variable | Purpose |
| --- | --- |
| `--ls-page-text-size` | Base page text size under `#root`. |
| `--ls-page-title-size` | Page title size. Mobile may override title layout. |
| `--ls-main-content-max-width`, `--ls-main-content-max-width-wide` | Main content width. |
| `--ls-font-family` | Global app font when not overridden by user font settings. |
| `--ls-scrollbar-width` | Custom scrollbar width. |
| `--ls-border-radius-low`, `--ls-border-radius-medium` | Classic radius tokens. |
| `--ls-headbar-height`, `--ls-headbar-inner-top-padding` | Header sizing. |
| `--ls-left-sidebar-width`, `--ls-left-sidebar-sm-width`, `--ls-left-sidebar-nav-btn-size` | Left sidebar sizing. |
### Logseq semantic colors
| Variable | Purpose |
| --- | --- |
| `--ls-primary-background-color` | Main background. |
| `--ls-secondary-background-color` | Secondary surfaces, editors, menus. |
| `--ls-tertiary-background-color` | Nested/raised surfaces. |
| `--ls-quaternary-background-color` | Active/hover surfaces. |
| `--ls-primary-text-color`, `--ls-secondary-text-color`, `--ls-title-text-color` | Body, stronger text, titles. |
| `--ls-border-color`, `--ls-secondary-border-color`, `--ls-tertiary-border-color` | Borders. |
| `--ls-guideline-color` | Block indentation guide lines. |
| `--ls-active-primary-color`, `--ls-active-secondary-color` | Active states. |
| `--ls-a-chosen-bg`, `--ls-menu-hover-color` | Chosen menu/list item backgrounds. |
| `--ls-focus-ring-color` | Focus rings. |
### Links, references, tags, and blocks
| Variable | Purpose |
| --- | --- |
| `--ls-link-text-color`, `--ls-link-text-hover-color` | General links. |
| `--ls-link-ref-text-color`, `--ls-link-ref-text-hover-color` | Page references. |
| `--ls-block-ref-link-text-color` | Block references. |
| `--ls-tag-text-color`, `--ls-tag-text-hover-color`, `--ls-tag-text-opacity`, `--ls-tag-text-hover-opacity` | Tags. |
| `--ls-block-bullet-color`, `--ls-block-bullet-border-color`, `--ls-block-bullet-active-color` | Block bullets. |
| `--ls-block-highlight-color` | Block highlight. |
| `--ls-block-properties-background-color`, `--ls-page-properties-background-color` | Property panels. |
### Content tokens
| Variable | Purpose |
| --- | --- |
| `--ls-selection-background-color`, `--ls-selection-text-color` | Text selection. |
| `--ls-page-checkbox-color`, `--ls-page-checkbox-border-color` | Markdown task checkboxes/radios. |
| `--ls-page-blockquote-color`, `--ls-page-blockquote-bg-color`, `--ls-page-blockquote-border-color` | Blockquotes. |
| `--ls-page-mark-color`, `--ls-page-mark-bg-color` | Highlight/mark text. |
| `--ls-page-inline-code-color`, `--ls-page-inline-code-bg-color` | Inline code. |
| `--ls-table-tr-even-background-color` | Alternating table rows. |
| `--ls-cloze-text-color` | Cloze text. |
| `--ls-slide-background-color` | Slide mode background. |
### shui/Tailwind HSL tokens
Newer shared UI components use HSL channel tokens, often through Tailwind classes such as `bg-background`, `bg-popover`, `border`, `text-foreground`, and `bg-primary`.
Set these as space-separated HSL channels:
```css
html[data-theme="dark"] {
--background: 222 47% 11%;
--foreground: 210 40% 96%;
--card: 222 47% 13%;
--card-foreground: 210 40% 96%;
--popover: 222 47% 10%;
--popover-foreground: 210 40% 96%;
--primary: 199 89% 48%;
--primary-foreground: 210 40% 98%;
--secondary: 217 33% 18%;
--secondary-foreground: 210 40% 96%;
--muted: 217 33% 16%;
--border: 217 33% 24%;
--input: 217 33% 24%;
--ring: 199 89% 48%;
--accent: 199 89% 48%;
--accent-foreground: 210 40% 98%;
}
```
Do not write `--primary: hsl(199 89% 48%)`; consumers call `hsl(var(--primary))`.
### `--lx-*` and `--rx-*` color scales
- `--rx-*` variables are Radix-style raw palettes. They are broad and already available.
- `--lx-gray-01` through `--lx-gray-12` and `--lx-gray-*-alpha` represent the active neutral scale.
- `--lx-accent-01` through `--lx-accent-12` and `--lx-accent-*-alpha` represent the active accent scale.
If your theme is a complete palette, map `--lx-gray-*` and `--lx-accent-*` as well as `--ls-*`. This makes Radix/shui-heavy UI more consistent:
```css
html[data-theme="dark"] {
--lx-gray-01: #0f172a;
--lx-gray-02: #111827;
--lx-gray-03: #1e293b;
--lx-gray-04: #273449;
--lx-gray-05: #334155;
--lx-gray-06: #475569;
--lx-gray-07: #64748b;
--lx-gray-08: #94a3b8;
--lx-gray-09: #cbd5e1;
--lx-gray-10: #dbeafe;
--lx-gray-11: #e2e8f0;
--lx-gray-12: #f8fafc;
--lx-accent-09: #0ea5e9;
--lx-accent-10: #0284c7;
--lx-accent-11: #38bdf8;
--lx-accent-12: #e0f2fe;
}
```
## UI selector map
Use this as a last-mile map after variables. Keep overrides narrow and prefer `:where(...)` to avoid specificity wars.
### App shell and layout
| Area | Useful selectors | Notes |
| --- | --- | --- |
| Root/app | `#root`, `#app-container`, `#root-container.theme-container`, `main.theme-container-inner` | `#root` uses `--ls-page-text-size`; `main.theme-container-inner` defines `--left-sidebar-bg-color`. |
| Main content | `#main-container`, `#main-content`, `#main-content-container`, `.page-blocks-inner` | Avoid hard-coded viewport hacks; test desktop and mobile. |
| Header | `.cp__header`, `.head`, `.button`, `.ui__button` | Mostly tokenized through shui/Tailwind variables. |
| Left sidebar | `.left-sidebar-inner`, `.sidebar-header-container`, `.sidebar-contents-container`, `.sidebar-content-group`, `.sidebar-navigations` | Prefer `--left-sidebar-bg-color`, `--ls-left-sidebar-text-color`, and `--ls-left-sidebar-width`. |
| Right sidebar | `#right-sidebar`, `.cp__right-sidebar-inner`, `.sidebar-item`, `.references-blocks-item` | Nested reference cards may need extra contrast. |
### Pages, blocks, and editor
| Area | Useful selectors | Notes |
| --- | --- | --- |
| Page title | `.ls-page-title`, `.page-title`, `.journal-title`, `.page-title-sizer-wrapper` | Prefer `--ls-page-title-size` and title text variables. |
| Blocks | `.ls-block`, `.block-main-container`, `.block-content-wrapper`, `.block-content`, `.block-body` | Avoid altering core layout unless necessary. |
| Block tree guides | `.block-children-container`, `.block-children`, `.block-children-left-border` | Governed by `--ls-guideline-color` and related border variables. |
| Bullets/control | `.block-control-wrap`, `.block-control`, `.bullet-container`, `.bullet-link-wrap` | Governed by bullet variables; keep hit areas accessible. |
| Page refs | `.page-ref`, `.page-reference`, `.breadcrumb` | Use link/reference variables first. |
| Block refs | `.block-ref`, `.block-ref-no-title`, `.open-block-ref-link` | Use `--ls-block-ref-link-text-color`, property surface variables. |
| Properties | `.block-properties`, `.page-properties`, `.property-value-inner`, `.property-key` | Use property background variables. |
| Editor textarea | `.editor-inner textarea`, `.edit-input`, `.non-block-editor textarea` | Background usually comes from `--ls-secondary-background-color`. |
| Autocomplete/slash popup | `#ui__ac-inner`, `.menu-link`, `.absolute-modal[data-modal-name]`, `.cp__commands-slash` | Use menu hover and popover tokens. |
### Shared UI, dialogs, command palette
| Area | Useful selectors | Notes |
| --- | --- | --- |
| Buttons | `.ui__button`, `.button`, `.ui__toggle`, `.ui__toggle-background-on`, `.ui__toggle-background-off` | Set `--primary`, `--primary-foreground`, `--accent`, `--ring`. |
| Dialogs/modals | `.ui__modal`, `.ui__modal-panel`, `.ui__dialog-overlay`, `.ui__dialog-content`, `.ui__alert-dialog-content` | `--ls-modal-overlay-gradient-start/end` can control modal overlays. |
| Dropdowns/popovers | `.ui__dropdown-menu-content`, `.ui__popover-content`, `div[data-radix-menu-content]`, `div[data-radix-popper-content-wrapper]` | Usually driven by `--popover`, `--border`, `--accent`, `--lx-popover-bg`. |
| Select/calendar | `.ui__select-content`, `.ui__calendar`, `.rc-datepicker` | shui tokens and `--accent` matter. |
| Notifications | `.ui__notifications .notification-area` | Use `--ls-notification-background`, `--ls-notification-text-color`. |
| Command palette | `.cp__cmdk` | Border and hint colors use `--ls-border-color`, `--lx-gray-*`, `--accent`. |
| Context menu | `#custom-context-menu`, `.ls-context-menu-content`, `.menu-links-wrapper` | Popover/menu variables usually cover it. |
### Extensions and special surfaces
| Area | Useful selectors/variables | Notes |
| --- | --- | --- |
| CodeMirror | `.cm-s-lsradix`, `.cm-s-lsradix.cm-s-dark`, `.cm-s-lsradix.cm-s-light`, `.CodeMirror-*`, `.cm-*` | `resources/css/codemirror.lsradix.css` already bridges many `--lx-*`/`--ls-*` variables. Override only syntax colors if needed. |
| PDF | `.extensions__pdf-container`, `.extensions__pdf-toolbar`, `.extensions__pdf-outline`, `--ph-highlight-color-*`, `--ph-link-color`, `--ph-view-container-width`, `--lx-pdf-container-dark-bg` | PDF highlight colors are `--ph-*`, not `--ls-highlight-color-*`. |
| Graph | `.graph-layout`, `.graph-filters`, graph extension CSS under `src/main/frontend/extensions/graph.css` | Some graph styles are canvas/SVG-driven; inspect DOM before overriding. |
| Whiteboard/tldraw | `.tl-container`, `.tl-button`, `--ls-wb-stroke-color-default`, `--ls-wb-background-color-default`, `--ls-wb-text-color-default` | Accent colors in `colors.css` set default whiteboard tokens. |
| Tables | `.table-wrapper`, `.table-auto`, table rows | Start with `--ls-table-tr-even-background-color`, borders, text tokens. |
| Cards/flashcards | `.ls-card`, `.ui__dialog-content[label=flashcards__cp]` | Check dialog contrast and card minimum sizes. |
| Plugins UI | `.cp__plugins-page`, `.cp__themes-installed` | Useful when styling the installed theme picker itself. |
## Scoped overrides: examples
### Keep overrides mode-specific
```text
html[data-theme="dark"] :where(.cp__right-sidebar-inner .references-blocks-item) {
background: color-mix(in srgb, var(--ls-secondary-background-color) 88%, white 12%);
}
```
### Prefer low specificity for optional polish
```text
html[data-theme="light"] :where(.page-reference:hover) {
background: rgb(14 165 233 / 0.12);
}
```
### Avoid global resets
Avoid rules like this in a theme plugin:
```css
/* Avoid */
* {
transition: all 200ms ease;
}
```
They can slow editing, affect CodeMirror/PDF.js, and break subtle interaction states.
## Mobile and desktop considerations
- Test `html.is-mobile`, `html.is-native-iphone`, `html.is-native-android`, and `html.is-electron` layouts if your theme changes spacing or header/sidebar dimensions.
- Respect safe-area insets in mobile-specific layout. Existing CSS uses `env(safe-area-inset-*)` in some places.
- Avoid changing `-webkit-app-region` on Electron headers/PDF toolbars unless you are fixing a drag-region issue.
- Do not shrink block control or bullet hit areas below the existing touch-friendly sizes.
- Avoid fixed pixel widths for the main content unless they are variables such as `--ls-main-content-max-width`.
## Assets and fonts
Prefer local assets packaged with the plugin. Relative URLs inside the selected theme CSS are resolved relative to the CSS file by the browser.
```css
@font-face {
font-family: "MyThemeText";
src: url("../fonts/MyThemeText.woff2") format("woff2");
font-display: swap;
}
html[data-theme="dark"] {
--ls-font-family: "MyThemeText", Inter, ui-sans-serif, system-ui, sans-serif;
}
```
Be careful with remote `@import` URLs: they can slow startup, fail offline, and leak network requests from a local-first app.
## Debugging workflow
1. Enable Logseq Developer mode.
2. Load the unpacked theme plugin.
3. Open DevTools and inspect `document.documentElement`:
- `data-theme`
- `data-color`
- `data-font`
- platform classes
4. In the theme selector, choose each declared light/dark theme.
5. Verify the selected CSS appears as a `<link rel="stylesheet">` in `document.head`.
6. Inspect computed variables on `html`, `body`, `.theme-container`, and the specific UI area you are styling.
7. If a variable is not taking effect, check whether the component is using `--lx-*`, shui HSL tokens, or a targeted class instead of `--ls-*`.
## Validation checklist
Before publishing a theme plugin, test at least these screens and states:
- [ ] Light and dark mode selection; switching between modes keeps each custom theme selection.
- [ ] Default accent color and at least one non-default `data-color` accent.
- [ ] Page title, journal title, normal blocks, nested blocks, block refs, page refs, tags, properties.
- [ ] Editing state: textarea, slash command menu, autocomplete, date picker, block context menu.
- [ ] Left sidebar, right sidebar, search, command palette, settings dialogs.
- [ ] Notifications, confirmation dialogs, dropdowns, popovers, tooltips.
- [ ] Code blocks / CodeMirror, inline code, marks, blockquotes, tables.
- [ ] PDF viewer highlights and toolbar if PDF is in scope.
- [ ] Graph and whiteboard/tldraw surfaces if your palette changes accents broadly.
- [ ] Desktop Electron and at least one mobile/narrow viewport if the theme changes layout/spacing.
- [ ] High contrast of text against every background; visible focus rings; visible selected/hover states.
- [ ] Reduced-motion friendliness if you add animations.
## Common pitfalls
- Overriding only `--ls-*` while newer shui components still read `--background`, `--popover`, `--border`, `--primary`, or `--lx-*` variables.
- Writing HSL tokens as full CSS colors instead of channel values.
- Using high-specificity selectors or `!important` everywhere, making user custom CSS and future Logseq changes harder to coexist with.
- Styling by generated Tailwind class names instead of stable Logseq/Radix selectors and variables.
- Forgetting to scope a dark theme to `html[data-theme="dark"]` or a light theme to `html[data-theme="light"]`.
- Hard-coding external font/image URLs without offline fallback.
- Changing editor layout, block bullets, or mobile sidebars without testing touch interactions.
## Quick reference: priority order for theme authors
1. Set `--ls-*` semantic variables for Logseq-specific UI.
2. Set shui/Tailwind HSL tokens (`--background`, `--foreground`, `--popover`, `--border`, `--primary`, etc.).
3. Map `--lx-gray-*` and `--lx-accent-*` for newer shared UI consistency.
4. Add extension variables such as `--ph-*` and `--ls-wb-*` only if your theme covers PDF/whiteboard.
5. Add low-specificity, mode-scoped selector overrides for the remaining gaps.

View File

@@ -0,0 +1,293 @@
# Logseq DB Properties (Developer Guide)
This note explains how **properties** work in Logseq when using a **DB graph** (database mode), how that differs from a **file graph** (Markdown/Org), and how to use the **plugin SDK** APIs defined in `libs/src/LSPlugin.ts`.
> Scope: This is written for plugin developers. It focuses on `logseq.Editor.*` property APIs and tag/class modeling.
---
## 1) Two worlds: File graph vs DB graph
### File graph (Markdown / Org)
- “Properties” are primarily **text syntax** stored in the file:
- Markdown frontmatter (`---`)
- Org property drawers (`:PROPERTIES:`)
- Inline props (`key:: value`)
- When you update a property, you’re often ultimately **editing text**.
- SDK calls like `Editor.insertBlock`/`updateBlock` have `opts.properties`, but note this warning in the typings:
- `IBatchBlock.properties` is **not supported for DB graph**.
### DB graph (Database mode)
DB mode treats properties as **first-class DB entities**.
Think of it as a 2-layer model:
1) **Property definitions (schema)**
- A property key (e.g. `rating`, `authors`, `zotero_key`) exists as an entity.
- It may have a schema: type, cardinality, visibility.
2) **Property values (data)**
- Blocks/pages can store values for property keys.
- Persisted structurally in the DB (not by emitting `key:: value` text).
In DB graphs, prefer the dedicated APIs:
- Property schema: `Editor.getProperty / upsertProperty / removeProperty`
- Values on blocks: `Editor.upsertBlockProperty / removeBlockProperty / getBlockProperties`
---
## 2) Important types (from `LSPlugin.ts`)
### `PropertySchema`
```ts
export type PropertySchema = {
type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string,
cardinality: 'many' | 'one',
hide: boolean
public: boolean
}
```
Practical meaning:
- `type`
- Controls editor/UI behavior and (in DB graphs) how values are interpreted.
- Common: `default`, `number`, `date`, `checkbox`, `url`, `node`.
- `cardinality`
- `'one'`: a single value
- `'many'`: multiple values (typically passed as an array)
- `hide`
- Hide in UI property panels.
- `public`
- Expose property in UI (and typically configure discoverability).
### Entities you’ll see
- `BlockEntity`: blocks, but also used for some “special blocks” such as property entities.
- `PageEntity`: pages, tags (classes), property pages (`type: 'property'`), etc.
- `BlockIdentity`: a block uuid string OR `{ uuid }`.
---
## 3) DB-only vs graph-agnostic checks
Before doing DB-only work, check graph type:
```ts
const isDbGraph = await logseq.App.checkCurrentIsDbGraph()
if (!isDbGraph) {
await logseq.UI.showMsg('This feature requires a DB graph.', 'warning')
return
}
// Also check app-level capability:
const { supportDb } = await logseq.App.getInfo()
```
---
## 4) Property schema APIs (DB only)
### Get a property definition
```ts
const propEntity = await logseq.Editor.getProperty('zotero_key')
// -> BlockEntity | null
```
### Create or update a property definition (idempotent)
Use `upsertProperty` to ensure the property exists and has the schema you expect.
```ts
await logseq.Editor.upsertProperty(
'zotero_key',
{
type: 'default',
cardinality: 'one',
hide: false,
public: true,
},
{ name: 'Zotero Key' }
)
```
Notes:
- `key` is your stable identifier (recommend `snake_case`).
- `opts.name` can be used as a **display name** for users.
### Remove a property definition
```ts
await logseq.Editor.removeProperty('zotero_key')
```
---
## 5) Block/page property value APIs
### Set (upsert) a property value on a block
```ts
const block = await logseq.Editor.getCurrentBlock()
if (!block) return
await logseq.Editor.upsertBlockProperty(block.uuid, 'zotero_key', 'ABCD1234')
```
### Set multi-value properties (`cardinality: 'many'`)
For `many`, pass an array. Use `{ reset: true }` when you want to overwrite vs merge.
```ts
await logseq.Editor.upsertBlockProperty(
block.uuid,
'authors',
['Ada Lovelace', 'Alan Turing'],
{ reset: true }
)
```
### Remove a value
```ts
await logseq.Editor.removeBlockProperty(block.uuid, 'zotero_key')
```
### Read properties
```ts
const props = await logseq.Editor.getBlockProperties(block.uuid)
// -> Record<string, any> | null
const pageProps = await logseq.Editor.getPageProperties('My Page')
```
### Read a single property (returns a `BlockEntity | null`)
`getBlockProperty` is useful when you want the DB entity wrapper for a key.
```ts
const v = await logseq.Editor.getBlockProperty(block.uuid, 'zotero_key')
```
---
## 6) Tags as “classes” + attaching properties (DB modeling)
In DB graphs, tags can behave like **classes**.
Relevant APIs:
- `Editor.createTag(tagName, opts)`
- `Editor.addTagProperty(tagId, propertyIdOrName)` / `removeTagProperty`
- `Editor.addTagExtends(tagId, parentTagIdOrName)` / `removeTagExtends`
- `Editor.addBlockTag(blockId, tagId)` / `removeBlockTag`
### Create a tag with tagProperties
This is the most developer-friendly way to ship a “schema bundle”:
```ts
const tag = await logseq.Editor.createTag('ZoteroItem', {
tagProperties: [
{
name: 'zotero_key',
schema: { type: 'default', cardinality: 'one', public: true, hide: false },
},
{
name: 'authors',
schema: { type: 'node', cardinality: 'many', public: true, hide: false },
},
{
name: 'published_at',
schema: { type: 'date', cardinality: 'one', public: true, hide: false },
},
],
})
if (tag) {
const block = await logseq.Editor.getCurrentBlock()
if (block) await logseq.Editor.addBlockTag(block.uuid, tag.uuid)
}
```
Why this pattern is popular:
- Users see a consistent “record type” (`ZoteroItem`) with fields.
- Plugins can be idempotent: create if missing, otherwise just reuse.
### Add properties to an existing tag later
```ts
const tag = await logseq.Editor.getTag('ZoteroItem')
if (tag) {
await logseq.Editor.addTagProperty(tag.uuid, 'zotero_key')
await logseq.Editor.addTagProperty(tag.uuid, 'authors')
}
```
### Tag inheritance
```ts
await logseq.Editor.addTagExtends('ZoteroItem', 'Reference')
```
---
## 7) Recommended conventions for plugin authors
### Prefer DB APIs over text editing in DB graphs
- Don’t generate `key:: value` strings as your “source of truth” in DB mode.
- Use `upsertProperty` + `upsertBlockProperty`.
### Keep keys stable; use display names for UX
- Use safe keys (`snake_case`, ASCII) for `key`.
- Use `upsertProperty(key, schema, { name })` to show a nice label.
### Be explicit about cardinality
- If you intend a list, set `cardinality: 'many'` and always write arrays.
- Use `{ reset: true }` when you want to overwrite.
### Guard DB-only APIs
- Always check `App.checkCurrentIsDbGraph()`.
---
## 8) A tiny helper wrapper (optional)
If you’re building a small internal “DB properties SDK” for your plugin, a minimal shape looks like:
```ts
export async function ensureDbGraph() {
if (!(await logseq.App.checkCurrentIsDbGraph())) {
throw new Error('DB graph required')
}
}
export async function ensureProperty(
key: string,
schema: Partial<import('../src/LSPlugin').PropertySchema>,
name?: string
) {
return logseq.Editor.upsertProperty(key, schema, name ? { name } : undefined)
}
export async function setProp(
block: string,
key: string,
value: any,
reset = false
) {
return logseq.Editor.upsertBlockProperty(block, key, value, reset ? { reset: true } : undefined)
}
```
---
## Appendix: API quick reference (from `LSPlugin.ts`)
**Property schema (DB only)**
- `Editor.getProperty(key)`
- `Editor.upsertProperty(key, schema?, opts?)`
- `Editor.removeProperty(key)`
**Block/page values**
- `Editor.upsertBlockProperty(block, key, value, { reset? })`
- `Editor.removeBlockProperty(block, key)`
- `Editor.getBlockProperty(block, key)`
- `Editor.getBlockProperties(block)`
- `Editor.getPageProperties(page)`
**Tags / Classes**
- `Editor.createTag(tagName, { tagProperties? })`
- `Editor.getTag(nameOrIdent)` / `Editor.getAllTags()`
- `Editor.addTagProperty(tagId, propertyIdOrName)` / `removeTagProperty`
- `Editor.addTagExtends(tagId, parentTagIdOrName)` / `removeTagExtends`
- `Editor.addBlockTag(blockId, tagId)` / `removeBlockTag`

View File

@@ -0,0 +1,680 @@
# Logseq DB Properties SDK Reference
## Overview
Logseq has two storage modes:
- **File-based**: Properties stored as `key:: value` in markdown files
- **DB-based**: Properties stored in SQLite with structured schema, types, and relationships
This guide covers **DB-based properties API** for plugin development.
---
## Core Concepts
### 1. Property Schema (Global Definition)
Properties must be defined globally before use. Think of it as creating columns in a database.
### 2. Tags as Classes
Tags act as classes/types. Pages/blocks can be tagged to inherit property schemas.
### 3. Property Values
After defining properties, assign values to specific blocks/pages.
---
## API Reference
### Property Schema APIs
#### `upsertProperty(name, schema?, opts?)`
Define or update a property schema globally.
**Parameters:**
- `name` (string): Property name
- `schema` (optional object):
- `type`: `'default'` | `'node'` | `'date'` | `'number'` | `'checkbox'` | `'url'`
- `cardinality`: `'one'` | `'many'` (for `node` type)
- `hide`: boolean - hide property in UI
- `public`: boolean - make property public
- `opts` (optional object):
- `name`: string - display name override
**Returns:** `IEntityID` object with `id`
**Example:**
```typescript
// Simple text property
await logseq.Editor.upsertProperty('author', { type: 'default' })
// Reference property (links to other pages)
await logseq.Editor.upsertProperty('tags', {
type: 'node',
cardinality: 'many'
})
// Hidden property with custom display name
await logseq.Editor. upsertProperty('internalKey',
{ type: 'default', hide: true },
{ name: 'Internal Key' }
)
```
#### `getProperty(key)`
Retrieve an existing property by key.
**Returns:** `BlockEntity` or `null`
**Example:**
```typescript
const prop = await logseq.Editor. getProperty('author')
if (!prop) {
await logseq.Editor.upsertProperty('author', { type: 'default' })
}
```
#### `removeProperty(key)`
Remove a property definition globally.
**Example:**
```typescript
await logseq.Editor.removeProperty('deprecatedField')
```
---
### Tag (Class) Management APIs
#### `createTag(name, opts?)`
Create a tag as a class/type definition with properties in one call.
**Parameters:**
- `name` (string): Tag name
- `opts` (optional object):
- `uuid`: string - custom UUID
- `tagProperties`: Array of property definitions
- `name`: string - property name
- `schema`: Partial<PropertySchema> - property schema
- `properties`: object - additional properties
**Returns:** `PageEntity` or `null`
**Example:**
```typescript
// Simple tag
const bookTag = await logseq.Editor.createTag('book')
// Tag with custom UUID
const articleTag = await logseq.Editor.createTag('article', {
uuid: 'custom-uuid-123'
})
// Tag with properties defined in one call
const bookTag = await logseq.Editor. createTag('book', {
uuid: generateStableUUID('book'),
tagProperties: [
{
name: 'author',
schema: { type: 'default' }
},
{
name: 'isbn',
schema: { type: 'default' }
},
{
name: 'tags',
schema: { type: 'node', cardinality: 'many' }
}
]
})
```
#### `getTag(nameOrIdent)`
Retrieve an existing tag by name or entity ID.
**Returns:** `PageEntity` or `null`
**Example:**
```typescript
const tag = await logseq.Editor.getTag('book')
if (!tag) {
// Tag doesn't exist, create it
}
```
#### `getTagsByName(tagName)`
Retrieve all tags matching a name (useful for namespaced tags).
**Returns:** `Array<PageEntity>` or `null`
**Example:**
```typescript
const tags = await logseq.Editor.getTagsByName('book')
```
#### `getAllTags()`
Get all tags in the current graph.
**Returns:** `Array<PageEntity>` or `null`
**Example:**
```typescript
const allTags = await logseq. Editor.getAllTags()
```
#### `addTagProperty(tagId, propertyIdOrName)`
Add a property to a tag's schema. Property must be defined via `upsertProperty` first.
**Parameters:**
- `tagId` (BlockIdentity): Tag UUID
- `propertyIdOrName` (BlockIdentity): Property UUID or name
**Example:**
```typescript
// Define property globally
await logseq.Editor. upsertProperty('author', { type: 'default' })
// Add to tag
const bookTag = await logseq. Editor.createTag('book')
await logseq.Editor. addTagProperty(bookTag.uuid, 'author')
```
#### `removeTagProperty(tagId, propertyIdOrName)`
Remove a property from a tag's schema.
**Example:**
```typescript
await logseq.Editor.removeTagProperty(bookTag.uuid, 'deprecatedField')
```
#### `addTagExtends(tagId, parentTagIdOrName)`
Set tag inheritance (class extends).
**Parameters:**
- `tagId` (BlockIdentity): Child tag UUID
- `parentTagIdOrName` (BlockIdentity): Parent tag UUID or name
**Example:**
```typescript
const mediaTag = await logseq.Editor.createTag('Media')
const bookTag = await logseq. Editor.createTag('book')
// book extends Media
await logseq.Editor. addTagExtends(bookTag. uuid, mediaTag.uuid)
```
#### `removeTagExtends(tagId, parentTagIdOrName)`
Remove tag inheritance.
**Example:**
```typescript
await logseq.Editor.removeTagExtends(bookTag.uuid, mediaTag.uuid)
```
---
### Block/Page Tagging APIs
#### `addBlockTag(blockId, tagId)`
Tag a block or page, making it an instance of that tag's class.
**Parameters:**
- `blockId` (BlockIdentity): Block or page UUID
- `tagId` (BlockIdentity): Tag UUID
**Example:**
```typescript
const page = await logseq.Editor. createPage('Moby Dick')
const bookTag = await logseq.Editor.getTag('book')
await logseq.Editor.addBlockTag(page.uuid, bookTag.uuid)
```
#### `removeBlockTag(blockId, tagId)`
Remove a tag from a block or page.
**Example:**
```typescript
await logseq.Editor.removeBlockTag(page.uuid, bookTag.uuid)
```
#### `getTagObjects(nameOrIdent)`
Get all blocks/pages tagged with a specific tag.
**Returns:** `Array<BlockEntity>` or `null`
**Example:**
```typescript
const allBooks = await logseq.Editor.getTagObjects('book')
```
---
### Block/Page Property Value APIs
#### `upsertBlockProperty(blockUUID, key, value, options?)`
Set property value for a specific block or page.
**Parameters:**
- `blockUUID` (BlockIdentity | EntityID): Block or page UUID or entity ID
- `key` (string): Property name
- `value` (any): Property value
- For `node` type with `cardinality: 'many'`: Use array of page IDs (`.id` not `.uuid`)
- For `node` type with `cardinality: 'one'`: Use single page ID
- For other types: Use primitive values
- `options` (optional object):
- `reset`: boolean - replace instead of merge
**Example:**
```typescript
const page = await logseq.Editor.getPage('my-page-uuid')
// Set simple value
await logseq.Editor.upsertBlockProperty(page.uuid, 'author', 'John Doe')
// Set number
await logseq.Editor.upsertBlockProperty(page.uuid, 'year', 2024)
// Set single reference (use . id not .uuid)
await logseq.Editor.upsertBlockProperty(page.uuid, 'category', categoryPage.id)
// Set multiple references (use . id not .uuid)
await logseq.Editor.upsertBlockProperty(
page.uuid,
'tags',
[tagPage1.id, tagPage2.id, tagPage3.id]
)
// Reset property value completely
await logseq.Editor. upsertBlockProperty(
page.uuid,
'tags',
[newTag1.id],
{ reset: true }
)
```
#### `getBlockProperty(blockId, key)`
Get a single property value from a block or page.
**Returns:** `BlockEntity` or `null`
**Example:**
```typescript
const author = await logseq.Editor. getBlockProperty(page.uuid, 'author')
```
#### `getBlockProperties(blockId)`
Get all properties from a block.
**Returns:** `Record<string, any>` or `null`
**Example:**
```typescript
const props = await logseq.Editor. getBlockProperties(page.uuid)
console.log(props. author, props.isbn)
```
#### `getPageProperties(pageId)`
Get all properties from a page.
**Returns:** `Record<string, any>` or `null`
**Example:**
```typescript
const props = await logseq.Editor.getPageProperties(page.uuid)
```
#### `removeBlockProperty(blockId, key)`
Remove a property from a block or page.
**Example:**
```typescript
await logseq.Editor.removeBlockProperty(page.uuid, 'deprecatedField')
```
---
### Special Properties & Utilities
#### Hide Empty Values
Use special property key `:logseq. property/hide-empty-value`:
```typescript
const prop = await logseq.Editor.upsertProperty('optionalField', { type: 'default' })
await logseq.Editor.upsertBlockProperty(
prop.id, // Use property's entity ID
': logseq.property/hide-empty-value',
true
)
```
#### Set Block Icon
```typescript
// Tabler icon
await logseq.Editor.setBlockIcon(blockId, 'tabler-icon', 'book')
// Emoji (from emoji-mart)
await logseq.Editor.setBlockIcon(blockId, 'emoji', 'books')
```
#### Remove Block Icon
```typescript
await logseq.Editor.removeBlockIcon(blockId)
```
---
## Common Patterns
### Pattern 1: Define Schema with Tag and Properties (Simple)
```typescript
async function setupBookSchema() {
// Create tag with all properties in one call
const bookTag = await logseq.Editor.createTag('book', {
uuid: generateStableUUID('book'),
tagProperties: [
{ name: 'title', schema: { type: 'default' } },
{ name: 'author', schema: { type: 'default' } },
{ name: 'isbn', schema: { type: 'default' } },
{ name: 'year', schema: { type: 'number' } },
{ name: 'tags', schema: { type: 'node', cardinality: 'many' } }
]
})
return bookTag
}
```
### Pattern 2: Define Schema with Inheritance
```typescript
async function setupSchemaWithInheritance() {
// 1. Create parent tag
const mediaTag = await logseq.Editor.createTag('Media', {
tagProperties: [
{ name: 'title', schema: { type: 'default' } },
{ name: 'year', schema: { type: 'number' } },
{ name: 'tags', schema: { type: 'node', cardinality: 'many' } }
]
})
// 2. Create child tag with specific properties
const bookTag = await logseq.Editor.createTag('book', {
tagProperties: [
{ name: 'author', schema: { type: 'default' } },
{ name: 'isbn', schema: { type: 'default' } }
]
})
// 3. Set inheritance
await logseq. Editor.addTagExtends(bookTag.uuid, mediaTag.uuid)
return { mediaTag, bookTag }
}
```
### Pattern 3: Create Instance with Properties
```typescript
async function createBookInstance(bookData) {
// 1. Create page with stable UUID
const pageUUID = generateStableUUID(bookData.id)
let page = await logseq.Editor.getPage(pageUUID)
if (!page) {
page = await logseq.Editor. createPage(
bookData.title,
{},
{ customUUID: pageUUID, redirect: false }
)
}
// 2. Tag the page
const bookTag = await logseq. Editor.getTag('book')
await logseq.Editor.addBlockTag(page.uuid, bookTag. uuid)
// 3. Set property values
await logseq. Editor.upsertBlockProperty(page.uuid, 'title', bookData.title)
await logseq.Editor.upsertBlockProperty(page.uuid, 'author', bookData. author)
await logseq.Editor.upsertBlockProperty(page.uuid, 'isbn', bookData.isbn)
await logseq.Editor.upsertBlockProperty(page.uuid, 'year', bookData. year)
return page. uuid
}
```
### Pattern 4: Link Related Entities
```typescript
async function linkBookToCategories(bookPageUUID, categoryKeys) {
// Get or create category pages
const categoryIDs = await Promise.all(
categoryKeys. map(async (key) => {
const catUUID = generateStableUUID(key)
let catPage = await logseq.Editor.getPage(catUUID)
if (!catPage) {
catPage = await logseq.Editor.createPage(key, {}, {
customUUID: catUUID
})
}
return catPage.id // ⚠️ Use .id not .uuid for references
})
)
// Link book to categories
await logseq.Editor.upsertBlockProperty(
bookPageUUID,
'tags', // Must be 'node' type with 'many' cardinality
categoryIDs
)
}
```
### Pattern 5: Batch Schema Setup from Configuration
```typescript
async function setupSchemaFromConfig(config) {
// config example:
// {
// "Media": {
// "properties": ["title", "year"],
// "parent": null
// },
// "book": {
// "properties": ["author", "isbn"],
// "parent": "Media"
// }
// }
for (const [tagName, tagConfig] of Object. entries(config)) {
let tag = await logseq.Editor. getTag(tagName)
if (!tag) {
// Create tag with properties
tag = await logseq.Editor.createTag(tagName, {
tagProperties: tagConfig.properties. map(name => ({
name,
schema: { type: 'default' }
}))
})
// Set parent if specified
if (tagConfig.parent) {
const parentTag = await logseq. Editor.getTag(tagConfig. parent)
if (parentTag) {
await logseq.Editor.addTagExtends(tag.uuid, parentTag.uuid)
}
}
}
}
}
```
### Pattern 6: Migrate Existing Pages to New Schema
```typescript
async function migrateToBookSchema() {
// 1. Setup schema
const bookTag = await logseq.Editor.createTag('book', {
tagProperties: [
{ name: 'author', schema: { type: 'default' } },
{ name: 'year', schema: { type: 'number' } }
]
})
// 2. Find all pages with old structure
const allPages = await logseq.Editor.getAllPages()
// 3. Migrate each page
for (const page of allPages) {
const props = await logseq.Editor. getPageProperties(page.uuid)
// Check if page has book-like properties
if (props?. author || props?.isbn) {
// Add book tag
await logseq.Editor.addBlockTag(page.uuid, bookTag.uuid)
// Migrate properties if needed
if (props. author) {
await logseq.Editor.upsertBlockProperty(
page.uuid,
'author',
props. author
)
}
}
}
}
```
---
## Best Practices
### 1. Use Stable UUIDs
Generate deterministic UUIDs from stable identifiers:
```typescript
import { v5 as uuidv5 } from 'uuid'
const NAMESPACE = 'your-plugin-namespace-uuid'
function generateStableUUID(id: string): string {
return uuidv5(id, NAMESPACE)
}
```
### 2. Check Before Creating
Always check if tags/pages/properties exist before creating:
```typescript
let tag = await logseq.Editor.getTag('book')
if (!tag) {
tag = await logseq.Editor.createTag('book', {
tagProperties: [/* ... */]
})
}
```
### 3. Property Definition Order (Manual Approach)
If not using `createTag` with `tagProperties`:
1. Define property globally with `upsertProperty`
2. Create tag with `createTag`
3. Add property to tag with `addTagProperty`
4. Create page and tag it with `addBlockTag`
5. Set values with `upsertBlockProperty`
### 4. Use . id for References ⚠️
When setting `node` type properties, use `.id` not `.uuid`:
```typescript
const categoryPage = await logseq.Editor.getPage('category-uuid')
// ✅ Correct
await logseq.Editor.upsertBlockProperty(
page.uuid,
'category',
categoryPage.id
)
// ❌ Wrong
await logseq.Editor.upsertBlockProperty(
page.uuid,
'category',
categoryPage.uuid
)
```
### 5. Error Handling
Wrap API calls in try-catch for robustness:
```typescript
try {
await logseq.Editor.upsertBlockProperty(uuid, 'field', value)
} catch (error) {
console.error(`Failed to set property: ${error}`)
await logseq.UI.showMsg(`Error: ${error. message}`, 'error')
}
```
### 6. Use createTag with tagProperties
Prefer the streamlined approach for better readability:
```typescript
// ✅ Better: One call
const tag = await logseq.Editor. createTag('book', {
tagProperties: [
{ name: 'author', schema: { type: 'default' } },
{ name: 'isbn', schema: { type: 'default' } }
]
})
// ❌ Verbose: Multiple calls
await logseq. Editor.upsertProperty('author', { type: 'default' })
await logseq.Editor.upsertProperty('isbn', { type: 'default' })
const tag = await logseq.Editor. createTag('book')
await logseq.Editor. addTagProperty(tag.uuid, 'author')
await logseq.Editor.addTagProperty(tag.uuid, 'isbn')
```
---
## Real-World Example: Zotero Plugin
See complete implementation: [xyhp915/logseq-zotero-plugin](https://github.com/xyhp915/logseq-zotero-plugin/blob/main/src/handlers. ts#L19-L207)
Key techniques:
- Schema defined from `z_item_types. json` metadata
- Tag hierarchy: `Zotero` → `book`, `article`, etc.
- Stable UUIDs via `v5(itemKey, namespace)`
- Relationship properties for collections
- Nested blocks for attachments
---
## Type Definitions
```typescript
type PropertySchema = {
type: 'default' | 'number' | 'node' | 'date' | 'checkbox' | 'url' | string
cardinality: 'many' | 'one'
hide: boolean
public: boolean
}
type BlockIdentity = BlockUUID | Pick<BlockEntity, 'uuid'>
type PageIdentity = BlockPageName | BlockIdentity
type EntityID = number
```
---
## Reference
- **Type Definitions**: `@logseq/libs` - [LSPlugin. ts](https://github.com/logseq/logseq/blob/master/libs/src/LSPlugin.ts)
- **Real Example**: [logseq-zotero-plugin/handlers.ts](https://github.com/xyhp915/logseq-zotero-plugin/blob/main/src/handlers. ts)
- **Search More**: [GitHub Code Search](https://github.com/search?type=code&q=repo:xyhp915/logseq-zotero-plugin+upsertBlockProperty)

View File

@@ -0,0 +1,550 @@
## Logseq Datascript Query Skills Guide
### 1. Logseq SDK Query APIs
```typescript
// DSL Query - Logseq's query language
const results0 = await logseq.DB.q('[[page-name]]')
const todos = await logseq.DB.q('(task TODO DOING)')
// Datascript Query - Datalog syntax (more powerful)
const results1 = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where [?b :block/marker "TODO"]]
`)
// Query with parameters
const results2 = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:in $ ?name
:where
[?p :block/name ?name]
[? b :block/page ?p]]
`, `"my-page"`)
// Watch database changes
logseq.DB.onChanged(({ blocks, txData }) => {
console.log('Changed blocks:', blocks)
})
// Watch specific block changes
logseq.DB.onBlockChanged(uuid, (block, txData) => {
console.log('Block changed:', block)
})
```
---
### 2. Datalog Query Syntax Basics
#### 2.1 Basic Structure
```clojure
[:find <return-value> ;; what to return
:in $ <params... > ;; input parameters ($ = database)
:where <clauses...>] ;; query conditions
```
#### 2.2 : find Clause
```clojure
;; Return entity IDs
[:find ?b :where ...]
;; Return multiple values
[:find ?b ?name :where ...]
;; Return full entity (commonly used)
[:find (pull ?b [*]) :where ...]
;; Return specific attributes
[:find (pull ?b [:block/uuid :block/content]) :where ...]
;; Aggregate functions
[:find (count ?b) :where ...]
[:find (min ?d) (max ?d) :where ...]
```
#### 2.3 :where Clause - Data Patterns
```clojure
;; Basic triple: [entity attribute value]
[?b :block/marker "TODO"] ;; ? b's marker equals "TODO"
[? b :block/page ?p] ;; ?b belongs to page ? p
[?p :block/name ?name] ;; ? p's name binds to ?name
;; Multiple conditions (implicit AND)
[?b :block/marker "TODO"]
[?b :block/page ?p]
[?p :block/journal? true] ;; all three must be satisfied
```
#### 2.4 :in Clause - Parameter Passing
```clojure
;; $ is the database (required)
[:find (pull ?b [*])
:in $
:where ...]
;; Single parameter
[:find (pull ?b [*])
:in $ ?marker
:where [?b :block/marker ? marker]]
;; Multiple parameters
[:find (pull ?b [*])
:in $ ?marker ? page-name
:where
[?b :block/marker ?marker]
[? p :block/name ?page-name]
[?b :block/page ?p]]
;; Collection parameter (match multiple values)
[:find (pull ?b [*])
:in $ [?marker ...]
:where [?b :block/marker ?marker]]
```
---
### 3. Schema Reference: File Graph vs DB Graph
#### 3.1 Block Attributes
| Attribute | File Graph | DB Graph | Type | Description |
|---------------------|:----------:|:--------:|--------|-------------------------------------|
| `:block/uuid` | ✅ | ✅ | UUID | Unique block identifier |
| `:block/content` | ✅ | ❌ | String | Raw block content (File Graph only) |
| `:block/title` | ❌ | ✅ | String | Block title/content (DB Graph only) |
| `:block/page` | ✅ | ✅ | Ref | Parent page reference |
| `:block/parent` | ✅ | ✅ | Ref | Parent block reference |
| `:block/left` | ✅ | ❌ | Ref | Left sibling block |
| `:block/order` | ❌ | ✅ | String | Block order in DB Graph |
| `:block/refs` | ✅ | ✅ | Ref[] | Referenced pages/blocks |
| `:block/marker` | ✅ | ❌ | String | Task marker (TODO/DOING/DONE) |
| `:block/priority` | ✅ | ❌ | String | Priority (A/B/C) |
| `:block/scheduled` | ✅ | ❌ | Int | Scheduled date (YYYYMMDD) |
| `:block/deadline` | ✅ | ❌ | Int | Deadline date (YYYYMMDD) |
| `:block/properties` | ✅ | ❌ | Map | Properties as key-value map |
| `:block/tags` | ✅ | ✅ | Ref[] | Tag references |
| `:block/link` | ❌ | ✅ | Ref | Link to class/tag in DB Graph |
| `:block/created-at` | ✅ | ❌ | Int | Creation timestamp (File Graph) |
| `:block/updated-at` | ✅ | ❌ | Int | Update timestamp (File Graph) |
#### 3.2 Page Attributes
| Attribute | File Graph | DB Graph | Type | Description |
|------------------------|:----------:|:--------:|---------|------------------------------------------|
| `:block/name` | ✅ | ✅ | String | Page name (lowercase) |
| `:block/original-name` | ✅ | ✅ | String | Original page name |
| `:block/journal?` | ✅ | ✅ | Boolean | Is journal page |
| `:block/journal-day` | ✅ | ✅ | Int | Journal date (YYYYMMDD) |
| `:block/type` | ❌ | ✅ | String | Type ("page", "class", "property", etc.) |
| `:block/format` | ✅ | ❌ | Keyword | Format (:markdown or :org) |
| `:block/file` | ✅ | ❌ | Ref | Associated file reference |
#### 3.3 DB Graph Specific - System Properties (Idents)
| Attribute | Type | Description |
|--------------------------------------|----------|------------------------------|
| `:logseq.property/created-at` | Int (ms) | Creation timestamp |
| `:logseq.property/updated-at` | Int (ms) | Update timestamp |
| `:logseq.property/icon` | Object | Icon definition `{type, id}` |
| `:logseq.property/hide-empty-value` | Boolean | Hide property if empty |
| `:logseq.property/closed-value-mode` | Boolean | Enum mode for property |
| `:logseq.property/closed-values` | Array | Allowed enum values |
| `:logseq.property/schema` | Object | Property schema definition |
| `:logseq.property. class/extends` | Ref[] | Parent class references |
| `:logseq.property.class/properties` | Ref[] | Class property references |
#### 3.4 DB Graph Task System
In DB Graph, tasks use a different system based on tags/classes:
| Attribute | Description |
|--------------------------|-------------------------------------------------------------|
| `:block/tags` | Reference to task status class (e.g., `#logseq.class/Todo`) |
| `:logseq.task/status` | Task status property |
| `:logseq.task/priority` | Task priority property |
| `:logseq.task/deadline` | Task deadline property |
| `:logseq.task/scheduled` | Task scheduled property |
---
### 4. Common Query Examples
#### 4.1 Task Queries
**File Graph:**
```typescript
// All TODOs
const todos = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where [?b :block/marker "TODO"]]
`)
// TODO or DOING
const activeTasks = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:in $ [?m ...]
:where [?b :block/marker ? m]]
`, `["TODO" "DOING"]`)
// Tasks with priority A
const priorityTasks = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :block/marker ? m]
[?b : block/priority "A"]]
`)
// Scheduled for today
const today = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const scheduled = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:in $ ? today
:where [?b : block/scheduled ?today]]
`, parseInt(today))
```
**DB Graph:**
```typescript
// All tasks with TODO status
const todos = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :block/tags ?tag]
[?tag :db/ident : logseq.class/Todo]]
`)
// Tasks by status property
const tasks = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :logseq.task/status ?s]
[?s :db/ident :logseq. task/status. todo]]
`)
```
#### 4.2 Page Queries
**Both File Graph and DB Graph:**
```typescript
// All journal pages
const journals = await logseq.DB.datascriptQuery(`
[:find (pull ?p [*])
:where [?p :block/journal? true]]
`)
// Journals in specific month
const monthJournals = await logseq.DB.datascriptQuery(`
[:find (pull ?p [*])
:where
[?p :block/journal? true]
[?p : block/journal-day ?d]
[(>= ?d 20250101)]
[(<= ?d 20250131)]]
`)
// Blocks in a specific page
const blocks = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:in $ ? name
:where
[? p :block/name ?name]
[?b :block/page ?p]]
`, `"my-page"`)
```
**DB Graph only:**
```typescript
// Pages by type
const pages = await logseq.DB.datascriptQuery(`
[:find (pull ?p [*])
:where [?p :block/type "page"]]
`)
// Class/Tag pages
const classes = await logseq.DB.datascriptQuery(`
[:find (pull ?p [*])
:where [?p : block/type "class"]]
`)
// Recently created pages (last 7 days)
const sevenDaysAgo = Date.now() - 7 * 24 * 60 * 60 * 1000
const recentPages = await logseq.DB.datascriptQuery(`
[:find (pull ?p [*])
:where
[?p :block/type "page"]
[?p :logseq.property/created-at ?created]
[(> ? created ${sevenDaysAgo})]]
`)
```
#### 4.3 Reference Queries
**Both File Graph and DB Graph:**
```typescript
// Blocks referencing a specific page
const refs = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:in $ ?page
:where
[? p :block/name ?page]
[?b :block/refs ?p]]
`, `"target-page"`)
// Block references (backlinks)
const blockRefs = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:in $ ?uuid
:where
[? target :block/uuid ?uuid]
[?b :block/refs ?target]]
`, `#uuid "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`)
```
#### 4.4 Property Queries
**File Graph:**
```typescript
// Blocks with specific property
const withProp = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :block/properties ?props]
[(get ?props :status)]]
`)
// Property equals specific value
const statusDone = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :block/properties ?props]
[(get ?props :status) ?v]
[(= ?v "done")]]
`)
```
**DB Graph:**
```typescript
// Blocks with property value (properties are direct attributes)
const withProp = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b : user. property/status ?v]]
`)
// Query by property ident
const results = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[? prop :db/ident : user.property/status]
[?b ? prop ? v]
[?v :block/title "done"]]
`)
```
#### 4.5 Content Search
**File Graph:**
```typescript
// Content contains keyword
const results = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[? b :block/content ?c]
[(clojure.string/includes? ?c "keyword")]]
`)
```
**DB Graph:**
```typescript
// Title contains keyword
const results = await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :block/title ?t]
[(clojure.string/includes? ?t "keyword")]]
`)
```
---
### 5. Predicate Functions
```clojure
;; Comparison
[(> ? d 20250101)]
[(>= ?d 20250101)]
[(< ?d 20250131)]
[(<= ?d 20250131)]
[(= ?v "value")]
[(not= ?v "value")]
;; String operations
[(clojure.string/includes? ?s "text")]
[(clojure.string/starts-with? ?s "prefix")]
[(clojure.string/ends-with? ?s "suffix")]
[(clojure.string/blank? ?s)]
;; Collection operations
[(contains? #{"A" "B" "C"} ?v)]
;; Null checks
[(some? ?v)]
[(nil? ?v)]
;; Math operations
[(+ ?a ?b) ? sum]
[(- ?a ?b) ?diff]
[(* ?a ?b) ?product]
```
---
### 6. NOT and OR Clauses
```clojure
;; NOT - exclude conditions
[:find (pull ?b [*])
:where
[? b :block/marker ? m]
(not [?b :block/priority "C"])]
;; OR - any condition matches
[:find (pull ?b [*])
:where
(or [?b :block/marker "TODO"]
[?b :block/marker "DOING"])]
;; OR with AND (or-join)
[:find (pull ?b [*])
:where
(or-join [? b]
(and [?b :block/marker "TODO"]
[?b :block/priority "A"])
(and [?b :block/marker "DOING"]))]
```
---
### 7. Aggregation Queries
```typescript
// Count
const count = await logseq.DB.datascriptQuery(`
[:find (count ?b)
:where [?b : block/marker "TODO"]]
`)
// Group by page
const grouped = await logseq.DB.datascriptQuery(`
[:find ? name (count ?b)
:where
[?b :block/marker "TODO"]
[?b :block/page ?p]
[?p :block/name ?name]]
`)
// Min/Max
const range = await logseq.DB.datascriptQuery(`
[:find (min ?d) (max ?d)
:where
[?p :block/journal? true]
[?p :block/journal-day ?d]]
`)
```
---
### 8. Cross-Compatible Plugin Example
```typescript
import '@logseq/libs'
async function detectGraphType(): Promise<'db' | 'file'> {
const result = await logseq.DB.datascriptQuery(`
[:find ? type .
:where [_ :block/type ?type]]
`)
return result ? 'db' : 'file'
}
async function queryTodos(graphType: 'db' | 'file') {
if (graphType === 'file') {
return await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where [?b :block/marker "TODO"]]
`)
} else {
return await logseq.DB.datascriptQuery(`
[:find (pull ?b [*])
:where
[?b :block/tags ?tag]
[?tag :db/ident :logseq.class/Todo]]
`)
}
}
async function queryBlockContent(uuid: string, graphType: 'db' | 'file') {
const attr = graphType === 'file' ? ': block/content' : ':block/title'
return await logseq.DB.datascriptQuery(`
[:find ? content .
:in $ ?uuid
:where
[? b : block/uuid ?uuid]
[?b ${attr} ?content]]
`, `#uuid "${uuid}"`)
}
async function main() {
const graphType = await detectGraphType()
console.log('Graph type:', graphType)
logseq.Editor.registerSlashCommand('Query TODOs', async () => {
try {
const results = await queryTodos(graphType)
const todos = results?.flat() || []
if (todos.length) {
const content = todos.map(t => `- ((${t.uuid}))`).join('\n')
await logseq.Editor.insertAtEditingCursor(content)
} else {
await logseq.UI.showMsg('No TODOs found', 'warning')
}
} catch (e) {
console.error('Query failed:', e)
await logseq.UI.showMsg('Query failed', 'error')
}
})
}
logseq.ready().then(main)
```
---
### 9. Important Notes
1. **Page names are lowercase**: `:block/name` stores lowercase names; convert before querying
2. **Parameter format**:
- Strings: `"value"`
- Numbers: `123`
- UUIDs: `#uuid "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"`
- Keywords: `:keyword`
- Collections: `["a" "b" "c"]`
3. **Flatten results**: Query results usually need `.flat()` processing
4. **Error handling**: Always wrap queries in try-catch
5. **Performance**: Use `pull` with specific attributes instead of `[*]` when possible
6. **Graph compatibility**: Check graph type before using graph-specific attributes

View File

@@ -0,0 +1,352 @@
# Property & Tag Idents
## What is an Ident?
An **ident** is a unique keyword identifier for a property or tag in the database. It's distinct from:
- **name**: Human-readable display name (e.g., "Created At")
- **uuid**: Universal unique identifier for the entity
- **ident**: Internal database identifier (e.g., `:logseq.property/created-at`)
**Key characteristics:**
- Idents use kebab-case with namespace prefixes
- System properties use `:logseq.property/` namespace
- System class properties use `:logseq.class/` namespace
- Plugin created class use `:plugin.class.xxx-plugin-id/` namespace
- Plugin created property use `:plugin.property.xxx-plugin-id/` namespace
- Custom properties typically don't have explicit idents (auto-generated)
## When to Use Idents
Use idents when:
1. Working with **built-in system properties**
2. Setting **special behaviors** for properties
3. Querying the database directly with datascript
4. Creating **stable references** that survive renames
**Example:**
```typescript
// Using ident for system property
await logseq.Editor. upsertBlockProperty(
blockId,
':logseq. property/created-at', // ident
new Date().getTime()
)
// Using regular property name
await logseq.Editor. upsertBlockProperty(
blockId,
'author', // regular name
'John Doe'
)
```
---
## Built-in System Properties
### Common System Properties
#### `:logseq.property/created-at`
Timestamp when the block/page was created.
```typescript
await logseq.Editor.upsertBlockProperty(
page.uuid,
':logseq.property/created-at',
Date.now()
)
```
#### `:logseq.property/updated-at`
Timestamp when the block/page was last updated.
```typescript
await logseq.Editor.upsertBlockProperty(
block.uuid,
':logseq.property/updated-at',
Date.now()
)
```
#### `:logseq.property/icon`
Icon for a page or block (set via `setBlockIcon` API).
```typescript
// Don't set directly, use API:
await logseq.Editor. setBlockIcon(page.uuid, 'emoji', 'book')
// Behind the scenes, this sets:
// :logseq.property/icon {: type "emoji", :id "book"}
```
#### `:logseq.property/hide-empty-value`
Hide property when value is empty.
```typescript
const prop = await logseq.Editor.upsertProperty('optional-field', {
type: 'default'
})
await logseq.Editor.upsertBlockProperty(
prop. id, // Apply to property entity itself
':logseq.property/hide-empty-value',
true
)
```
#### `:logseq.property/closed-value-mode`
Property only accepts predefined values (enum-like).
```typescript
const prop = await logseq.Editor. upsertProperty('status', { type: 'default' })
await logseq.Editor.upsertBlockProperty(
prop.id,
':logseq.property/closed-value-mode',
true
)
// Define allowed values
await logseq.Editor.upsertBlockProperty(
prop. id,
':logseq.property/closed-values',
['todo', 'in-progress', 'done']
)
```
#### `:logseq.property/schema`
Property schema definition (type, cardinality, etc.).
```typescript
// Automatically set by upsertProperty, don't set manually
await logseq.Editor. upsertProperty('tags', {
type: 'node',
cardinality: 'many'
})
// Behind the scenes sets:
// :logseq. property/schema {:type "node", :cardinality "many"}
```
---
## Built-in Class (Tag) Properties
### Tag Inheritance & Schema
#### `:logseq.property. class/extends`
Define parent classes (tag inheritance).
```typescript
const mediaTag = await logseq.Editor.createTag('Media')
const bookTag = await logseq. Editor.createTag('book')
// Use addTagExtends API (recommended)
await logseq.Editor. addTagExtends(bookTag. uuid, mediaTag.uuid)
// Or set directly:
await logseq.Editor. upsertBlockProperty(
bookTag.uuid,
': logseq.property.class/extends',
[mediaTag.id] // Array of parent tag IDs
)
```
#### `:logseq.property.class/properties`
Properties available for this class.
```typescript
// Use addTagProperty API (recommended)
await logseq.Editor.addTagProperty(bookTag.uuid, 'author')
// Or set directly:
const authorProp = await logseq.Editor.getProperty('author')
await logseq.Editor.upsertBlockProperty(
bookTag.uuid,
':logseq.property. class/properties',
[authorProp.id]
)
```
---
## Working with Idents
### Pattern 1: Query by Ident
```typescript
// Get property entity by ident
const createdAtProp = await logseq. DB.datascriptQuery(`
[: find (pull ?p [*])
:where
[?p :block/type "property"]
[?p :block/ident : logseq.property/created-at]]
`)
console.log(createdAtProp)
```
### Pattern 2: Check if Property Has Ident
```typescript
async function getPropertyIdent(propertyName: string) {
const prop = await logseq.Editor. getProperty(propertyName)
return prop?.ident || null
}
const ident = await getPropertyIdent('created-at')
// Returns: ": logseq.property/created-at" or null
```
### Pattern 3: Create Custom Namespaced Property
```typescript
// For plugin-specific properties, use your own namespace
const pluginNamespace = 'myplugin'
async function createPluginProperty(name: string, schema: any) {
// Note: Custom idents are not directly settable via API
// They're auto-generated, but you can query by name
const prop = await logseq.Editor.upsertProperty(name, schema)
// Custom properties won't have idents unless they're system properties
return prop
}
```
### Pattern 4: Set Multiple System Properties
```typescript
async function setupPageMetadata(pageUUID: string, metadata: any) {
const now = Date.now()
// Set creation timestamp
await logseq. Editor.upsertBlockProperty(
pageUUID,
':logseq.property/created-at',
metadata.createdAt || now
)
// Set update timestamp
await logseq. Editor.upsertBlockProperty(
pageUUID,
':logseq.property/updated-at',
now
)
// Set custom properties
await logseq. Editor.upsertBlockProperty(pageUUID, 'author', metadata.author)
await logseq.Editor.upsertBlockProperty(pageUUID, 'source', metadata.source)
}
```
### Pattern 5: Configure Property Behavior
```typescript
async function createEnumProperty(name: string, allowedValues: string[]) {
// 1. Create property
const prop = await logseq.Editor.upsertProperty(name, { type: 'default' })
// 2. Enable closed-value mode
await logseq.Editor.upsertBlockProperty(
prop.id,
':logseq.property/closed-value-mode',
true
)
// 3. Set allowed values
await logseq.Editor. upsertBlockProperty(
prop.id,
':logseq.property/closed-values',
allowedValues
)
// 4. Optionally hide when empty
await logseq. Editor.upsertBlockProperty(
prop.id,
':logseq.property/hide-empty-value',
true
)
return prop
}
// Usage
await createEnumProperty('priority', ['high', 'medium', 'low'])
```
---
## System Property Reference Table
| Ident | Purpose | Value Type | Applied To |
|-------|---------|------------|------------|
| `:logseq.property/created-at` | Creation timestamp | number (ms) | Block/Page |
| `:logseq.property/updated-at` | Update timestamp | number (ms) | Block/Page |
| `:logseq.property/icon` | Icon definition | `{type, id}` object | Block/Page |
| `:logseq.property/hide-empty-value` | Hide if empty | boolean | Property entity |
| `:logseq.property/closed-value-mode` | Enum mode | boolean | Property entity |
| `:logseq.property/closed-values` | Allowed values | array | Property entity |
| `:logseq.property/schema` | Schema definition | object | Property entity |
| `:logseq.property. class/extends` | Parent classes | array of IDs | Tag entity |
| `:logseq.property.class/properties` | Class properties | array of IDs | Tag entity |
---
## Best Practices for Idents
### 1. Prefer High-Level APIs Over Direct Ident Manipulation
Use specialized APIs instead of setting idents directly:
```typescript
// ✅ Better: Use API
await logseq.Editor.addTagExtends(childTag.uuid, parentTag.uuid)
// ❌ Avoid: Direct ident manipulation
await logseq. Editor.upsertBlockProperty(
childTag.uuid,
': logseq.property. class/extends',
[parentTag.id]
)
```
### 2. Only Use Idents for System Properties
Custom properties should use regular names:
```typescript
// ✅ Correct: System property with ident
await logseq. Editor.upsertBlockProperty(
page.uuid,
': logseq.property/created-at',
Date.now()
)
// ✅ Correct: Custom property with name
await logseq.Editor. upsertBlockProperty(
page.uuid,
'author',
'John Doe'
)
// ❌ Wrong: Don't create custom idents
await logseq.Editor. upsertBlockProperty(
page.uuid,
':myplugin/custom-prop', // Not supported
'value'
)
```
### 3. Query System Properties by Ident in Datascript
When using advanced queries, idents provide stable references:
```typescript
// Query all pages created in the last 7 days
const recentPages = await logseq. DB.datascriptQuery(`
[:find (pull ?p [*])
:where
[?p :block/type "page"]
[?p : logseq.property/created-at ? created]
[(> ?created ${Date.now() - 7 * 24 * 60 * 60 * 1000})]]
`)
```

View File

@@ -0,0 +1,804 @@
# Logseq Experiments API Guide
This guide covers the **experimental APIs** exposed as `logseq.Experiments` in the Logseq Plugin SDK.
These APIs are intentionally lower-level than the stable SDK. They let plugins:
- reuse host React/ReactDOM
- render internal Logseq components
- convert between JS and ClojureScript data structures
- load scripts dynamically
- register custom renderers for fenced code, routes, sidebars, properties, and block bodies
- hook internal extensions such as KaTeX
- access host/plugin internals when absolutely necessary
> **⚠️ Warning**
>
> Everything in `logseq.Experiments` is unstable. Signatures, render props, and behaviors may change without a normal deprecation window. Plugins using these APIs may be temporarily unsupported on the Marketplace.
---
## Overview
The current experimental surface includes:
1. **React integration**: `React`, `ReactDOM`
2. **Internal components**: `Components.Editor`
3. **Interop utilities**: `Utils.toClj`, `toJs`, `jsxToClj`, `toKeyword`, `toSymbol`
4. **Script loading**: `loadScripts(...)`
5. **Renderer registration**:
- `registerFencedCodeRenderer(...)`
- `registerDaemonRenderer(...)`
- `registerRouteRenderer(...)`
- `registerHostedRenderer(...)`
- `registerSidebarRenderer(...)`
- `registerBlockPropertiesRenderer(...)`
- `registerBlockRenderer(...)`
6. **Extension enhancers**: `registerExtensionsEnhancer(...)`
7. **Host/plugin internals**:
- `pluginLocal`
- `ensureHostScope()`
- `invokeExperMethod(...)`
---
## 1. React Integration
Use the host's React runtime instead of bundling your own copy.
> **React version note**
>
> Current Logseq hosts ship React 19.x and ReactDOM 19.x. Treat
> `logseq.Experiments.React` and `logseq.Experiments.ReactDOM` as the
> host's concrete runtime, not as version-neutral compatibility shims.
>
> Compatibility guidance:
>
> - Prefer the host React runtime for elements returned from experimental
> renderers.
> - Do not mix host React elements, hooks, contexts, or portals with a
> separately bundled React copy.
> - Audit third-party integrations before attaching them to the host
> runtime. Older helpers that rely on legacy root APIs such as
> `ReactDOM.render`, `ReactDOM.hydrate`, `ReactDOM.unmountComponentAtNode`,
> or `findDOMNode` may need updates for React 19.
> - In most cases you do **not** need to mount manually with `ReactDOM`.
> Return React elements from the experimental `render` callback and let
> Logseq own the root.
### `logseq.Experiments.React`
Returns the React instance from the host scope.
```typescript
const React = logseq.Experiments.React
```
### `logseq.Experiments.ReactDOM`
Returns the ReactDOM instance from the host scope.
```typescript
const ReactDOM = logseq.Experiments.ReactDOM
```
### Example
```typescript
const React = logseq.Experiments.React
const MyComponent = React.createElement(
'div',
{ className: 'my-plugin-card' },
'Hello from a host React tree'
)
```
---
## 2. Components
### `logseq.Experiments.Components.Editor`
Renders Logseq page content using an internal page editor component.
**Type**
```typescript
(props: { page: string } & Record<string, any>) => any
```
**Parameters**
- `page`: page name to render
```typescript
const Editor = logseq.Experiments.Components.Editor
const preview = Editor({ page: 'My Page Name' })
```
---
## 3. Utilities
`logseq.Experiments.Utils` exposes host interop helpers.
### `toClj(input: any)`
Convert JavaScript data into ClojureScript data structures.
```typescript
const cljData = logseq.Experiments.Utils.toClj({ key: 'value' })
```
### `jsxToClj(input: any)`
Convert JS/JSX-style input to ClojureScript while preserving JSX-ish structures better than a plain conversion.
```typescript
const view = { type: 'div', props: { children: 'Content' } }
const cljView = logseq.Experiments.Utils.jsxToClj(view)
```
### `toJs(input: any)`
Convert ClojureScript values back into plain JavaScript.
```typescript
const jsData = logseq.Experiments.Utils.toJs(cljData)
```
### `toKeyword(input: any)`
Convert a string into a ClojureScript keyword.
```typescript
const keyword = logseq.Experiments.Utils.toKeyword('my-key')
```
### `toSymbol(input: any)`
Convert a string into a ClojureScript symbol.
```typescript
const symbol = logseq.Experiments.Utils.toSymbol('my-symbol')
```
---
## 4. Script Loading
### `logseq.Experiments.loadScripts(...scripts: string[])`
Dynamically load scripts into the host environment.
**Parameters**
- `scripts`: HTTP(S) URLs or relative plugin resource paths
**Returns**
```typescript
Promise<void>
```
**Behavior**
- relative paths are resolved against the current plugin resource root
- HTTP/HTTPS URLs are used as-is
- scripts are loaded in the given order
```typescript
await logseq.Experiments.loadScripts(
'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.umd.min.js'
)
await logseq.Experiments.loadScripts('./vendor/local-helper.js')
await logseq.Experiments.loadScripts(
'https://cdn.example.com/lib1.js',
'https://cdn.example.com/lib2.js',
'./local-script.js'
)
```
---
## 5. Custom Renderers
Experimental renderers are where most of the newer APIs live.
## 5.1 Fenced Code Renderer
Register a custom renderer for fenced code blocks such as:
````markdown
```my-lang
...
```
````
### `logseq.Experiments.registerFencedCodeRenderer(lang, opts)`
```text
registerFencedCodeRenderer(
lang: string,
opts: {
edit?: boolean
before?: () => Promise<void>
subs?: string[]
render: (props: { content: string }) => any
}
): any
```
**Options**
- `edit`: whether the fenced block remains editable
- `before`: async preload hook, usually for loading scripts/assets
- `subs`: experimental subscription list
- `render`: React renderer receiving `{ content }`
```typescript
logseq.Experiments.registerFencedCodeRenderer('my-chart', {
edit: false,
before: async () => {
await logseq.Experiments.loadScripts(
'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.umd.min.js'
)
},
render: ({ content }) => {
const React = logseq.Experiments.React
return React.createElement('canvas', {
ref: (canvas: HTMLCanvasElement | null) => {
if (!canvas || !window.Chart) return
try {
const config = JSON.parse(content)
new window.Chart(canvas, config)
} catch (error) {
console.error('Chart renderer error', error)
}
},
})
},
})
```
## 5.2 Daemon Renderer
Register a renderer that stays mounted in a global daemon container.
### `logseq.Experiments.registerDaemonRenderer(key, opts)`
```text
registerDaemonRenderer(
key: string,
opts: {
before?: () => Promise<void>
subs?: string[]
render: (props: {}) => any
}
): any
```
**Notes**
- use `subs`, not `sub`
- `before` is supported by the host even if older typings may not show it yet
- daemon renderers are useful for lightweight always-on UI, not large app shells
```typescript
logseq.Experiments.registerDaemonRenderer('my-status-bar', {
subs: ['ui/theme'],
render: () => {
const React = logseq.Experiments.React
return React.createElement(
'div',
{
style: {
position: 'fixed',
right: 12,
bottom: 12,
padding: '6px 10px',
borderRadius: 8,
background: 'var(--ls-secondary-background-color)',
},
},
'Plugin active'
)
},
})
```
## 5.3 Route Renderer
Register a custom route view.
### `logseq.Experiments.registerRouteRenderer(key, opts)`
```text
registerRouteRenderer(
key: string,
opts: {
path: string
name?: string
subs?: string[]
render: (props: {}) => any
}
): any
```
**Options**
- `path`: route path, e.g. `'/my-plugin-dashboard'`
- `name`: optional display name; if omitted, the internal key is reused
- `subs`: experimental subscription list
- `render`: route component
```typescript
logseq.Experiments.registerRouteRenderer('my-custom-page', {
path: '/my-plugin-dashboard',
name: 'Dashboard',
render: () => {
const React = logseq.Experiments.React
return React.createElement('div', { className: 'my-plugin-dashboard' }, [
React.createElement('h1', { key: 'title' }, 'Plugin Dashboard'),
React.createElement('p', { key: 'body' }, 'Custom content here'),
])
},
})
```
> Route navigation is handled by Logseq's router. In docs and examples, prefer describing the registered `path` rather than relying on page navigation APIs, which are not the same thing.
## 5.4 Hosted Renderer
Low-level API for host-managed render targets.
Today, the main built-in consumer is the right sidebar, so most plugins should prefer `registerSidebarRenderer(...)` unless they specifically need the lower-level primitive.
### `logseq.Experiments.registerHostedRenderer(key, opts)`
```text
registerHostedRenderer(
key: string,
opts: {
title?: string
mode?: string
type?: string
subs?: string[]
render: (props: {}) => any
}
): any
```
**Options**
- `title`: display title when the host surfaces the renderer
- `type`: host-specific placement type
- `mode`: host-specific placement mode
- `subs`: experimental subscription list
- `render`: React renderer
The host currently passes the registered renderer record back into the render function in some placements. Treat that as implementation detail, not a stable contract.
## 5.5 Sidebar Renderer
Convenience wrapper over `registerHostedRenderer(...)` for right-sidebar tools.
### `logseq.Experiments.registerSidebarRenderer(key, opts)`
```text
registerSidebarRenderer(
key: string,
opts: {
title?: string
subs?: string[]
render: (props: {}) => any
[key: string]: any
}
): any
```
**Behavior**
- your key is automatically namespaced internally as `_sidebar.${key}`
- `type` is forced to `'sidebar'`
- the renderer appears in the right-sidebar plugin menu
```typescript
logseq.Experiments.registerSidebarRenderer('inspector', {
title: 'Inspector',
render: () => {
const React = logseq.Experiments.React
return React.createElement('div', null, 'Hello from the sidebar renderer')
},
})
```
## 5.6 Block Properties Renderer
Render custom UI inside a block's properties area.
### `logseq.Experiments.registerBlockPropertiesRenderer(key, opts)`
```text
type BlockPropertiesCondition =
| { has: string }
| { equals: [string, any] }
| { in: [string, any[]] }
| { not: BlockPropertiesCondition }
| { any: BlockPropertiesCondition[] }
| { all: BlockPropertiesCondition[] }
type BlockPropertiesRendererProps = {
blockId: string
properties: Record<string, any>
}
registerBlockPropertiesRenderer(
key: string,
opts: {
when?: BlockPropertiesCondition | ((props: BlockPropertiesRendererProps) => boolean)
mode?: 'prepend' | 'append' | 'replace'
priority?: number
subs?: string[]
render: (props: BlockPropertiesRendererProps) => any
}
): any
```
**Behavior**
- `when` may be omitted, a declarative condition, or a synchronous predicate
- `mode` controls placement in the properties area:
- `prepend`: before native properties
- `append`: after native properties
- `replace`: replace native properties UI
- higher `priority` wins for conflicts
- for `replace`, the highest-priority matching replace renderer wins
- for `prepend`/`append`, all matching renderers are rendered in priority order
**Render props**
- `blockId`: block UUID string
- `properties`: plain JS object keyed by property names without the leading `:`
**Property serialization details**
Before data is passed into plugins, Logseq normalizes some values:
- keywords become strings like `'logseq.property/status'`
- UUIDs become strings
- entity references become small objects such as `{ uuid, title }`
- sets / collections of entity references become arrays of those objects
```typescript
logseq.Experiments.registerBlockPropertiesRenderer('priority-pill', {
when: { has: 'priority' },
mode: 'prepend',
priority: 10,
render: ({ properties }) => {
const React = logseq.Experiments.React
const priority = properties.priority
if (!priority) return null
return React.createElement(
'span',
{
style: {
display: 'inline-flex',
marginRight: 8,
padding: '2px 8px',
borderRadius: 9999,
fontSize: 12,
background: 'var(--ls-tertiary-background-color)',
},
},
`Priority: ${priority}`
)
},
})
```
## 5.7 Block Renderer
Replace a block's main outline body with plugin UI.
### `logseq.Experiments.registerBlockRenderer(key, opts)`
```text
type BlockRendererChild = Record<string, any> & {
children?: BlockRendererChild[]
}
type BlockRendererProps = {
blockId: string
properties: Record<string, any>
uuid?: string
page?: string
content?: string
format?: string
children?: BlockRendererChild[]
}
registerBlockRenderer(
key: string,
opts: {
when?: (props: BlockRendererProps) => boolean
includeChildren?: boolean
priority?: number
subs?: string[]
render: (props: BlockRendererProps) => any
}
): any
```
**Behavior**
- `when` must be a **synchronous predicate function** if provided
- declarative conditions are **not** supported here
- highest `priority` match wins
- when the plugin renderer is active, users can switch back to the native outline view via built-in UI on that block
- when `includeChildren` is `true`, Logseq passes a recursive child tree and hides native outline children while the plugin renderer is active
**Render props**
- `blockId`: block UUID string
- `uuid`: same block UUID
- `page`: page title
- `content`: block content/title text
- `format`: `'markdown'`, `'org'`, etc.
- `properties`: normalized property object
- `children`: recursive normalized child tree when `includeChildren` is enabled
```typescript
logseq.Experiments.registerBlockRenderer('kanban-card', {
when: ({ properties }) => properties.view === 'kanban-card',
includeChildren: true,
priority: 20,
render: ({ content, children = [] }) => {
const React = logseq.Experiments.React
return React.createElement('section', { className: 'my-kanban-card' }, [
React.createElement('h3', { key: 'title' }, content || 'Untitled'),
React.createElement(
'ul',
{ key: 'children' },
children.map((child, index) =>
React.createElement('li', { key: child.uuid || index }, child.title || child.content)
)
),
])
},
})
```
---
## 6. Extension Enhancers
### `logseq.Experiments.registerExtensionsEnhancer(type, enhancer)`
Enhance host libraries such as KaTeX.
```text
registerExtensionsEnhancer(
type: 'katex' | 'codemirror',
enhancer: (value: any) => Promise<any>
): any
```
For `katex`, the host immediately invokes the enhancer if KaTeX is already present.
```typescript
logseq.Experiments.registerExtensionsEnhancer('katex', async (katex) => {
katex.macros = {
...katex.macros,
'\\RR': '\\mathbb{R}',
'\\NN': '\\mathbb{N}',
'\\ZZ': '\\mathbb{Z}',
}
})
```
---
## 7. Host / Plugin Internals
## 7.1 `logseq.Experiments.pluginLocal`
Returns the internal `PluginLocal` instance for the current plugin.
```typescript
const pluginLocal = logseq.Experiments.pluginLocal
console.log(pluginLocal.id)
```
Use this sparingly. It is intentionally internal.
## 7.2 `logseq.Experiments.ensureHostScope()`
Returns the host scope, currently `window.top`, after attempting an access check.
```typescript
const host = logseq.Experiments.ensureHostScope()
```
This is mostly useful when you need direct access to host globals and understand the risks.
## 7.3 `logseq.Experiments.invokeExperMethod(type, ...args)`
Direct escape hatch for calling experimental host methods.
```typescript
const result = logseq.Experiments.invokeExperMethod(
'someExperimentalFeature',
arg1,
arg2
)
```
`type` is normalized to snake_case before resolution.
---
## 8. Complete Example: Fenced Code Renderer
```typescript
import '@logseq/libs'
async function main() {
logseq.Experiments.registerFencedCodeRenderer('chart', {
edit: false,
before: async () => {
await logseq.Experiments.loadScripts(
'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.umd.min.js'
)
},
render: ({ content }) => {
const React = logseq.Experiments.React
return React.createElement('canvas', {
ref: (canvas: HTMLCanvasElement | null) => {
if (!canvas || !window.Chart) return
try {
const config = JSON.parse(content)
new window.Chart(canvas, config)
} catch (error) {
console.error('Chart rendering error:', error)
}
},
})
},
})
}
logseq.ready(main).catch(console.error)
```
**Usage**
````markdown
```chart
{
"type": "line",
"data": {
"labels": ["Jan", "Feb", "Mar", "Apr"],
"datasets": [{
"label": "Sales",
"data": [10, 20, 15, 30],
"borderColor": "rgb(75, 192, 192)"
}]
}
}
```
````
---
## 9. Complete Example: Block Properties Badge
```typescript
import '@logseq/libs'
async function main() {
logseq.Experiments.registerBlockPropertiesRenderer('task-status-chip', {
when: {
any: [
{ equals: ['status', 'todo'] },
{ equals: ['status', 'doing'] },
],
},
mode: 'append',
priority: 5,
render: ({ properties }) => {
const React = logseq.Experiments.React
const value = properties.status
return React.createElement(
'span',
{
style: {
marginLeft: 8,
padding: '2px 8px',
borderRadius: 9999,
fontSize: 12,
background: 'var(--ls-secondary-background-color)',
},
},
`Status: ${value}`
)
},
})
}
logseq.ready(main).catch(console.error)
```
---
## 10. Best Practices
1. **Prefer stable APIs first**. Only use `Experiments` when the stable SDK cannot solve the problem.
2. **Use host React**. Avoid bundling a second React runtime into the same tree.
3. **Keep `when` predicates synchronous**. This is especially important for `registerBlockRenderer(...)`.
4. **Use `before` to preload dependencies** instead of doing ad hoc script injection inside render.
5. **Treat `subs` as experimental**. Reactive semantics may change.
6. **Keep renderers lightweight**. Block and daemon renderers can affect overall app responsiveness.
7. **Handle bad input defensively**. Render props often contain user-authored content and properties.
8. **Document your experimental usage** in the plugin README so users understand the risk.
9. **Prefer `registerSidebarRenderer(...)` over raw hosted renderers** when your goal is a right-sidebar tool.
10. **Test against real graphs**. Property values, references, and child trees can vary a lot.
---
## 11. Limitations and Notes
- **Experimental status**: no stability guarantee
- **Marketplace support**: may be restricted temporarily
- **Security**: be careful with external scripts and direct host access
- **Performance**: custom renderers run inside the app UI, so poor implementations are noticeable
- **Typings may lag behavior**: some newer runtime options can land before every generated wrapper/type is refreshed
### ClojureScript SDK note
The generated ClojureScript wrapper namespace `com.logseq.experiments` currently includes wrappers for:
- `load-scripts`
- `register-fenced-code-renderer`
- `register-daemon-renderer`
- `register-hosted-renderer`
- `register-sidebar-renderer`
- `register-route-renderer`
- `register-extensions-enhancer`
At the time of writing, `register-block-properties-renderer` and `register-block-renderer` are not yet present in that generated wrapper, so ClojureScript plugins may need to call them via `invoke-exper-method` until the wrapper is regenerated.
---
## See Also
- [Starter Guide](./starter_guide.md) - getting started with plugin development
- [DB Properties Guide](./db_properties_guide.md) - working with database properties
- [DB Query Guide](./db_query_guide.md) - querying the Logseq database
---
## Support
For questions and issues:
- [Logseq Discord](https://discord.gg/logseq) - `#plugin-dev`
- [GitHub Discussions](https://github.com/logseq/logseq/discussions)
- [Plugin API Documentation](https://plugins-doc.logseq.com/)
Remember: these are experimental features. Use them carefully and test thoroughly.

View File

@@ -0,0 +1,125 @@
# Logseq Plugin Starter Guide
# For Developers
In this short guide, it will walk you through the steps needed to set up your development environment for writing and
running a hello world simple inside of Logseq Desktop Client. You know Logseq Plugin based on Web Technologies composed
of JS & HTML & CSS, but neither are mandatory, you can develop plugins use any language which can be translated to
javascript (Logseq made by Clojurescript!). We will use Typescript to demonstrate this sample. Because there
is [a type definition file](https://www.npmjs.com/package/@logseq/libs) to make development experience even better.
## Install Node.js and NPM
You can download Node.js here, which will include NPM: https://nodejs.org/en/download/.
## Install TypeScript
To install TypeScript, run `npm install -g typescript` in a terminal.
## Get Logseq desktop app
At this time, plugin development and testing needs to be done using the Logseq desktop app. This is because Logseq needs
to read your code saved as a local file. The Logseq desktop app can be downloaded
here: https://github.com/logseq/logseq/releases.
If you already have the desktop app, please make sure to update to the latest version, as several features have been
added specifically in order to provide a better plugin development experience.
## Go to Settings > Turn on Developer Mode
This will bring up the "Plugins" entry in three dots more menu list on top right of head bar. Go to Plugins page, and
you will get a button with `Load unpacked plugin` label.
## Create plugin package from scaffold
We will use [logseq-hello-world](https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-hello-world) as
scaffold. Download this package files using Git or download zip straight to your own plugin package directory.
## Explain `package.json`
Generally Logseq plugin package is described by `package.json` file located root. Let's checkout these primary keys.
```json
{
"name": "logseq-plugin-hello-world",
"version": "0.0.1",
"main": "dist/index.html",
"logseq": {
"id": "yet-another-hello-world-plugin",
"title": "Say Hi to Logseq",
"icon": "logseq.png"
}
}
```
- __name__ [required] a name of plugin.
- it starts with `logseq-plugin` is better for Googling.
- __version__ [required] semantic version
- related link: https://semver.org/
- __main__ [optional] entry of plugin, optional for theme plugin only.
- `.html` file entry indicate that the plugin provide main iframe ui.
- `.js` file entry indicate that the plugin without main iframe ui.
- __logseq__ [required] specs of plugin & indicate it's a Logseq package.
- `id` [optional] the identity key of plugin. if you don't provide it that will be auto generated on load
development package.
- `title` [optional] the title display of plugin list card. if not provided, value of `pkg#name` would be used.
- `icon` [optional] plugin logo for your own brand :D.
- `minSDKVersion` - [optional] the minimum sdk version what your plugin required. eg: `0.1.0`.
- `supportsDB` - [optional] the boolean value indicate whether your plugin support the database feature.
- `supportsDBOnly` - [optional] the boolean value indicate whether your plugin only support the database feature.
Apart from these primary keys, you might find other useful fields, e.g. `author`, `description`, `repository`.
Although these are not necessary, but completed fields are kind for your plugin users.
## Develop your code
This scaffold using [Parcel](https://v2.parceljs.org/) as `ts` bundler, just it works out of box. Of Course, you could
choose another one what you prefer. Then install dependencies `npm install` and watch your code in development mode
`npm run dev`. It will build the development version outputs to the dist directory. Go to plugins page and pick your
plugin root directory by `Load unpacked plugin` button. You will get a message from this plugin. Let's explain the entry
file of `index.ts`.
### 1. import user plugin SDK [required]
```js
import '@logseq/libs'
```
It provides TS types and global sdk namespace `logseq`.
### 2. bootstrap your main function [required]
```js
logseq.ready(main).catch(console.error)
```
Limited by the plugin system mechanism, user main function should wait for some preparations to make sure the SDK api
works correctly!
### 3. show your code [required]
```js
function main () {
logseq.UI.showMsg('❤️ Message from Hello World Plugin :)')
}
```
Change your code and reload the plugin to make it works.
> If you develop main ui in iframe, [HMR](https://webpack.js.org/concepts/hot-module-replacement/) feature provided by
> some ui frameworks is convenient that can avoid reloading plugin again and again.
🎉 Actually, you're done!
## Next steps
- Awesome samples
- https://github.com/logseq/logseq-plugin-samples
- https://github.com/logseq/awesome-logseq
- Community plugins
- https://github.com/search?q=logseq-plugin
# For users
Currently, Logseq plugin can only run in developer mode. Under this mode, plugin has ability to access notes data. So if
you're working with sensitive data, you'd better confirm the plugin is from trusted resource before installing it.