๐Ÿ“… let's chat! explore the endless possibilities creating industries that don't exist. click here

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)

saas packs v1.0.0
View Plugin

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`.

Skill file: plugins/saas-packs/obsidian-pack/skills/obsidian-core-workflow-b/SKILL.md