Skip to content

feat(honeycrisp): complete UI overhaul — Apple Notes clone with soft delete, command palette, context menus#1526

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

feat(honeycrisp): complete UI overhaul — Apple Notes clone with soft delete, command palette, context menus#1526
braden-w merged 15 commits intomainfrom
opencode/calm-forest

Conversation

@braden-w
Copy link
Member

@braden-w braden-w commented Mar 13, 2026

Redesigns Honeycrisp from a developer scaffold into a faithful Apple Notes clone: soft-delete schema, extracted state module, sidebar with Recently Deleted, context menus with move-to-folder, ⌘K command palette, visual polish, and word count tracking.

The app was structurally correct—three-column layout, folder CRUD, note CRUD, Tiptap + Yjs editor—but everything lived in a 296-line +page.svelte with flat styling. This PR surgically targets every gap between "working prototype" and "convincing Apple Notes clone" across 5 execution waves, each independently verifiable and committed separately.

Before                              After
┌──────────────────────────┐       ┌──────────────────────────┐
│  +page.svelte (296 lines)│       │  +page.svelte (103 lines)│ ← Layout only
│    ALL state              │       │  notes.svelte.ts (301)   │ ← Factory singleton
│    ALL actions            │       │  NoteCard.svelte (207)   │ ← Context menus
│    ALL layout             │       │  CommandPalette.svelte   │ ← ⌘K search
│    ALL derived state      │       │  Sidebar.svelte (191)    │ ← Collapsible, trash
│                           │       │  NoteList.svelte (223)   │ ← Date grouping
└──────────────────────────┘       └──────────────────────────┘

Wave 0: Schema + State Extraction

Notes table bumped to v2 with deletedAt field for soft delete. All reactive state, workspace observers, derived computations, and actions extracted from +page.svelte into createNotesState() factory following the established codebase pattern (saved-tab-state, browser-state).

// Factory encloses $state in a closure — Svelte 5 requires this
// for module-level state that gets reassigned by observers
function createNotesState() {
  let allNotes = $state<Note[]>(readNotes());

  workspaceClient.tables.notes.observe(() => {
    allNotes = readNotes(); // Reassignment works inside closure
  });

  const notes = $derived(allNotes.filter((n) => n.deletedAt === undefined));
  const deletedNotes = $derived(allNotes.filter((n) => n.deletedAt !== undefined));

  return {
    get notes() { return notes; },
    get deletedNotes() { return deletedNotes; },
    softDeleteNote(noteId) { ... },
    restoreNote(noteId) { ... },
    permanentlyDeleteNote(noteId) { ... },
  };
}
export const notesState = createNotesState();

Wave 1: Sidebar Overhaul

"Recently Deleted" smart folder with badge count. Folders section wrapped in Collapsible (Apple Notes style). Folder delete triggers AlertDialog confirmation instead of instant destruction. Duplicate footer button removed.

Wave 2: NoteCard + Context Menus

Extracted NoteCard.svelte with right-click context menus:

Normal mode               Recently Deleted mode
┌──────────────────────┐  ┌──────────────────────┐
│ 📌 Pin / Unpin       │  │ Restore              │
│ ───────────────────── │  │ ───────────────────── │
│ Move to Folder   ►   │  │ Delete Permanently   │
│ ───────────────────── │  └──────────────────────┘
│ 🗑️ Delete            │
└──────────────────────┘

Move to Folder submenu lists all user folders + "Unfiled" option. Permanent delete from Recently Deleted triggers AlertDialog confirmation. Date grouping expanded: Today → Yesterday → Previous 7 Days → Previous 30 Days → month names.

Wave 3: Command Palette

CommandPalette.svelte using shadcn Command.Dialog with ⌘K shortcut. Three groups: Folders (navigate), Notes (search by title/preview), Actions (New Note, New Folder).

Wave 4: Visual Polish + Cleanup

Note card selection upgraded to rounded-lg with softer bg-accent/30 hover. Arrow key navigation in the note list. Warmer empty states with keyboard shortcut hints. Removed redundant border-r on resizable pane that was creating a double-bar artifact alongside the Resizable.Handle.

Prop Drilling Elimination

Components were initially wired through 30+ forwarded props. A follow-up cleanup wave removed all prop drilling—every component now imports the notesState singleton directly, matching the established browser-state/saved-tab-state pattern. +page.svelte went from 296 lines of wiring harness to 103 lines of pure layout + document handle lifecycle.

Why factory function, not module-level exports?

Initial implementation used bare export { folders, notes, ... } with module-level $state. Svelte 5 rejects this: Cannot export state from a module if it is reassigned. The Y.Doc observers reassign state on every change, so the factory pattern (same as saved-tab-state.svelte.ts) is required.

Also in this PR

  • feat(honeycrisp): Word count tracking for notes
  • fix(fuji): Add hover states to table rows, prep timeline for action buttons
  • fix(honeycrisp): Remove redundant border-r on resizable pane (double-bar fix)

Changelog

  • Add soft delete for notes — deleted notes move to "Recently Deleted" instead of permanent destruction
  • Add right-click context menus on notes with Pin, Move to Folder, and Delete actions
  • Add ⌘K command palette for searching notes, navigating folders, and creating notes
  • Add collapsible folder sections and "Recently Deleted" smart folder in sidebar
  • Add AlertDialog confirmation for folder delete and permanent note delete
  • Add arrow key navigation in the note list
  • Add word count tracking to notes
  • Fix double-bar visual artifact between resizable panels
  • Fix missing hover states on Fuji table rows

…elte.ts

- Add deletedAt optional field to notes table, bump to _v: 2 with migration
- Extract all state management from +page.svelte (296 lines) into
  lib/state/notes.svelte.ts (264 lines) with module-level $state
- +page.svelte is now layout-only (131 lines): document handle + markup
- Add softDeleteNote, restoreNote, permanentlyDeleteNote actions
- Filter deleted notes from normal views, add deletedNotes derived state
- Fix defineKv calls to include required default values (API change)
- Wire onDeleteNote to softDeleteNote instead of permanent delete
…lders, AlertDialog

- Add Recently Deleted smart folder below All Notes with deleted count badge
- Wrap Folders section in Collapsible (open by default, Apple Notes style)
- Replace instant folder delete with AlertDialog confirmation dialog
- Remove duplicate New Folder button from Sidebar.Footer
- NoteList supports viewMode 'recentlyDeleted' with Restore/Delete Forever actions
- +page.svelte manages isRecentlyDeletedView state and derived folderName
- Header shows current folder name + note count
…ed date grouping

- Extract NoteCard.svelte with ContextMenu (Pin, Move to Folder submenu, Delete)
- Move to Folder submenu lists all user folders + Unfiled option
- Recently Deleted context menu: Restore, Delete Permanently with AlertDialog
- Expand date grouping: Today, Yesterday, Previous 7 Days, Previous 30 Days, month names
- NoteList simplified to delegate card rendering to NoteCard component
- Wire moveNoteToFolder action through +page.svelte to workspace client
…igation

- Create CommandPalette.svelte using shadcn Command.Dialog component
- Search notes by title and preview text with built-in fuzzy matching
- Quick folder navigation: All Notes + user folders in Folders group
- New Note and New Folder actions in Actions group
- Wire \u2318K keyboard shortcut to toggle palette open/close
- Selecting from palette clears Recently Deleted view
…board nav, empty states

- Upgrade NoteCard selection from rounded-md to rounded-lg
- Soften hover state from bg-accent/50 to bg-accent/30
- Add arrow key navigation in NoteList (up/down cycles through notes)
- Improve editor empty state with warmer messaging and ⌘N hint
- Verify editor title CSS (1.75rem bold) and toolbar spacing unchanged
…valid_export)

Svelte 5 doesn't allow exporting reassigned $state from modules.
Refactored from bare module-level exports to createNotesState() factory
following the established codebase pattern (saved-tab-state, browser-state).

- All $state/$derived enclosed in factory closure
- Public API via getters (state) and method shorthand (actions)
- Exported as singleton: export const notesState = createNotesState()
- +page.svelte updated to use notesState.xxx references
- Moved moveNoteToFolder into the state module (was inline in +page)
@rupokghosh
Copy link
Member

need this!

…ame method, extract parseDateTime

- 1.1: Add isRecentlyDeletedView + selectRecentlyDeleted() to notesState
- 1.2: Add folderName derived getter to notesState
- 1.8: Fix generateId() as unknown as → as string as (2 occurrences)
- 2.2: Rename handleContentChange → updateNoteContent
- 2.1: Extract parseDateTime to $lib/utils/date.ts
…mmandPalette

- 1.3: Sidebar imports notesState directly (zero props)
- 1.5: NoteCard props reduced to just `note`, computes isSelected internally
- 1.6: CommandPalette props reduced to just `open` bindable
- 2.3: Removed unnecessary `as` casts (props eliminated entirely)
- 2.4: Naming inconsistency resolved (direct notesState.setSortBy calls)
…svelte to layout shell

- 1.4: NoteList imports notesState directly (zero props, derives note list internally)
- 1.7: +page.svelte slimmed from 164 → 103 lines (layout + doc handle + shortcuts only)
…thods

- 3.1: JSDoc on all 14 existing methods with @example blocks
- 3.2: JSDoc on selectRecentlyDeleted (new from 1.1)
- Mark spec as Implemented with review section
…ntation state

- Umbrella: check off Phase 2 (Honeycrisp complete), mark descoped items
  (checklist detection, word count, focus mode), note template system obsolete
- Umbrella: check off Phase 4 items (command palette, empty states, shortcuts),
  mark sync extension as deferred, update success criteria
- Overhaul: check off Phase 5 verification items (typecheck, all CRUD flows),
  update success criteria to reflect PR cleanup results
- Honeycrisp exec: mark mobile Sheet drawer as unverified, template as obsolete
…uttons

- 1.11: Add hover:bg-accent/50 + transition-colors to EntriesTable rows
  (matches existing EntryTimeline hover behavior)
- 1.9: Add `group` class to timeline entry container for future
  group-hover action buttons
- 1.10: Mark as decided — Sidebar is correct for Fuji's layout
- Add optional wordCount field to v2 notes schema
- Compute word count in Editor on each content change
- Store via updateNoteContent alongside title/preview
- New notes initialize with wordCount: 0
- Legacy notes remain undefined until next edit
The Resizable.Handle already renders as a 1px bg-border element—adding
border-r to the pane created a double-bar visual artifact between the
notes list and editor panels.
@braden-w braden-w merged commit e10bc69 into main Mar 14, 2026
1 of 8 checks passed
@braden-w braden-w deleted the opencode/calm-forest branch March 14, 2026 06:07
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.

2 participants