Adds first-class subtask rendering and editing in the document-mode
plugin, plus a couple of polish fixes:
- New `subTaskRef` node — same shape as taskRef (content: inline*,
taskId + isDone attrs, identical NodeView wiring) but rendered with
a `.sub-task-ref` class so CSS indents it under the parent and
draws a short connector line. Subtask Enter at end creates another
subtask under the same parent (resolved via findParentTaskIdBefore);
Enter on an empty subtask deletes the task and outdents.
- Seeding (buildSeedDoc) now emits each parent followed by its
subTaskRefs from task.subTaskIds, so opening a context shows the
full task tree. migrateStoredDoc handles both taskRef and
subTaskRef. collectKnownTaskIds, reconcileTitlesFromDoc,
refreshTaskRef and isTaskRefFocused all walk both node types.
- appendMissingTask now routes subtasks to insertSubtaskByParent
(which places the new subTaskRef after the parent's existing
subtask group), so an externally-added subtask appears under
its parent instead of at the doc tail.
- Enter at the end of a parent taskRef now uses
positionAfterParentGroup so the new sibling lands past any
subtasks — previously it would interleave with the parent's
children.
- Done-toggle is now a squircle (`<rect rx="5">` instead of
`<circle>`) to match the shape used in the actual app's task list.
- Replace the HTML5 drag-API grip with a pointer-event-driven drag.
The native API was unreliable because the grip lives outside the
ProseMirror view (in document.body), and the editor's contentEditable
region interfered with dragstart on a sibling button. Pointer events
also give us setPointerCapture for stable tracking and a clean
click-vs-drag split (with a justDragged guard so the synthetic click
doesn't open the block menu right after a drop).
- Slash and block menus now add window.scrollX/Y when positioning,
so they stay anchored to the caret when the iframe is scrolled.
Previously the popovers used viewport-relative coords with
position:absolute and drifted off when the document scrolled.
- Task chips now render the app's <done-toggle> visual instead of a
native checkbox: faint outline circle with an animated checkmark
that fades in once done. Mirrors src/app/ui/done-toggle styling
(stroke widths, opacities, transitions) so chips feel like the
rest of the app.
Adds three editor UX improvements in the document-mode plugin iframe:
- Move up / Move down entries in the block menu, plus full HTML5
drag-to-reorder via the grip handle (with a horizontal drop
indicator between blocks).
- Slash and block menus flip above the caret when there is no
room below, avoiding clipped popovers near the viewport bottom.
- Inline-SVG Material icon fallback so the editor renders
correctly offline (drops the Google Fonts dependency).
Two issues:
1. Enter/Backspace inside a task chip had no special handling. Enter
would split a taskRef into two with the same taskId (broken).
Backspace at start could merge the chip's content into the previous
block, detaching the title from its task entity.
2. The block hover gutter sat outside #editor-root. Moving the mouse
from a block toward the gutter crossed an unwatched gap, firing
mouseleave on the root and hiding the gutter before the mouse
reached it.
Fixes:
- TaskRefNode.addKeyboardShortcuts:
- Enter at end of chip → addTask + insert new chip below; cursor
lands inside it. Async; chain runs after refreshTaskCache.
- Enter on an empty chip → convert to paragraph + deleteTask.
- Enter in middle → swallow (no-op for POC).
- Backspace at start of empty chip → deleteTask + remove chip.
- Backspace at start of non-empty chip → swallow (no merge).
- #editor-root padding-left bumped to 60px so the gutter (positioned
52px left of each block) renders inside the root — the mouse stays
inside the root the whole time.
- .block-gutter adds padding-right so its hit area extends rightward
toward the block, and uses [style*='flex'] as the visibility cue
rather than the previous brittle hover chain.
Task chips were atom nodes that just displayed the title and floated as
cards, with extra block spacing between them. Now they're content-bearing
nodes whose inline content IS the task title — typing inside one edits
the linked task.
- taskRef switches from atom to content: 'inline*', exposes contentDOM
on the .title span so ProseMirror manages the editable content.
- isDone moves to an attr (instead of read-from-cache) so undo/redo
carries it; the checkbox dispatches a setNodeAttribute transaction.
- buildSeedDoc populates each taskRef with the task title from cache.
- migrateStoredDoc walks older atom-style stored docs and backfills
content from the cache so they load under the new schema.
- Title write-back: walking the doc on each onUpdate, per-task debounced
updateTask. pendingTitleWrites + isTaskRefFocused guard the
reconciler from clobbering an active edit with the echo of our own
write (or an unrelated ANY_TASK_UPDATE).
- ANY_TASK_UPDATE now refreshes only the affected node via a precise
replaceWith on the taskRef's inline range — preserves cursor on
surrounding blocks.
- CSS: zero margin between consecutive taskRefs, baseline-aligned
checkbox, no card background — the chip reads as a list item.
Layer Notion-like interaction on top of the TipTap editor:
- Block hover gutter on the left margin with `+` (insert below + open
slash menu) and `⋮⋮` (select node and open block menu) buttons.
- Bubble menu on text selection with Bold / Italic / Strike / Code
(via @tiptap/extension-bubble-menu).
- Slash menu shows icons next to labels, supports keyboard nav, and
now includes bullet list, numbered list, quote, code block, and
divider in addition to paragraph / headings / new task.
- Block menu (from ⋮⋮) offers turn-into for paragraph + H1-H3,
duplicate, and delete.
- Tighter CSS — Material Icons via Google Fonts, cleaner spacing
matching Notion-style density, selectednode highlight on taskRef.
Document mode is now remembered per context across sessions. Toggling
the header button writes the ctxId to enabledCtxIds in the shared blob;
on app start (and on every WORK_CONTEXT_CHANGE), the background script
auto-shows or -closes the embed based on whether the active context is
enabled.
The editor's debounced save now does a read-modify-write so it doesn't
clobber enabledCtxIds (or any other field future code adds to the blob).
Previously clicking the button only enabled the embed; user was stuck
in document mode with no way back to the task list. Track isShown in
memory and toggle showInWorkContext / closeWorkContextView. State is
not persisted — restart returns to the task list.
A bundled plugin that replaces the work-view task list with a TipTap
editor for projects and the TODAY tag. Task references are read-only
chips with a checkbox; everything else is normal ProseMirror content
(paragraphs, headings, dividers) plus a simple slash menu for inserts.
- Iframe plugin scaffolded under packages/plugin-dev/document-mode with
esbuild bundling. The editor.js bundle is inlined into index.html
because the blob-URL iframe can't resolve relative scripts.
- background.ts registers a work-context header button with
showFor: ['PROJECT', 'TODAY']; click calls showInWorkContext().
- editor.ts: TipTap (core + StarterKit + Placeholder) with a custom
taskRef atom node-view (checkbox + title from getTasks() cache).
Subscribes to WORK_CONTEXT_CHANGE for nav and ANY_TASK_UPDATE for
title/done-state refresh. Persists per-ctx ProseMirror JSON inside
the existing single plugin blob ({ docs: { [ctxId]: doc } }) with
5 s debounced save and a pagehide flush.
- Plugin auto-bundled by packages/plugin-dev/scripts/build-all.js and
registered in BUNDLED_PLUGIN_PATHS.
Out of scope for the POC (documented in the plan): inline-editable
task titles, keyed persistDataSynced API, removal of in-tree
document-mode, data migration, Electron beforeUnload hook.