mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
feat: add collapsible project note subtasks widget
- Add ProjectNoteDecorations extension for live preview mode - Create ProjectSubtasksService for shared project logic - Implement collapsible subtask display with persistent state - Add comprehensive event listening for real-time updates - Remove duplicate post-processor implementation - Use proper Obsidian editorLivePreviewField API - Add responsive CSS with smooth transitions
This commit is contained in:
parent
3a9e2dff25
commit
f193dade24
5 changed files with 593 additions and 0 deletions
|
|
@ -18,6 +18,7 @@ const CSS_FILES = [
|
|||
'styles/task-selector-modal.css', // TaskSelectorModal component with proper BEM scoping
|
||||
'styles/unscheduled-tasks-selector-modal.css', // UnscheduledTasksSelectorModal component with proper BEM scoping
|
||||
'styles/task-action-palette-modal.css', // TaskActionPaletteModal component with proper BEM scoping
|
||||
'styles/project-note-subtasks.css', // ProjectNoteSubtasks component with proper BEM scoping
|
||||
|
||||
// BEM View Files
|
||||
'styles/task-list-view.css', // TaskListView component with proper BEM scoping
|
||||
|
|
|
|||
373
src/editor/ProjectNoteDecorations.ts
Normal file
373
src/editor/ProjectNoteDecorations.ts
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
import { Decoration, DecorationSet, EditorView, PluginSpec, PluginValue, ViewPlugin, ViewUpdate, WidgetType } from '@codemirror/view';
|
||||
import { Extension, RangeSetBuilder } from '@codemirror/state';
|
||||
import { TFile, editorLivePreviewField, EventRef } from 'obsidian';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import { TaskInfo, EVENT_DATA_CHANGED, EVENT_TASK_UPDATED, EVENT_TASK_DELETED } from '../types';
|
||||
import { createTaskCard } from '../ui/TaskCard';
|
||||
import { ProjectSubtasksService } from '../services/ProjectSubtasksService';
|
||||
|
||||
class ProjectSubtasksWidget extends WidgetType {
|
||||
constructor(private plugin: TaskNotesPlugin, private tasks: TaskInfo[]) {
|
||||
super();
|
||||
}
|
||||
|
||||
toDOM(view: EditorView): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'tasknotes-plugin project-note-subtasks';
|
||||
|
||||
// Force block display and full width for inline widget
|
||||
container.style.display = 'block';
|
||||
container.style.width = '100%';
|
||||
container.style.clear = 'both';
|
||||
container.style.position = 'relative';
|
||||
|
||||
// Add title with collapsible functionality
|
||||
const titleEl = container.createEl('h3', {
|
||||
text: `Subtasks (${this.tasks.length})`,
|
||||
cls: 'project-note-subtasks__title'
|
||||
});
|
||||
|
||||
// Create task list container
|
||||
const taskListContainer = container.createEl('div', {
|
||||
cls: 'project-note-subtasks__list'
|
||||
});
|
||||
|
||||
// Add collapsible functionality
|
||||
const isCollapsed = this.getCollapsedState();
|
||||
if (isCollapsed) {
|
||||
titleEl.classList.add('collapsed');
|
||||
taskListContainer.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// Add click handler for collapsing/expanding
|
||||
titleEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const isCurrentlyCollapsed = titleEl.classList.contains('collapsed');
|
||||
|
||||
if (isCurrentlyCollapsed) {
|
||||
titleEl.classList.remove('collapsed');
|
||||
taskListContainer.classList.remove('collapsed');
|
||||
this.setCollapsedState(false);
|
||||
} else {
|
||||
titleEl.classList.add('collapsed');
|
||||
taskListContainer.classList.add('collapsed');
|
||||
this.setCollapsedState(true);
|
||||
}
|
||||
});
|
||||
|
||||
// Sort and render tasks (use static method to avoid creating new service instance)
|
||||
const sortedTasks = this.sortTasks(this.tasks);
|
||||
sortedTasks.forEach(task => {
|
||||
const taskCard = createTaskCard(task, this.plugin, {
|
||||
showDueDate: true,
|
||||
showCheckbox: false,
|
||||
showArchiveButton: false,
|
||||
showTimeTracking: false,
|
||||
showRecurringControls: true,
|
||||
groupByDate: false
|
||||
});
|
||||
|
||||
taskCard.classList.add('project-note-subtasks__task');
|
||||
taskListContainer.appendChild(taskCard);
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private sortTasks(tasks: TaskInfo[]): TaskInfo[] {
|
||||
return tasks.sort((a, b) => {
|
||||
// First sort by completion status (incomplete first)
|
||||
const aCompleted = this.plugin.statusManager.isCompletedStatus(a.status);
|
||||
const bCompleted = this.plugin.statusManager.isCompletedStatus(b.status);
|
||||
|
||||
if (aCompleted !== bCompleted) {
|
||||
return aCompleted ? 1 : -1;
|
||||
}
|
||||
|
||||
// Then sort by priority
|
||||
const aPriorityWeight = this.plugin.priorityManager.getPriorityWeight(a.priority);
|
||||
const bPriorityWeight = this.plugin.priorityManager.getPriorityWeight(b.priority);
|
||||
|
||||
if (aPriorityWeight !== bPriorityWeight) {
|
||||
return bPriorityWeight - aPriorityWeight; // Higher priority first
|
||||
}
|
||||
|
||||
// Then sort by due date (earliest first)
|
||||
if (a.due && b.due) {
|
||||
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||
} else if (a.due) {
|
||||
return -1; // Tasks with due dates come first
|
||||
} else if (b.due) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Finally sort by title
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collapsed state for project subtasks from localStorage
|
||||
*/
|
||||
private getCollapsedState(): boolean {
|
||||
try {
|
||||
const stored = localStorage.getItem('tasknotes-project-subtasks-collapsed');
|
||||
return stored === 'true';
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the collapsed state for project subtasks in localStorage
|
||||
*/
|
||||
private setCollapsedState(collapsed: boolean): void {
|
||||
try {
|
||||
if (collapsed) {
|
||||
localStorage.setItem('tasknotes-project-subtasks-collapsed', 'true');
|
||||
} else {
|
||||
localStorage.removeItem('tasknotes-project-subtasks-collapsed');
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ProjectNoteDecorationsPlugin implements PluginValue {
|
||||
decorations: DecorationSet;
|
||||
private cachedTasks: TaskInfo[] = [];
|
||||
private currentFile: TFile | null = null;
|
||||
private projectService: ProjectSubtasksService;
|
||||
private eventListeners: EventRef[] = [];
|
||||
private view: EditorView;
|
||||
|
||||
constructor(view: EditorView, private plugin: TaskNotesPlugin) {
|
||||
this.view = view;
|
||||
this.projectService = new ProjectSubtasksService(plugin);
|
||||
this.decorations = this.buildDecorations(view);
|
||||
|
||||
// Set up event listeners for data changes
|
||||
this.setupEventListeners();
|
||||
|
||||
// Load tasks for current file asynchronously
|
||||
this.loadTasksForCurrentFile(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
// Store the updated view reference
|
||||
this.view = update.view;
|
||||
|
||||
if (update.docChanged || update.viewportChanged) {
|
||||
this.decorations = this.buildDecorations(update.view);
|
||||
}
|
||||
|
||||
// Check if file changed
|
||||
const newFile = this.plugin.app.workspace.getActiveFile();
|
||||
if (newFile !== this.currentFile) {
|
||||
this.currentFile = newFile;
|
||||
this.loadTasksForCurrentFile(update.view);
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
// Clean up event listeners
|
||||
this.eventListeners.forEach(listener => {
|
||||
this.plugin.emitter.offref(listener);
|
||||
});
|
||||
this.eventListeners = [];
|
||||
}
|
||||
|
||||
private setupEventListeners() {
|
||||
// Listen for data changes that might affect project subtasks
|
||||
const dataChangeListener = this.plugin.emitter.on(EVENT_DATA_CHANGED, () => {
|
||||
// Refresh tasks for current file when data changes
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
const taskUpdateListener = this.plugin.emitter.on(EVENT_TASK_UPDATED, () => {
|
||||
// Refresh tasks for current file when tasks are updated
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
const taskDeleteListener = this.plugin.emitter.on(EVENT_TASK_DELETED, () => {
|
||||
// Refresh tasks for current file when tasks are deleted
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
// Listen for settings changes that might affect project subtasks
|
||||
const settingsChangeListener = this.plugin.emitter.on('settings-changed', () => {
|
||||
// Refresh tasks when settings change (e.g., custom fields, statuses)
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
// Listen for cache events that might affect project subtasks
|
||||
const fileUpdateListener = this.plugin.emitter.on('file-updated', (data: { path: string }) => {
|
||||
// Refresh if the updated file might contain project references
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
const fileDeleteListener = this.plugin.emitter.on('file-deleted', (data: { path: string }) => {
|
||||
// Refresh if a file was deleted that might have affected project references
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
const fileRenameListener = this.plugin.emitter.on('file-renamed', (data: { oldPath: string, newPath: string }) => {
|
||||
// Refresh if a file was renamed that might have affected project references
|
||||
this.loadTasksForCurrentFile(this.view);
|
||||
});
|
||||
|
||||
this.eventListeners.push(
|
||||
dataChangeListener,
|
||||
taskUpdateListener,
|
||||
taskDeleteListener,
|
||||
settingsChangeListener,
|
||||
fileUpdateListener,
|
||||
fileDeleteListener,
|
||||
fileRenameListener
|
||||
);
|
||||
}
|
||||
|
||||
private async loadTasksForCurrentFile(view: EditorView) {
|
||||
const file = this.plugin.app.workspace.getActiveFile();
|
||||
|
||||
if (file instanceof TFile) {
|
||||
try {
|
||||
this.cachedTasks = await this.projectService.getTasksLinkedToProject(file);
|
||||
|
||||
this.decorations = this.buildDecorations(view);
|
||||
view.requestMeasure();
|
||||
} catch (error) {
|
||||
console.error('Error loading tasks for project note:', error);
|
||||
}
|
||||
} else {
|
||||
this.cachedTasks = [];
|
||||
}
|
||||
}
|
||||
|
||||
private buildDecorations(view: EditorView): DecorationSet {
|
||||
const builder = new RangeSetBuilder<Decoration>();
|
||||
|
||||
try {
|
||||
// Only show in live preview mode, not source mode
|
||||
if (!view.state.field(editorLivePreviewField)) {
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
// Only build decorations if we have cached tasks
|
||||
if (this.cachedTasks.length === 0) {
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
const doc = view.state.doc;
|
||||
|
||||
// Ensure document has content
|
||||
if (doc.length === 0) {
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
// Find insertion position after frontmatter/properties
|
||||
let insertPos = this.findInsertionPosition(view, doc);
|
||||
|
||||
// Ensure position is valid
|
||||
if (insertPos < 0 || insertPos > doc.length) {
|
||||
insertPos = 0;
|
||||
}
|
||||
|
||||
const widget = Decoration.widget({
|
||||
widget: new ProjectSubtasksWidget(this.plugin, this.cachedTasks),
|
||||
side: 1 // Place widget after the position
|
||||
});
|
||||
|
||||
builder.add(insertPos, insertPos, widget);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error building project note decorations:', error);
|
||||
}
|
||||
|
||||
return builder.finish();
|
||||
}
|
||||
|
||||
|
||||
private findInsertionPosition(view: EditorView, doc: any): number {
|
||||
if (doc.lines === 0) return 0;
|
||||
|
||||
// Find the end of frontmatter if it exists
|
||||
let insertionPos = 0;
|
||||
let inFrontmatter = false;
|
||||
|
||||
for (let lineNum = 1; lineNum <= Math.min(doc.lines, 20); lineNum++) {
|
||||
try {
|
||||
const line = doc.line(lineNum);
|
||||
const text = line.text.trim();
|
||||
|
||||
// Check for frontmatter start
|
||||
if (lineNum === 1 && text === '---') {
|
||||
inFrontmatter = true;
|
||||
insertionPos = line.to;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for frontmatter end
|
||||
if (inFrontmatter && text === '---') {
|
||||
insertionPos = line.to;
|
||||
inFrontmatter = false;
|
||||
|
||||
// Look for next line to insert after frontmatter
|
||||
if (lineNum < doc.lines) {
|
||||
const nextLine = doc.line(lineNum + 1);
|
||||
if (nextLine.text.trim() === '') {
|
||||
insertionPos = nextLine.to;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we're in frontmatter, continue
|
||||
if (inFrontmatter) {
|
||||
insertionPos = line.to;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If no frontmatter, insert at the beginning
|
||||
if (lineNum === 1) {
|
||||
insertionPos = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// If we can't read a line, fall back to previous position
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we went through all lines or hit an error, insert at the end of processed content
|
||||
return Math.max(0, insertionPos);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const projectNoteDecorationsSpec: PluginSpec<ProjectNoteDecorationsPlugin> = {
|
||||
decorations: (plugin: ProjectNoteDecorationsPlugin) => plugin.decorations
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the project note decorations extension
|
||||
*/
|
||||
export function createProjectNoteDecorations(plugin: TaskNotesPlugin): Extension {
|
||||
return ViewPlugin.fromClass(
|
||||
class extends ProjectNoteDecorationsPlugin {
|
||||
constructor(view: EditorView) {
|
||||
super(view, plugin);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
super.destroy();
|
||||
}
|
||||
},
|
||||
projectNoteDecorationsSpec
|
||||
);
|
||||
}
|
||||
|
|
@ -54,6 +54,7 @@ import { FilterService } from './services/FilterService';
|
|||
import { ViewStateManager } from './services/ViewStateManager';
|
||||
import { createTaskLinkOverlay, dispatchTaskUpdate } from './editor/TaskLinkOverlay';
|
||||
import { createReadingModeTaskLinkProcessor } from './editor/ReadingModeTaskLinkProcessor';
|
||||
import { createProjectNoteDecorations } from './editor/ProjectNoteDecorations';
|
||||
import { DragDropManager } from './utils/DragDropManager';
|
||||
import { ICSSubscriptionService } from './services/ICSSubscriptionService';
|
||||
import { MigrationService } from './services/MigrationService';
|
||||
|
|
@ -265,6 +266,9 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
// Register essential editor extensions (now safe after layout ready)
|
||||
this.registerEditorExtension(createTaskLinkOverlay(this));
|
||||
|
||||
// Register project note decorations for live preview
|
||||
this.registerEditorExtension(createProjectNoteDecorations(this));
|
||||
|
||||
// Register reading mode task link processor
|
||||
this.registerMarkdownPostProcessor(createReadingModeTaskLinkProcessor(this));
|
||||
|
||||
|
|
|
|||
87
src/services/ProjectSubtasksService.ts
Normal file
87
src/services/ProjectSubtasksService.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { TFile } from 'obsidian';
|
||||
import TaskNotesPlugin from '../main';
|
||||
import { TaskInfo } from '../types';
|
||||
|
||||
export class ProjectSubtasksService {
|
||||
private plugin: TaskNotesPlugin;
|
||||
|
||||
constructor(plugin: TaskNotesPlugin) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tasks that reference this file as a project
|
||||
*/
|
||||
async getTasksLinkedToProject(projectFile: TFile): Promise<TaskInfo[]> {
|
||||
try {
|
||||
const allTasks = await this.plugin.cacheManager.getAllTasks();
|
||||
const projectFileName = projectFile.basename;
|
||||
const projectPath = projectFile.path;
|
||||
|
||||
return allTasks.filter(task => {
|
||||
if (!task.projects || task.projects.length === 0) return false;
|
||||
|
||||
return task.projects.some(project => {
|
||||
if (!project || typeof project !== 'string' || project.trim() === '') return false;
|
||||
|
||||
// Check for wikilink format [[Note Name]]
|
||||
if (project.startsWith('[[') && project.endsWith(']]')) {
|
||||
const linkedNoteName = project.slice(2, -2).trim();
|
||||
if (!linkedNoteName) return false;
|
||||
|
||||
// Try to resolve the link using Obsidian's metadata cache
|
||||
const resolvedFile = this.plugin.app.metadataCache.getFirstLinkpathDest(linkedNoteName, '');
|
||||
if (resolvedFile && resolvedFile.path === projectFile.path) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback to string matching
|
||||
return linkedNoteName === projectFileName || linkedNoteName === projectPath;
|
||||
}
|
||||
|
||||
// Check for plain text match
|
||||
const trimmedProject = String(project).trim();
|
||||
return trimmedProject === projectFileName || trimmedProject === projectPath;
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting tasks linked to project:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort tasks by priority and status
|
||||
*/
|
||||
sortTasks(tasks: TaskInfo[]): TaskInfo[] {
|
||||
return tasks.sort((a, b) => {
|
||||
// First sort by completion status (incomplete first)
|
||||
const aCompleted = this.plugin.statusManager.isCompletedStatus(a.status);
|
||||
const bCompleted = this.plugin.statusManager.isCompletedStatus(b.status);
|
||||
|
||||
if (aCompleted !== bCompleted) {
|
||||
return aCompleted ? 1 : -1;
|
||||
}
|
||||
|
||||
// Then sort by priority
|
||||
const aPriorityWeight = this.plugin.priorityManager.getPriorityWeight(a.priority);
|
||||
const bPriorityWeight = this.plugin.priorityManager.getPriorityWeight(b.priority);
|
||||
|
||||
if (aPriorityWeight !== bPriorityWeight) {
|
||||
return bPriorityWeight - aPriorityWeight; // Higher priority first
|
||||
}
|
||||
|
||||
// Then sort by due date (earliest first)
|
||||
if (a.due && b.due) {
|
||||
return new Date(a.due).getTime() - new Date(b.due).getTime();
|
||||
} else if (a.due) {
|
||||
return -1; // Tasks with due dates come first
|
||||
} else if (b.due) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Finally sort by title
|
||||
return a.title.localeCompare(b.title);
|
||||
});
|
||||
}
|
||||
}
|
||||
128
styles/project-note-subtasks.css
Normal file
128
styles/project-note-subtasks.css
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* Project Note Subtasks Component
|
||||
*
|
||||
* Styles for the subtask display area that appears in project notes
|
||||
* via CodeMirror decorations in live preview mode. Uses BEM methodology.
|
||||
*/
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin: 1.5em 0;
|
||||
padding: 1em;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
background-color: var(--background-secondary);
|
||||
clear: both;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.1em;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0.8em 0;
|
||||
color: var(--text-normal);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__title:hover {
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__title::before {
|
||||
content: "▼";
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.8em;
|
||||
transition: transform 0.2s ease;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__title.collapsed::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5em;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__list.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task {
|
||||
/* Task card wrapper */
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task .task-card {
|
||||
/* Enhance task card for subtask display */
|
||||
background-color: var(--background-primary);
|
||||
border-left: 3px solid var(--accent-color);
|
||||
padding: 0.6em 0.8em;
|
||||
border-radius: var(--radius-s);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task .task-card:hover {
|
||||
transform: translateX(2px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task .task-card--completed {
|
||||
opacity: 0.7;
|
||||
border-left-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task .task-card--completed:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.tasknotes-plugin .project-note-subtasks {
|
||||
margin: 1em 0;
|
||||
padding: 0.8em;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__title {
|
||||
font-size: 1em;
|
||||
margin-bottom: 0.6em;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__list {
|
||||
gap: 0.3em;
|
||||
}
|
||||
|
||||
.tasknotes-plugin .project-note-subtasks__task .task-card {
|
||||
padding: 0.5em 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
.theme-dark .tasknotes-plugin .project-note-subtasks {
|
||||
background-color: var(--background-secondary);
|
||||
border-color: var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.theme-dark .tasknotes-plugin .project-note-subtasks__task .task-card {
|
||||
background-color: var(--background-primary);
|
||||
border-left-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.theme-dark .tasknotes-plugin .project-note-subtasks__task .task-card--completed {
|
||||
border-left-color: var(--text-muted);
|
||||
}
|
||||
Loading…
Reference in a new issue