From 4f68f82e962a1c1c889955a9dc75046d02f0e61a Mon Sep 17 00:00:00 2001 From: Art Malanok Date: Tue, 26 May 2026 17:08:12 -0700 Subject: [PATCH] v0.1.4: address Obsidian Community automated review scorecard (code + styles) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrated from the legacy obsidianmd/obsidian-releases PR workflow to the new https://community.obsidian.md portal. The new automated scan flagged 128 findings on v0.1.3; this commit addresses the code, style, and infrastructure findings. Code (TypeScript) - Replace document.createElement("div"|"span"|"input"|"datalist"| "option") with Obsidian's createDiv/createSpan/createEl helpers (~30 call sites across src/views/ and src/interactions/). - Replace document.createElementNS(svg-ns, ...) with createSvg(). - Replace bare document.X / document.querySelector / document.createTextNode with activeDocument.X for popout window compatibility. - Replace setTimeout/clearTimeout/window.setTimeout/window.clearTimeout with activeWindow.setTimeout/clearTimeout (7 call sites). - Introduce TaskFrontmatter interface in src/types.ts and narrow all metadataCache.getFileCache(...).frontmatter accesses to it — fixes the bulk of no-unsafe-* lint warnings. - Type-safe loadData() in main.ts via Partial. - stripWikilinks() now accepts unknown and narrows internally. - Use configured inlineTaskTag in the inline task regex (the `tag` parameter was declared-but-unused). - Prefix the unused sourceFile parameter on updateStub with `_`. Styles - Remove all !important declarations (5 occurrences); bump specificity via .ot-view parent selector instead. - Merge the duplicate .ot-task-row definition and guard touch-target overrides under @media (pointer: coarse). - Expand #fff -> #ffffff. Infrastructure - Add CONTRIBUTING.md (fixes "missing contributing guide" hygiene flag on the portal scorecard). Deferred for follow-up - package-lock.json — couldn't generate from this session (npmjs.org unreachable). Run `npm install` locally and commit the lockfile to clear the last "no lockfile" finding. - .github/workflows/release.yml — needs `workflow` OAuth scope to push; will be added in a follow-up commit. --- CONTRIBUTING.md | 64 +++++++++++++++++++++++++++++ manifest.json | 2 +- package.json | 2 +- src/data/frontmatterWriter.ts | 15 ++++--- src/data/inlineTaskWatcher.ts | 38 ++++++++++------- src/data/taskStore.ts | 35 +++++++++------- src/interactions/checkboxHandler.ts | 8 ++-- src/interactions/dragManager.ts | 12 +++--- src/interactions/inlineCreator.ts | 14 +++---- src/main.ts | 2 +- src/settings.ts | 4 +- src/types.ts | 26 ++++++++++++ src/views/accountGroup.ts | 6 +-- src/views/bucketComponent.ts | 12 +++--- src/views/taskRow.ts | 23 +++++------ src/views/taskView.ts | 16 ++++---- styles.css | 31 ++++++-------- versions.json | 3 +- 18 files changed, 207 insertions(+), 106 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..341cdf9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +# Contributing to Minimal Task Board + +Thanks for taking a look! This plugin is small and focused. Contributions, bug reports, and feature ideas are all welcome. + +## Reporting bugs + +Open an issue at https://github.com/selectstarfromusers/obsidian-task-manager/issues with: + +- Obsidian version (`Settings → About`) +- Plugin version (in `Settings → Community plugins → Minimal Task Board`) +- A short description of what you saw vs what you expected +- A minimal repro: a few example task files (or screenshots) and the bucket/secondary-grouping settings you had configured +- The full text of any error from `View → Toggle developer tools → Console` + +## Requesting features + +Open an issue with the `enhancement` label and describe the workflow you're trying to support. Concrete examples beat abstract requests — "I want to triage 50+ tasks per day across 4 customer accounts" gives much better context than "make filtering better". + +## Local development + +```bash +git clone https://github.com/selectstarfromusers/obsidian-task-manager +cd obsidian-task-manager +npm install +npm run dev # watches src/ and rebuilds on change +``` + +Then symlink the plugin folder into a test vault's `.obsidian/plugins/minimal-task-board/`: + +```bash +mkdir -p /path/to/test-vault/.obsidian/plugins +ln -s "$(pwd)" /path/to/test-vault/.obsidian/plugins/minimal-task-board +``` + +Enable the plugin under `Settings → Community plugins`, then `Ctrl/Cmd+P` → `Reload app without saving` after rebuilds. + +### Project layout + +- `src/main.ts` — plugin entry, view registration, settings load/save +- `src/types.ts` — shared type definitions (`TaskItem`, `BucketGroup`, `TasksPluginSettings`, `TaskFrontmatter`) +- `src/data/` — task store, inline-task watcher, frontmatter writer +- `src/views/` — board / focus renderers, bucket component, task row +- `src/interactions/` — drag/drop, checkbox toggle, inline-task creation +- `src/settings.ts` — settings tab UI +- `styles.css` — all styles (CSS variables and specificity, no `!important`) + +### Style and review notes + +- Use Obsidian's DOM helpers (`createDiv`, `createSpan`, `createEl`, `createSvg`, `activeDocument`, `activeWindow.setTimeout`, `this.registerDomEvent`) rather than raw DOM globals — the Obsidian community automated review enforces this. +- Frontmatter reads should narrow `frontmatter` to the `TaskFrontmatter` interface in `src/types.ts` rather than using `any`. +- Keep styles in `styles.css`, not inline `el.style.X` assignments. +- Don't introduce new third-party runtime dependencies without discussing first — the plugin is intentionally lightweight. + +## Pull requests + +1. Fork, branch off `main`, keep the change focused +2. `npm run build` must succeed and produce a clean `main.js` +3. Bump the version in `manifest.json` and add a matching entry in `versions.json` (use semver — patch bumps for fixes, minor for new features) +4. Open the PR with a brief summary of the change, screenshots if UI, and any new settings explained +5. Be patient — this is a hobby project, I'll get to it + +## License + +By contributing you agree your changes are released under the MIT License (see `LICENSE`). diff --git a/manifest.json b/manifest.json index 3fbddaa..9aade2c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "minimal-task-board", "name": "Minimal Task Board", - "version": "0.1.3", + "version": "0.1.4", "minAppVersion": "1.10.0", "description": "A clean, minimal task board with customizable buckets, secondary grouping, and inline task tracking.", "author": "Art Malanok", diff --git a/package.json b/package.json index 967ddc1..35abe9e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minimal-task-board", - "version": "0.1.3", + "version": "0.1.4", "private": true, "scripts": { "build": "node esbuild.config.mjs", diff --git a/src/data/frontmatterWriter.ts b/src/data/frontmatterWriter.ts index 84b32d8..068898f 100644 --- a/src/data/frontmatterWriter.ts +++ b/src/data/frontmatterWriter.ts @@ -2,6 +2,9 @@ import { App, TFile } from "obsidian"; /** * Safe frontmatter updates using Obsidian's processFrontMatter API. + * + * Obsidian's callback is typed as (frontmatter: any), so we narrow it to + * Record inside each method to satisfy no-unsafe-* lints. */ export class FrontmatterWriter { private app: App; @@ -12,15 +15,15 @@ export class FrontmatterWriter { /** Update a single frontmatter property */ async setProperty(file: TFile, key: string, value: unknown): Promise { - await this.app.fileManager.processFrontMatter(file, (fm) => { - fm[key] = value; + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { + rawFm[key] = value; }); } /** Toggle a boolean frontmatter property */ async toggleBoolean(file: TFile, key: string): Promise { - await this.app.fileManager.processFrontMatter(file, (fm) => { - fm[key] = !fm[key]; + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { + rawFm[key] = !rawFm[key]; }); } @@ -29,9 +32,9 @@ export class FrontmatterWriter { file: TFile, updates: Record ): Promise { - await this.app.fileManager.processFrontMatter(file, (fm) => { + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { for (const [key, value] of Object.entries(updates)) { - fm[key] = value; + rawFm[key] = value; } }); } diff --git a/src/data/inlineTaskWatcher.ts b/src/data/inlineTaskWatcher.ts index b8b7f1d..a790a5f 100644 --- a/src/data/inlineTaskWatcher.ts +++ b/src/data/inlineTaskWatcher.ts @@ -1,5 +1,5 @@ import { App, TFile, CachedMetadata, EventRef } from "obsidian"; -import { TasksPluginSettings } from "../types"; +import { TasksPluginSettings, TaskFrontmatter } from "../types"; interface TrackedTask { text: string; @@ -44,7 +44,7 @@ export class InlineTaskWatcher { private markRecentlyWritten(path: string): void { this.recentlyWritten.add(path); - setTimeout(() => { + activeWindow.setTimeout(() => { this.recentlyWritten.delete(path); }, 500); } @@ -90,10 +90,16 @@ export class InlineTaskWatcher { const lines = content.split("\n"); const tasks: TrackedTask[] = []; + // Build a regex from the configured tag so a user-overridden tag is honored. + const escapedTag = tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const taskLineRe = new RegExp( + `^[\\s]*-\\s+\\[([ xX])\\]\\s+(.+?)(?:\\s+#${escapedTag})\\s*$` + ); + for (let i = 0; i < lines.length; i++) { const line = lines[i]; - // Match: - [ ] text #task - const match = line.match(/^[\s]*-\s+\[([ xX])\]\s+(.+?)(?:\s+#task)\s*$/); + // Match: - [ ] text # + const match = line.match(taskLineRe); if (match) { const done = match[1].toLowerCase() === "x"; let text = match[2].trim(); @@ -119,7 +125,7 @@ export class InlineTaskWatcher { const folder = s.taskFolder; return this.app.vault.getFiles().filter((f) => { if (!f.path.startsWith(folder + "/") || f.extension !== "md") return false; - const fm = this.app.metadataCache.getFileCache(f)?.frontmatter; + const fm = this.app.metadataCache.getFileCache(f)?.frontmatter as TaskFrontmatter | undefined; return fm?.source === "inline" && fm?.source_file === sourceFile.path; }); } @@ -132,7 +138,7 @@ export class InlineTaskWatcher { // Build a map of existing stubs by source_line const stubByLine = new Map(); for (const stub of existingStubs) { - const fm = this.app.metadataCache.getFileCache(stub)?.frontmatter; + const fm = this.app.metadataCache.getFileCache(stub)?.frontmatter as TaskFrontmatter | undefined; const line = fm?.source_line; if (typeof line === "number") { stubByLine.set(line, stub); @@ -150,7 +156,7 @@ export class InlineTaskWatcher { if (!stub) { stub = existingStubs.find((s) => { if (matchedStubs.has(s.path)) return false; - const fm = this.app.metadataCache.getFileCache(s)?.frontmatter; + const fm = this.app.metadataCache.getFileCache(s)?.frontmatter as TaskFrontmatter | undefined; return fm?.action === task.text || fm?.source_line === task.lineNum; }); } @@ -173,8 +179,8 @@ export class InlineTaskWatcher { } } - private async updateStub(stub: TFile, task: TrackedTask, sourceFile: TFile): Promise { - const fm = this.app.metadataCache.getFileCache(stub)?.frontmatter; + private async updateStub(stub: TFile, task: TrackedTask, _sourceFile: TFile): Promise { + const fm = this.app.metadataCache.getFileCache(stub)?.frontmatter as TaskFrontmatter | undefined; if (!fm) return; const s = this.settings(); @@ -189,7 +195,8 @@ export class InlineTaskWatcher { if (!needsUpdate) return; this.markRecentlyWritten(stub.path); - await this.app.fileManager.processFrontMatter(stub, (frontmatter) => { + await this.app.fileManager.processFrontMatter(stub, (rawFm: Record) => { + const frontmatter = rawFm as TaskFrontmatter; frontmatter.action = task.text; frontmatter.done = task.done; frontmatter.source_line = task.lineNum; @@ -238,11 +245,12 @@ export class InlineTaskWatcher { const groupProp = s.secondaryGroupProperty; let groupValue = task.secondaryValue; if (!groupValue) { - const sourceFm = this.app.metadataCache.getFileCache(sourceFile)?.frontmatter; + const sourceFm = this.app.metadataCache.getFileCache(sourceFile)?.frontmatter as TaskFrontmatter | undefined; const fmValue = sourceFm?.[groupProp]; - groupValue = fmValue - ? String(fmValue).replace(/\[\[|\]\]/g, "") - : ""; + groupValue = + typeof fmValue === "string" && fmValue + ? fmValue.replace(/\[\[|\]\]/g, "") + : ""; } const defaultBucket = s.buckets.find((b) => b.id === s.defaultBucketId)?.name ?? ""; @@ -279,7 +287,7 @@ export class InlineTaskWatcher { */ async syncDoneToSource(stubFile: TFile, done: boolean): Promise { const cache = this.app.metadataCache.getFileCache(stubFile); - const fm = cache?.frontmatter; + const fm = cache?.frontmatter as TaskFrontmatter | undefined; if (!fm?.source_file || fm.source !== "inline") return; const sourceFile = this.app.vault.getAbstractFileByPath(fm.source_file); diff --git a/src/data/taskStore.ts b/src/data/taskStore.ts index b83ffe5..84e2432 100644 --- a/src/data/taskStore.ts +++ b/src/data/taskStore.ts @@ -5,13 +5,14 @@ import { BucketGroup, SubGroup, TasksPluginSettings, + TaskFrontmatter, } from "../types"; type ChangeCallback = () => void; -function stripWikilinks(value: string | undefined | null): string { - if (!value) return ""; - return String(value).replace(/^\[\[/, "").replace(/\]\]$/, ""); +function stripWikilinks(value: unknown): string { + if (typeof value !== "string" || !value) return ""; + return value.replace(/^\[\[/, "").replace(/\]\]$/, ""); } export class TaskStore { @@ -100,17 +101,17 @@ export class TaskStore { for (const file of taskFiles) { const cache = this.app.metadataCache.getFileCache(file); - const fm = cache?.frontmatter; + const fm = cache?.frontmatter as TaskFrontmatter | undefined; if (!fm || fm.type !== "task") continue; // P1: Property name normalization — read bucket with lowercase key tasks.push({ file, - action: fm.action ?? "", - account: stripWikilinks(fm.account), - bucket: fm[bucketProp] ?? "", - source: fm.source ?? "", - sourceNote: stripWikilinks(fm.source_note), + action: typeof fm.action === "string" ? fm.action : "", + account: stripWikilinks(typeof fm.account === "string" ? fm.account : ""), + bucket: typeof fm[bucketProp] === "string" ? (fm[bucketProp] as string) : "", + source: typeof fm.source === "string" ? fm.source : "", + sourceNote: stripWikilinks(typeof fm.source_note === "string" ? fm.source_note : ""), date: fm.date ? String(fm.date) : "", deadline: fm.deadline ? String(fm.deadline) : "", done: fm.done === true, @@ -253,7 +254,7 @@ export class TaskStore { const newIndex = new Map>(); for (const task of this.tasks) { const cache = this.app.metadataCache.getFileCache(task.file); - const fm = cache?.frontmatter; + const fm = cache?.frontmatter as TaskFrontmatter | undefined; const sourceFile = fm?.source_file; if (sourceFile && typeof sourceFile === "string") { if (!newIndex.has(sourceFile)) { @@ -266,21 +267,23 @@ export class TaskStore { } async toggleDone(file: TFile): Promise { - await this.app.fileManager.processFrontMatter(file, (fm) => { + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { + const fm = rawFm as TaskFrontmatter; fm.done = !fm.done; }); } async toggleDoneWithSync(file: TFile): Promise { let newDone = false; - await this.app.fileManager.processFrontMatter(file, (fm) => { + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { + const fm = rawFm as TaskFrontmatter; fm.done = !fm.done; newDone = fm.done; }); // Sync checkbox back to the source note for inline tasks const cache = this.app.metadataCache.getFileCache(file); - const fm = cache?.frontmatter; + const fm = cache?.frontmatter as TaskFrontmatter | undefined; if (fm?.source === "inline" && fm?.source_file) { const sourceFile = this.app.vault.getAbstractFileByPath(fm.source_file); if (sourceFile instanceof TFile) { @@ -304,13 +307,15 @@ export class TaskStore { async moveToBucket(file: TFile, bucketName: string): Promise { const settings = this.getSettings(); // P1: Normalize property name to lowercase - await this.app.fileManager.processFrontMatter(file, (fm) => { + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { + const fm = rawFm as TaskFrontmatter; fm[settings.bucketProperty.toLowerCase()] = bucketName; }); } async reorder(file: TFile, newSortOrder: number): Promise { - await this.app.fileManager.processFrontMatter(file, (fm) => { + await this.app.fileManager.processFrontMatter(file, (rawFm: Record) => { + const fm = rawFm as TaskFrontmatter; fm.sort_order = newSortOrder; }); } diff --git a/src/interactions/checkboxHandler.ts b/src/interactions/checkboxHandler.ts index 0e4c87c..87a26bd 100644 --- a/src/interactions/checkboxHandler.ts +++ b/src/interactions/checkboxHandler.ts @@ -48,21 +48,21 @@ export class CheckboxHandler { // Step 2: fill the circle after 300ms timeouts.push( - window.setTimeout(() => { + activeWindow.setTimeout(() => { checkbox?.classList.add("checked"); }, 300) ); // Step 3: height collapse after 1500ms timeouts.push( - window.setTimeout(() => { + activeWindow.setTimeout(() => { rowElement.classList.add("done"); }, 1500) ); // Step 4: write frontmatter after 1700ms timeouts.push( - window.setTimeout(() => { + activeWindow.setTimeout(() => { this.pendingTimeouts.delete(key); void this.callbacks.onToggleDone(task.file).then(() => { // Signal animation end so the view can resume re-renders @@ -87,7 +87,7 @@ export class CheckboxHandler { const timeouts = this.pendingTimeouts.get(key); if (timeouts) { for (const id of timeouts) { - window.clearTimeout(id); + activeWindow.clearTimeout(id); } this.pendingTimeouts.delete(key); } diff --git a/src/interactions/dragManager.ts b/src/interactions/dragManager.ts index 879dc4e..6280c4d 100644 --- a/src/interactions/dragManager.ts +++ b/src/interactions/dragManager.ts @@ -44,13 +44,13 @@ export class DragManager { const targetRow = this.findNearestRow(rows, event.clientY); if (targetRow) { - this.dropIndicator = document.createElement("div"); + this.dropIndicator = createDiv(); this.dropIndicator.className = `${CLS}-drop-indicator`; targetRow.parentElement?.insertBefore(this.dropIndicator, targetRow); } else if (rows.length > 0) { // Cursor is below all rows — place indicator after the last row const lastRow = rows[rows.length - 1]; - this.dropIndicator = document.createElement("div"); + this.dropIndicator = createDiv(); this.dropIndicator.className = `${CLS}-drop-indicator`; lastRow.parentElement?.insertBefore( this.dropIndicator, @@ -89,7 +89,7 @@ export class DragManager { ); if (accountGroup && this.callbacks.onMoveToGroup) { const newGroup = accountGroup.dataset.account; - const sourceRow = document.querySelector( + const sourceRow = activeDocument.querySelector( `.${CLS}-task-row[data-path="${this.draggedTask.file.path}"]` ); const sourceGroup = sourceRow @@ -194,7 +194,7 @@ export class DragManager { this.dropIndicator = null; } // Also clean up any orphaned indicators in the DOM - document + activeDocument .querySelectorAll(`.${CLS}-drop-indicator`) .forEach((el) => el.remove()); } @@ -202,10 +202,10 @@ export class DragManager { private cleanup(): void { this.removeDropIndicator(); - document + activeDocument .querySelectorAll(".dragging") .forEach((el) => el.classList.remove("dragging")); - document + activeDocument .querySelectorAll(".drag-over") .forEach((el) => el.classList.remove("drag-over")); } diff --git a/src/interactions/inlineCreator.ts b/src/interactions/inlineCreator.ts index eface9d..0f89de6 100644 --- a/src/interactions/inlineCreator.ts +++ b/src/interactions/inlineCreator.ts @@ -29,11 +29,11 @@ export class InlineCreator { } const s = this.settings(); - const wrapper = document.createElement("div"); + const wrapper = createDiv(); wrapper.className = `${CLS}-inline-input-wrapper`; // Action text input - const actionInput = document.createElement("input"); + const actionInput = createEl("input"); actionInput.type = "text"; actionInput.className = `${CLS}-inline-input`; actionInput.placeholder = "Task description..."; @@ -43,7 +43,7 @@ export class InlineCreator { const groupProp = s.secondaryGroupProperty; const datalistId = `${CLS}-group-suggestions-${Date.now()}`; - const groupInput = document.createElement("input"); + const groupInput = createEl("input"); groupInput.type = "text"; groupInput.className = `${CLS}-inline-input ${CLS}-inline-group-input`; groupInput.placeholder = `${groupProp}...`; @@ -51,10 +51,10 @@ export class InlineCreator { wrapper.appendChild(groupInput); // Datalist for autocomplete suggestions - const datalist = document.createElement("datalist"); + const datalist = createEl("datalist"); datalist.id = datalistId; for (const value of existingGroups) { - const option = document.createElement("option"); + const option = createEl("option"); option.value = value; datalist.appendChild(option); } @@ -90,8 +90,8 @@ export class InlineCreator { // Blur handler — remove wrapper if both inputs are empty after a short delay const handleBlur = () => { - setTimeout(() => { - const activeEl = document.activeElement; + activeWindow.setTimeout(() => { + const activeEl = activeDocument.activeElement; if ( activeEl !== actionInput && activeEl !== groupInput && diff --git a/src/main.ts b/src/main.ts index b30c7b4..2c1d1e9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -129,7 +129,7 @@ export default class TasksPlugin extends Plugin { } async loadSettings(): Promise { - const loaded = await this.loadData(); + const loaded = (await this.loadData()) as Partial | null; this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded); if (loaded?.display) { this.settings.display = Object.assign( diff --git a/src/settings.ts b/src/settings.ts index e7354ce..4c60a14 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -147,13 +147,13 @@ export class TasksSettingTab extends PluginSettingTab { const taskCount = this.countTasksInBucket(bucket); delBtn.textContent = `Confirm? (${taskCount} task${taskCount !== 1 ? "s" : ""})`; confirmPending = true; - confirmTimeout = setTimeout(() => { + confirmTimeout = activeWindow.setTimeout(() => { delBtn.textContent = "\u00d7"; confirmPending = false; }, 3000); return; } - if (confirmTimeout) clearTimeout(confirmTimeout); + if (confirmTimeout) activeWindow.clearTimeout(confirmTimeout); this.plugin.settings.buckets.splice(index, 1); this.plugin.settings.buckets.forEach((b, i) => (b.sortOrder = i)); if (this.plugin.settings.defaultBucketId === bucket.id) { diff --git a/src/types.ts b/src/types.ts index 2a369b3..0e258a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -88,3 +88,29 @@ export const TASKS_VIEW_TYPE = "minimal-task-board-view"; /** CSS class prefix */ export const CLS = "ot"; + +/** + * Frontmatter shape for a stub file managed by this plugin. + * + * Obsidian's MetadataCache.frontmatter is typed as Record, which + * triggers no-unsafe-* lint warnings on every property access. We narrow it to + * this interface so reads are type-checked. + * + * All fields are optional because user-edited frontmatter is best-effort. + * Dynamic bucket/group properties (whose key comes from settings) are captured + * by the index signature. + */ +export interface TaskFrontmatter { + action?: string; + source?: string; + source_file?: string; + source_line?: number; + source_note?: string; + date?: string; + deadline?: string; + done?: boolean; + sort_order?: number; + type?: string; + /** Catch-all for the configurable bucketProperty / secondaryGroupProperty. */ + [key: string]: unknown; +} diff --git a/src/views/accountGroup.ts b/src/views/accountGroup.ts index 9d38d8e..0983f70 100644 --- a/src/views/accountGroup.ts +++ b/src/views/accountGroup.ts @@ -8,13 +8,13 @@ export function createAccountGroup( callbacks: TaskRowCallbacks, collapsible: boolean ): HTMLElement { - const group = document.createElement("div"); + const group = createDiv(); group.className = `${CLS}-account-group`; if (key) { group.dataset.account = key; - const header = document.createElement("div"); + const header = createDiv(); header.className = `${CLS}-account-header`; header.textContent = key; @@ -27,7 +27,7 @@ export function createAccountGroup( group.appendChild(header); } - const tasksContainer = document.createElement("div"); + const tasksContainer = createDiv(); tasksContainer.className = `${CLS}-account-tasks`; for (const task of tasks) { diff --git a/src/views/bucketComponent.ts b/src/views/bucketComponent.ts index 26a8051..ef7f6c5 100644 --- a/src/views/bucketComponent.ts +++ b/src/views/bucketComponent.ts @@ -15,20 +15,20 @@ export function createBucket( collapsible: boolean, callbacks: BucketCallbacks ): HTMLElement { - const bucket = document.createElement("div"); + const bucket = createDiv(); bucket.className = `${CLS}-bucket`; bucket.dataset.bucket = group.bucket.name; // Header - const header = document.createElement("div"); + const header = createDiv(); header.className = `${CLS}-bucket-header`; - const name = document.createElement("span"); + const name = createSpan(); name.className = `${CLS}-bucket-name`; name.textContent = group.bucket.name; header.appendChild(name); - const count = document.createElement("span"); + const count = createSpan(); count.className = `${CLS}-bucket-count`; count.textContent = String(group.totalCount); header.appendChild(count); @@ -36,7 +36,7 @@ export function createBucket( bucket.appendChild(header); // Tasks container - const tasksContainer = document.createElement("div"); + const tasksContainer = createDiv(); tasksContainer.className = `${CLS}-bucket-tasks`; if (secondaryGrouping) { @@ -67,7 +67,7 @@ export function createBucket( bucket.appendChild(tasksContainer); // Add task button - const addBtn = document.createElement("div"); + const addBtn = createDiv(); addBtn.className = `${CLS}-bucket-add`; addBtn.textContent = "Add a task"; addBtn.addEventListener("click", () => { diff --git a/src/views/taskRow.ts b/src/views/taskRow.ts index 18e8eab..a327bc5 100644 --- a/src/views/taskRow.ts +++ b/src/views/taskRow.ts @@ -29,12 +29,12 @@ function formatDeadline(deadline: string): string { } function createCheckboxSvg(done: boolean): SVGElement { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + const svg = createSvg("svg"); svg.setAttribute("width", "18"); svg.setAttribute("height", "18"); svg.setAttribute("viewBox", "0 0 18 18"); - const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + const rect = createSvg("rect"); rect.setAttribute("x", "1"); rect.setAttribute("y", "1"); rect.setAttribute("width", "16"); @@ -47,7 +47,7 @@ function createCheckboxSvg(done: boolean): SVGElement { svg.appendChild(rect); if (done) { - const check = document.createElementNS("http://www.w3.org/2000/svg", "path"); + const check = createSvg("path"); check.setAttribute("d", "M5.5 9.5l2 2 5-5"); check.setAttribute("stroke", "var(--background-primary, #fff)"); check.setAttribute("stroke-width", "1.5"); @@ -65,7 +65,7 @@ export function createTaskRow( display: DisplayConfig, callbacks: TaskRowCallbacks ): HTMLElement { - const row = document.createElement("div"); + const row = createDiv(); row.className = `${CLS}-task-row`; row.draggable = true; row.dataset.path = task.file.path; @@ -82,13 +82,13 @@ export function createTaskRow( } // Drag handle - const handle = document.createElement("div"); + const handle = createDiv(); handle.className = `${CLS}-drag-handle`; handle.textContent = "\u{2817}"; row.appendChild(handle); // Checkbox - const checkbox = document.createElement("div"); + const checkbox = createDiv(); checkbox.className = `${CLS}-checkbox`; checkbox.setAttribute("role", "checkbox"); checkbox.setAttribute("aria-checked", String(task.done)); @@ -101,18 +101,17 @@ export function createTaskRow( row.appendChild(checkbox); // Content - const content = document.createElement("div"); + const content = createDiv(); content.className = `${CLS}-content`; // Action text + source icon on same line - const actionLine = document.createElement("span"); + const actionLine = createSpan(); actionLine.className = `${CLS}-action`; - const actionText = document.createTextNode(task.action); - actionLine.appendChild(actionText); + actionLine.appendText(task.action); if (display.showSourceIcon && task.source && SOURCE_ICONS[task.source]) { - const icon = document.createElement("span"); + const icon = createSpan(); icon.className = `${CLS}-source-icon`; icon.textContent = ` ${SOURCE_ICONS[task.source]}`; actionLine.appendChild(icon); @@ -133,7 +132,7 @@ export function createTaskRow( } if (metaParts.length > 0) { - const meta = document.createElement("span"); + const meta = createSpan(); meta.className = `${CLS}-meta`; meta.textContent = metaParts.join(" \u{2022} "); content.appendChild(meta); diff --git a/src/views/taskView.ts b/src/views/taskView.ts index 3b1889f..18fa8e3 100644 --- a/src/views/taskView.ts +++ b/src/views/taskView.ts @@ -98,8 +98,8 @@ export class TaskView extends ItemView { // React to data changes (debounced to avoid re-render storms) let debounceTimer: ReturnType | null = null; this.store.onChange(() => { - if (debounceTimer) clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => { + if (debounceTimer) activeWindow.clearTimeout(debounceTimer); + debounceTimer = activeWindow.setTimeout(() => { // P0: Skip re-render while checkbox animation is in progress if (this.animating) return; this.renderView(contentArea); @@ -137,21 +137,21 @@ export class TaskView extends ItemView { this.boardRenderer.destroy(); this.boardRenderer = null; } - const emptyState = document.createElement("div"); + const emptyState = createDiv(); emptyState.className = `${CLS}-empty-state`; - const iconEl = document.createElement("div"); + const iconEl = createDiv(); iconEl.className = `${CLS}-empty-icon`; iconEl.textContent = "\u2610"; emptyState.appendChild(iconEl); - const titleEl = document.createElement("div"); + const titleEl = createDiv(); titleEl.className = `${CLS}-empty-title`; titleEl.textContent = "No tasks yet"; emptyState.appendChild(titleEl); - const hint1 = document.createElement("div"); + const hint1 = createDiv(); hint1.className = `${CLS}-empty-hint`; hint1.textContent = "Add #task to any checkbox in your notes to see it here."; emptyState.appendChild(hint1); - const hint2 = document.createElement("div"); + const hint2 = createDiv(); hint2.className = `${CLS}-empty-hint`; hint2.textContent = "Example: - [ ] send report [[project alpha]] #task"; emptyState.appendChild(hint2); @@ -227,7 +227,7 @@ export class TaskView extends ItemView { this.animating = true; this.checkboxHandler.handleToggle(task, row); // The checkbox handler uses a 1700ms delay; set a timeout to clear the flag - setTimeout(() => { + activeWindow.setTimeout(() => { this.animating = false; }, 1800); } diff --git a/styles.css b/styles.css index a8dbe4f..ffe4e10 100644 --- a/styles.css +++ b/styles.css @@ -212,14 +212,16 @@ /* ============================================================ TASK ROW — frameless ============================================================ */ -.ot-task-row { +.ot-view .ot-task-row { display: flex; align-items: flex-start; - padding: 4px 10px 4px 16px !important; + padding: 4px 10px 4px 16px; gap: 6px; border-bottom: 1px solid rgba(var(--background-modifier-border-rgb, 0, 0, 0), 0.03); transition: opacity 300ms ease, height 200ms ease; overflow: visible; + /* Enable long-press context menu on mobile (merged from duplicate selector at line 502) */ + -webkit-touch-callout: default; } .ot-task-row:last-child { @@ -313,9 +315,9 @@ transition: opacity 300ms ease; } -.ot-task-row.done { +.ot-view .ot-task-row.done { opacity: 0; - height: 0 !important; + height: 0; padding-top: 0; padding-bottom: 0; border-bottom: none; @@ -486,7 +488,7 @@ padding: 0 3px; border-radius: 10px; background: var(--color-red, #e03e3e); - color: #fff; + color: #ffffff; font-size: 8px; font-weight: 700; line-height: 10px; @@ -498,24 +500,17 @@ /* ============================================================ MOBILE IMPROVEMENTS ============================================================ */ -/* Enable long-press context menu on mobile */ -.ot-task-row { - -webkit-touch-callout: default; -} - +/* Mobile touch targets — coarse pointer = touch device */ @media (pointer: coarse) { - .ot-task-row { + .ot-view .ot-task-row { min-height: 44px; align-items: center; - padding: 8px 10px 8px 12px !important; + padding: 8px 10px 8px 12px; } - .ot-drag-handle { - display: none !important; - } - - .ot-task-row:hover .ot-drag-handle { - display: none !important; + .ot-view .ot-drag-handle, + .ot-view .ot-task-row:hover .ot-drag-handle { + display: none; } .ot-checkbox { diff --git a/versions.json b/versions.json index f83425d..fc7eb23 100644 --- a/versions.json +++ b/versions.json @@ -2,5 +2,6 @@ "0.1.0": "1.10.0", "0.1.1": "1.10.0", "0.1.2": "1.10.0", - "0.1.3": "1.10.0" + "0.1.3": "1.10.0", + "0.1.4": "1.10.0" }