Skip to content

fix(opensidian): unify content model and modernize UI components#1530

Merged
braden-w merged 15 commits intomainfrom
opencode/neon-forest
Mar 14, 2026
Merged

fix(opensidian): unify content model and modernize UI components#1530
braden-w merged 15 commits intomainfrom
opencode/neon-forest

Conversation

@braden-w
Copy link
Member

Opensidian's content editor was silently writing to a store nothing else reads. Meanwhile, the UI was built with raw HTML inputs and inline SVGs instead of shared components. This PR fixes both.

Content model unification

Document Y.Docs had two independent content stores—Y.Text('content') (used by handle.read()/write()) and Y.Array('timeline') (used by fs.writeFile()/readFile()). Content written through the editor was invisible to the filesystem API and vice versa.

// BEFORE: editor writes to Y.Text('content'), fs reads from Y.Array('timeline')
const handle = await ws.documents.files.content.open(id);
handle.read();          // → reads Y.Text('content') — stale/empty
handle.write('hello');  // → writes Y.Text('content') — invisible to fs

// AFTER: editor uses the same timeline store as the filesystem
await fs.content.read(id);        // → reads from timeline
await fs.content.write(id, data); // → writes to timeline

This is Phase 1 of the unification spec (specs/20260313T224500-unify-document-content-model.md). Phase 2 will promote the timeline into packages/workspace so handle.read()/write() itself becomes timeline-backed—making both paths equivalent.

Documentation updated across skills, READMEs, and JSDoc to mark handle.read()/write() and handle.ydoc.getText('content') as anti-patterns until Phase 2 lands.

UI modernization

Replaced raw HTML and custom components with shared @epicenter/ui primitives and shadcn-svelte-extras:

Before                              After
─────────────────────────────────   ──────────────────────────────────
Raw <input>, <button>               Input, Button from @epicenter/ui
Inline SVG icons                    lucide-svelte icons
Custom TreeNode.svelte (177 lines)  tree-view from shadcn-svelte-extras
No tooltips                         Tooltip wrappers on toolbar actions

The custom TreeNode.svelte component was replaced with the upstream tree-view package, which provides proper ARIA roles, keyboard navigation, and tree-line nesting out of the box. Several follow-up fixes resolved folder icons, chevrons, and a malformed closing tag.

Also fixed a content persistence bug: dirty content now saves on component destroy, not just blur—preventing data loss when switching files via the tree view.

Changelog

  • Fix content editor writing to wrong Y.Doc store, making edits invisible to the filesystem API
  • Fix content loss when switching files without blurring the editor first
  • Replace raw HTML inputs and buttons with shared UI components
  • Replace custom tree view with accessible upstream implementation
  • Add tooltip hints to toolbar actions

braden-w added 15 commits March 13, 2026 15:00
- Replace raw <input> in CreateDialog and RenameDialog with Input component
- Wrap form inputs in Field + FieldLabel for proper form structure
- Replace raw <textarea> in ContentEditor with Textarea component
- Add lucide-svelte dependency for upcoming icon replacements
- Completed spec items 1.1, 1.2, 1.3, 1.4
- Replace ChevronRight, Folder, FolderOpen, File SVGs in TreeNode.svelte
- Import lucide-svelte components (File aliased as FileIcon to avoid collision)
- Toolbar.svelte has no inline SVGs, no changes needed
- Completed spec items 2.1, 2.2, 2.3, 2.4, 2.5
- Wrap all Toolbar buttons in Tooltip.Root with descriptive content
- Add Tooltip.Provider for shared delay settings
- Replace manual empty state in FileTree with Empty component
- Completed spec items 3.1, 3.2
- Update spec status to Implemented
- Check off all success criteria
- Add review section with per-file change summary
- Note deferred item (3.3 Kbd hints) and follow-up work
…extras

Install tree-view component via jsrepo and wrap it in a new FileTreeItem
component that preserves all existing behavior—selection state, context
menus, CRUD dialogs, keyboard handlers, and accessibility roles.

The installed tree-view-folder was customized to forward onOpenChange to
bits-ui's Collapsible.Root for controlled expansion state, and to accept
a style prop for depth-based padding indentation.
The {#key} block in ContentPanel destroys ContentEditor when
activeFileId changes. DOM removal doesn't fire blur events, so
unsaved edits were silently lost. The $effect cleanup function
now persists dirty content before the component is torn down.
…dle)

Two incompatible content stores coexist in document Y.Docs—the
timeline array (packages/filesystem) and raw Y.Text (handle API).
Writes through one are invisible to the other. Spec proposes
making timeline the single source of truth.
A tree view root is always semantically a tree—consumers shouldn't need
to remember to pass role="tree" every time.
The upstream tree-view-folder uses CSS nesting (border-l + mx-2) to
handle indentation automatically through composition—each nested Folder
adds its own margin. This eliminates the need for a depth prop, manual
padding-left, and the style prop customization we added.
Folders showed chevron + folder icon + name (3 elements) while files
showed icon + name (2 elements), causing misalignment. The chevron
already indicates expandability—the separate folder icon is redundant.
Matches Obsidian's approach where the chevron is the only folder indicator.
Remove the custom icon snippet from TreeView.Folder so the upstream
FolderIcon/FolderOpenIcon defaults render instead. This aligns folder
and file rows to the same [icon] + [name] pattern with no chevron.
… to document binding spec

Complete the draft spec with resolved open questions, Phase 2 (makeHandle
timeline unification), Phase 3 (fuji/honeycrisp migration), anti-pattern
reference, and success criteria. Add content model note to the document
binding migration spec marking the dual-path decision as superseded.
…w Y.Text

Switch readContent/writeContent from handle.read()/handle.write() (which
use Y.Text('content')) to fs.content.read()/fs.content.write() (which use
the timeline Y.Array). This ensures content written by fs.writeFile() is
visible to the editor and vice versa.
… READMEs

Update workspace-api skill, yjs skill, DocumentHandle JSDoc, workspace
READMEs, and AGENTS.md files to document that handle.read()/write() and
handle.ydoc.getText('content') are anti-patterns. Recommend fs.content
or createTimeline(ydoc) from @epicenter/filesystem instead.
@braden-w braden-w merged commit 3305e1b into main Mar 14, 2026
1 of 9 checks passed
@braden-w braden-w deleted the opencode/neon-forest branch March 14, 2026 05:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant