v0.1.4: address Obsidian Community automated review scorecard (code + styles)

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<TasksPluginSettings>.
- 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.
This commit is contained in:
Art Malanok 2026-05-26 17:08:12 -07:00
parent e69a0bc2cd
commit 4f68f82e96
18 changed files with 207 additions and 106 deletions

64
CONTRIBUTING.md Normal file
View file

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

View file

@ -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",

View file

@ -1,6 +1,6 @@
{
"name": "minimal-task-board",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"scripts": {
"build": "node esbuild.config.mjs",

View file

@ -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<string, unknown> 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<void> {
await this.app.fileManager.processFrontMatter(file, (fm) => {
fm[key] = value;
await this.app.fileManager.processFrontMatter(file, (rawFm: Record<string, unknown>) => {
rawFm[key] = value;
});
}
/** Toggle a boolean frontmatter property */
async toggleBoolean(file: TFile, key: string): Promise<void> {
await this.app.fileManager.processFrontMatter(file, (fm) => {
fm[key] = !fm[key];
await this.app.fileManager.processFrontMatter(file, (rawFm: Record<string, unknown>) => {
rawFm[key] = !rawFm[key];
});
}
@ -29,9 +32,9 @@ export class FrontmatterWriter {
file: TFile,
updates: Record<string, unknown>
): Promise<void> {
await this.app.fileManager.processFrontMatter(file, (fm) => {
await this.app.fileManager.processFrontMatter(file, (rawFm: Record<string, unknown>) => {
for (const [key, value] of Object.entries(updates)) {
fm[key] = value;
rawFm[key] = value;
}
});
}

View file

@ -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 #<tag>
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<number, TFile>();
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<void> {
const fm = this.app.metadataCache.getFileCache(stub)?.frontmatter;
private async updateStub(stub: TFile, task: TrackedTask, _sourceFile: TFile): Promise<void> {
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<string, unknown>) => {
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<void> {
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);

View file

@ -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<string, Set<string>>();
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<void> {
await this.app.fileManager.processFrontMatter(file, (fm) => {
await this.app.fileManager.processFrontMatter(file, (rawFm: Record<string, unknown>) => {
const fm = rawFm as TaskFrontmatter;
fm.done = !fm.done;
});
}
async toggleDoneWithSync(file: TFile): Promise<void> {
let newDone = false;
await this.app.fileManager.processFrontMatter(file, (fm) => {
await this.app.fileManager.processFrontMatter(file, (rawFm: Record<string, unknown>) => {
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<void> {
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<string, unknown>) => {
const fm = rawFm as TaskFrontmatter;
fm[settings.bucketProperty.toLowerCase()] = bucketName;
});
}
async reorder(file: TFile, newSortOrder: number): Promise<void> {
await this.app.fileManager.processFrontMatter(file, (fm) => {
await this.app.fileManager.processFrontMatter(file, (rawFm: Record<string, unknown>) => {
const fm = rawFm as TaskFrontmatter;
fm.sort_order = newSortOrder;
});
}

View file

@ -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);
}

View file

@ -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<HTMLElement>(
const sourceRow = activeDocument.querySelector<HTMLElement>(
`.${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"));
}

View file

@ -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 &&

View file

@ -129,7 +129,7 @@ export default class TasksPlugin extends Plugin {
}
async loadSettings(): Promise<void> {
const loaded = await this.loadData();
const loaded = (await this.loadData()) as Partial<TasksPluginSettings> | null;
this.settings = Object.assign({}, DEFAULT_SETTINGS, loaded);
if (loaded?.display) {
this.settings.display = Object.assign(

View file

@ -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) {

View file

@ -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<string, any>, 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;
}

View file

@ -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) {

View file

@ -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", () => {

View file

@ -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);

View file

@ -98,8 +98,8 @@ export class TaskView extends ItemView {
// React to data changes (debounced to avoid re-render storms)
let debounceTimer: ReturnType<typeof setTimeout> | 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);
}

View file

@ -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 {

View file

@ -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"
}