obsidian-core-workflow-b
Execute Obsidian secondary workflow: UI components and user interaction. Use when building modals, views, suggestions, or custom UI elements. Trigger with phrases like "obsidian modal", "obsidian UI", "obsidian view", "obsidian custom interface". allowed-tools: Read, Write, Edit, Bash(npm:*), Grep version: 1.0.0 license: MIT author: Jeremy Longshore <jeremy@intentsolutions.io>
Allowed Tools
No tools specified
Provided by Plugin
obsidian-pack
Claude Code skill pack for Obsidian plugin development and vault management (24 skills)
Installation
This skill is included in the obsidian-pack plugin:
/plugin install obsidian-pack@claude-code-plugins-plus
Click to copy
Instructions
# Obsidian Core Workflow B: UI Components
## Overview
Secondary workflow for Obsidian: building modals, views, suggestion popups, and custom UI elements.
## Prerequisites
- Completed `obsidian-install-auth` setup
- Familiarity with `obsidian-core-workflow-a`
- Understanding of DOM manipulation
## Instructions
### Step 1: Modal Dialogs
```typescript
import { App, Modal, Setting } from 'obsidian';
// Simple confirmation modal
export class ConfirmModal extends Modal {
private result: boolean = false;
private onSubmit: (result: boolean) => void;
private message: string;
constructor(app: App, message: string, onSubmit: (result: boolean) => void) {
super(app);
this.message = message;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Confirm Action' });
contentEl.createEl('p', { text: this.message });
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Cancel')
.onClick(() => {
this.result = false;
this.close();
}))
.addButton(btn => btn
.setButtonText('Confirm')
.setCta()
.onClick(() => {
this.result = true;
this.close();
}));
}
onClose() {
this.onSubmit(this.result);
}
}
// Form input modal
export class InputModal extends Modal {
private value: string = '';
private onSubmit: (value: string | null) => void;
private placeholder: string;
private title: string;
constructor(
app: App,
title: string,
placeholder: string,
onSubmit: (value: string | null) => void
) {
super(app);
this.title = title;
this.placeholder = placeholder;
this.onSubmit = onSubmit;
}
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: this.title });
new Setting(contentEl)
.setName('Input')
.addText(text => text
.setPlaceholder(this.placeholder)
.onChange(value => this.value = value));
new Setting(contentEl)
.addButton(btn => btn
.setButtonText('Cancel')
.onClick(() => {
this.close();
this.onSubmit(null);
}))
.addButton(btn => btn
.setButtonText('Submit')
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.value);
}));
}
onClose() {
const { contentEl } = this;
contentEl.empty();
}
}
```
### Step 2: Suggestion Popups (FuzzySuggestModal)
```typescript
import { App, FuzzySuggestModal, TFile } from 'obsidian';
// File picker modal
export class FileSuggestModal extends FuzzySuggestModal {
private onSelect: (file: TFile) => void;
constructor(app: App, onSelect: (file: TFile) => void) {
super(app);
this.onSelect = onSelect;
}
getItems(): TFile[] {
return this.app.vault.getMarkdownFiles();
}
getItemText(file: TFile): string {
return file.path;
}
onChooseItem(file: TFile, evt: MouseEvent | KeyboardEvent): void {
this.onSelect(file);
}
}
// Generic suggestion modal
export class SuggestModal extends FuzzySuggestModal {
private items: T[];
private getText: (item: T) => string;
private onSelect: (item: T) => void;
constructor(
app: App,
items: T[],
getText: (item: T) => string,
onSelect: (item: T) => void
) {
super(app);
this.items = items;
this.getText = getText;
this.onSelect = onSelect;
}
getItems(): T[] {
return this.items;
}
getItemText(item: T): string {
return this.getText(item);
}
onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void {
this.onSelect(item);
}
}
// Usage:
new SuggestModal(
this.app,
['Option 1', 'Option 2', 'Option 3'],
(item) => item,
(selected) => console.log('Selected:', selected)
).open();
```
### Step 3: Custom Views (Leaf Views)
```typescript
import { ItemView, WorkspaceLeaf } from 'obsidian';
export const VIEW_TYPE_CUSTOM = 'custom-view';
export class CustomView extends ItemView {
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewType(): string {
return VIEW_TYPE_CUSTOM;
}
getDisplayText(): string {
return 'Custom View';
}
getIcon(): string {
return 'dice';
}
async onOpen() {
const container = this.containerEl.children[1];
container.empty();
// Add content
container.createEl('h4', { text: 'Custom View Title' });
const content = container.createDiv({ cls: 'custom-view-content' });
content.createEl('p', { text: 'This is a custom view!' });
// Add interactive elements
const button = content.createEl('button', { text: 'Click me' });
button.addEventListener('click', () => {
console.log('Button clicked!');
});
}
async onClose() {
// Cleanup
}
}
// Register in main.ts:
export default class MyPlugin extends Plugin {
async onload() {
this.registerView(
VIEW_TYPE_CUSTOM,
(leaf) => new CustomView(leaf)
);
// Add ribbon icon to open view
this.addRibbonIcon('dice', 'Open Custom View', () => {
this.activateView();
});
// Add command
this.addCommand({
id: 'open-custom-view',
name: 'Open Custom View',
callback: () => this.activateView(),
});
}
async activateView() {
const { workspace } = this.app;
let leaf = workspace.getLeavesOfType(VIEW_TYPE_CUSTOM)[0];
if (!leaf) {
// Create new leaf in right sidebar
leaf = workspace.getRightLeaf(false);
await leaf.setViewState({
type: VIEW_TYPE_CUSTOM,
active: true,
});
}
workspace.revealLeaf(leaf);
}
onunload() {
// Clean up view
this.app.workspace.detachLeavesOfType(VIEW_TYPE_CUSTOM);
}
}
```
### Step 4: Editor Extensions (CodeMirror 6)
```typescript
import { EditorView, ViewPlugin, Decoration, DecorationSet } from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/state';
import { syntaxTree } from '@codemirror/language';
// Custom decoration plugin
const highlightPlugin = ViewPlugin.fromClass(class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.buildDecorations(view);
}
update(update: any) {
if (update.docChanged || update.viewportChanged) {
this.decorations = this.buildDecorations(update.view);
}
}
buildDecorations(view: EditorView): DecorationSet {
const builder = new RangeSetBuilder();
for (const { from, to } of view.visibleRanges) {
syntaxTree(view.state).iterate({
from,
to,
enter: (node) => {
if (node.name === 'HyperLink') {
builder.add(
node.from,
node.to,
Decoration.mark({ class: 'custom-highlight' })
);
}
},
});
}
return builder.finish();
}
}, {
decorations: (v) => v.decorations,
});
// Register in plugin:
this.registerEditorExtension(highlightPlugin);
```
### Step 5: Context Menus
```typescript
import { Menu, TFile, TFolder } from 'obsidian';
// Add to file menu
this.registerEvent(
this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => {
if (file instanceof TFile && file.extension === 'md') {
menu.addItem((item) => {
item
.setTitle('My Custom Action')
.setIcon('star')
.onClick(async () => {
// Handle click
console.log('File:', file.path);
});
});
}
})
);
// Add to editor context menu
this.registerEvent(
this.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => {
menu.addItem((item) => {
item
.setTitle('Insert Timestamp')
.setIcon('clock')
.onClick(() => {
editor.replaceSelection(new Date().toISOString());
});
});
})
);
```
## Output
- Modal dialogs for user input
- Suggestion popups with fuzzy search
- Custom sidebar views
- Editor decorations
- Context menus
## Error Handling
| Error | Cause | Solution |
|-------|-------|----------|
| View not showing | Not registered | Call `registerView` in `onload` |
| Modal closes immediately | Event propagation | Stop event propagation |
| Decorations not updating | Missing update handler | Implement `update` method |
| Menu item missing | Wrong event | Verify event type |
## Examples
### Progress Modal
```typescript
export class ProgressModal extends Modal {
private progressEl: HTMLElement;
private textEl: HTMLElement;
onOpen() {
const { contentEl } = this;
contentEl.createEl('h2', { text: 'Processing...' });
this.textEl = contentEl.createEl('p', { text: 'Starting...' });
const progressContainer = contentEl.createDiv({ cls: 'progress-container' });
this.progressEl = progressContainer.createDiv({ cls: 'progress-bar' });
this.progressEl.style.width = '0%';
}
setProgress(percent: number, text?: string) {
this.progressEl.style.width = `${percent}%`;
if (text) this.textEl.setText(text);
}
onClose() {
this.contentEl.empty();
}
}
// Usage:
const modal = new ProgressModal(this.app);
modal.open();
for (let i = 0; i <= 100; i += 10) {
modal.setProgress(i, `Processing ${i}%`);
await sleep(100);
}
modal.close();
```
### Sidebar Styling
```css
/* styles.css */
.custom-view-content {
padding: 16px;
}
.custom-view-content h4 {
margin-bottom: 12px;
}
.custom-view-content button {
margin-top: 8px;
}
```
## Resources
- [Obsidian Modal Reference](https://docs.obsidian.md/Reference/TypeScript+API/Modal)
- [Obsidian View Reference](https://docs.obsidian.md/Reference/TypeScript+API/ItemView)
- [CodeMirror 6 Documentation](https://codemirror.net/docs/)
## Next Steps
For common errors, see `obsidian-common-errors`.