From 1ca80269c3c8b9f972c8d7df5b784e638a61f3a0 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 26 Aug 2026 11:32:54 +1000 Subject: [PATCH 01/14] feat(staged): add "Write note" action and footer overflow menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch card footers get a `…` dropdown at the end of the left-aligned session buttons. It always holds a new "Write note" action, and absorbs the session buttons as the card narrows. Unlike "New note" (which launches an agent that writes a note), "Write note" opens a WYSIWYG markdown editor the user types into directly — both when creating the note and when clicking it in the timeline. Notes gain a nullable `subtype` column: NULL for session-produced notes, 'written' for user-authored ones. Session-less notes (drag-dropped files, saved action output) are backfilled to 'written', since they are exactly that class and become editable too. `update_note` refuses anything else, so an agent's note can't be overwritten out from under its session. Written notes have no separate title field — the leading H1 is the title, matching how `resolve_note_title_and_body` stores session notes. The overflow tier can't be a container query like the existing label tiers: the menu's content is portaled out of the `timeline` container, so button and menu both read a ResizeObserver width through the pure `computeFooterOverflow` in `footerOverflow.ts`. Review drops out first, then Commit, leaving Note last. The editor is Milkdown's Crepe, wrapped in `MarkdownWysiwygEditor.svelte` behind a value-in/`getMarkdown()`-out contract so it stays swappable, and lazily imported so ProseMirror and CodeMirror stay out of the main bundle. Its theme is remapped onto our `var(--*)` tokens. Thresholds are the plan's starting values and still want a visual pass on a real card; the nested DropdownMenu inside `TimelineContextMenu` also wants a manual right-click check. Signed-off-by: Matt Toohey --- apps/staged/package.json | 1 + apps/staged/src-tauri/src/lib.rs | 4 + apps/staged/src-tauri/src/note_commands.rs | 57 +- .../src-tauri/src/store/migration_tests.rs | 83 +- .../migrations/0027-add-note-subtype/up.sql | 8 + apps/staged/src-tauri/src/store/models.rs | 18 + apps/staged/src-tauri/src/store/notes.rs | 84 +- apps/staged/src-tauri/src/store/tests.rs | 94 ++ apps/staged/src-tauri/src/timeline.rs | 1 + apps/staged/src-tauri/src/web_server.rs | 27 +- apps/staged/src/lib/commands.ts | 27 +- .../lib/features/branches/BranchCard.svelte | 37 + .../notes/MarkdownWysiwygEditor.svelte | 193 +++ .../lib/features/notes/WriteNoteModal.svelte | 207 +++ .../lib/features/notes/noteMarkdown.test.ts | 47 +- .../src/lib/features/notes/noteMarkdown.ts | 41 + .../features/sessions/hashtagItems.test.ts | 2 + .../lib/features/sessions/noteFreshness.ts | 4 +- .../features/timeline/BranchTimeline.svelte | 108 +- .../features/timeline/footerOverflow.test.ts | 65 + .../lib/features/timeline/footerOverflow.ts | 77 + apps/staged/src/lib/types.ts | 12 + pnpm-lock.yaml | 1311 ++++++++++++++++- 23 files changed, 2447 insertions(+), 61 deletions(-) create mode 100644 apps/staged/src-tauri/src/store/migrations/0027-add-note-subtype/up.sql create mode 100644 apps/staged/src/lib/features/notes/MarkdownWysiwygEditor.svelte create mode 100644 apps/staged/src/lib/features/notes/WriteNoteModal.svelte create mode 100644 apps/staged/src/lib/features/timeline/footerOverflow.test.ts create mode 100644 apps/staged/src/lib/features/timeline/footerOverflow.ts diff --git a/apps/staged/package.json b/apps/staged/package.json index 4a5811845..af47486a9 100644 --- a/apps/staged/package.json +++ b/apps/staged/package.json @@ -47,6 +47,7 @@ }, "dependencies": { "@builderbot/diff-viewer": "workspace:*", + "@milkdown/crepe": "^7.22.1", "@tauri-apps/api": "^2.11.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", "@tauri-apps/plugin-dialog": "^2.7.1", diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index 44968f501..3a48186cf 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -181,6 +181,9 @@ pub struct NoteTimelineItem { pub completed_at: Option, pub suggested_next_commit_step: Option, pub suggested_next_note_step: Option, + /// `None` for session-produced notes, `"written"` for user-authored ones — + /// the frontend routes the latter to the editor instead of the viewer. + pub subtype: Option, } /// Review with session status resolved. @@ -2315,6 +2318,7 @@ pub fn run() { timeline::reset_branch_to_remote, // Notes note_commands::create_note, + note_commands::update_note, note_commands::delete_note, note_commands::get_note, note_commands::list_child_notes, diff --git a/apps/staged/src-tauri/src/note_commands.rs b/apps/staged/src-tauri/src/note_commands.rs index 90c750955..6731bced0 100644 --- a/apps/staged/src-tauri/src/note_commands.rs +++ b/apps/staged/src-tauri/src/note_commands.rs @@ -24,35 +24,66 @@ pub(crate) fn note_to_timeline_item(store: &Store, note: Note) -> NoteTimelineIt completed_at: note.completed_at, suggested_next_commit_step: note.suggested_next_commit_step, suggested_next_note_step: note.suggested_next_note_step, + subtype: note.subtype, + } +} + +/// Build the timeline item for a note that has no session by construction, so +/// there is no session status to resolve. Shared with the web-server dispatch. +pub(crate) fn standalone_note_to_timeline_item(note: Note) -> NoteTimelineItem { + NoteTimelineItem { + id: note.id, + title: note.title, + content: note.content, + session_id: None, + session_status: None, + completion_reason: None, + created_at: note.created_at, + updated_at: note.updated_at, + completed_at: note.completed_at, + suggested_next_commit_step: None, + suggested_next_note_step: None, + subtype: note.subtype, } } /// Create a standalone note (no session) for a branch. +/// +/// `subtype` is `"written"` when the user authored the note in the editor +/// dialog; the drag-drop and save-action-output paths leave it unset. #[tauri::command(rename_all = "camelCase")] pub fn create_note( store: tauri::State<'_, Mutex>>>, branch_id: String, title: String, content: String, + subtype: Option, ) -> Result { let store = crate::get_store(&store)?; let mut note = crate::store::models::Note::new(&branch_id, &title, &content); + note.subtype = subtype; store .create_note_with_unique_title(&mut note) .map_err(|e| e.to_string())?; - Ok(NoteTimelineItem { - id: note.id, - title: note.title, - content: note.content, - session_id: None, - session_status: None, - completion_reason: None, - created_at: note.created_at, - updated_at: note.updated_at, - completed_at: note.completed_at, - suggested_next_commit_step: None, - suggested_next_note_step: None, - }) + Ok(standalone_note_to_timeline_item(note)) +} + +/// Save an edit to a user-authored ("written") note. +/// +/// Rejects notes an agent session produced — their content is owned by that +/// session and would be overwritten on its next turn. +#[tauri::command(rename_all = "camelCase")] +pub fn update_note( + store: tauri::State<'_, Mutex>>>, + note_id: String, + title: String, + content: String, +) -> Result { + let store = crate::get_store(&store)?; + let note = store + .update_written_note(¬e_id, &title, &content) + .map_err(|e| e.to_string())?; + Ok(standalone_note_to_timeline_item(note)) } /// Delete a note and optionally its linked session. diff --git a/apps/staged/src-tauri/src/store/migration_tests.rs b/apps/staged/src-tauri/src/store/migration_tests.rs index 57a61730c..5378229b4 100644 --- a/apps/staged/src-tauri/src/store/migration_tests.rs +++ b/apps/staged/src-tauri/src/store/migration_tests.rs @@ -145,7 +145,7 @@ fn test_store_bootstraps_fresh_database_with_baseline_migration() { ) .unwrap(); - assert_eq!(version, 26); + assert_eq!(version, 27); assert_eq!(app_version, super::APP_VERSION); assert!(table_exists(&conn, "projects")); assert!(table_exists(&conn, "project_notes")); @@ -207,7 +207,7 @@ fn test_store_repairs_github_comment_tracking_user_version() { created_at INTEGER NOT NULL, image_ids TEXT DEFAULT NULL ); - CREATE TABLE notes (id TEXT PRIMARY KEY); + CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT); -- Only the table/column the 0026 auto-review cleanup targets. CREATE TABLE reviews ( id TEXT PRIMARY KEY, @@ -245,7 +245,7 @@ fn test_store_repairs_github_comment_tracking_user_version() { let version: i64 = conn .query_row("PRAGMA user_version", [], |row| row.get(0)) .unwrap(); - assert_eq!(version, 26); + assert_eq!(version, 27); assert!(column_exists(&conn, "sessions", "pipeline")); assert!(column_exists(&conn, "sessions", "acp_config_selection")); assert!(column_exists(&conn, "sessions", "acp_title")); @@ -287,7 +287,7 @@ fn test_store_repairs_pipeline_user_version() { created_at INTEGER NOT NULL, image_ids TEXT DEFAULT NULL ); - CREATE TABLE notes (id TEXT PRIMARY KEY); + CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT); -- Only the table/column the 0026 auto-review cleanup targets. CREATE TABLE reviews ( id TEXT PRIMARY KEY, @@ -320,7 +320,7 @@ fn test_store_repairs_pipeline_user_version() { let version: i64 = conn .query_row("PRAGMA user_version", [], |row| row.get(0)) .unwrap(); - assert_eq!(version, 26); + assert_eq!(version, 27); assert!(column_exists(&conn, "comments", "github_comment_id")); assert!(column_exists(&conn, "comments", "github_comment_type")); assert!(column_exists(&conn, "comments", "github_comment_stale")); @@ -366,8 +366,8 @@ fn test_completion_effects_migration_backfills_finished_pipeline_sessions() { id TEXT PRIMARY KEY, detecting_actions INTEGER NOT NULL DEFAULT 0 ); - -- Only the table the 0025 column add targets. - CREATE TABLE notes (id TEXT PRIMARY KEY); + -- Only the table the 0025/0027 note column adds target. + CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT); -- Only the table/column the 0026 auto-review cleanup targets. CREATE TABLE reviews ( id TEXT PRIMARY KEY, @@ -385,7 +385,7 @@ fn test_completion_effects_migration_backfills_finished_pipeline_sessions() { let version: i64 = conn .query_row("PRAGMA user_version", [], |row| row.get(0)) .unwrap(); - assert_eq!(version, 26); + assert_eq!(version, 27); assert!(column_exists(&conn, "sessions", "completion_effects_at")); let marker = |id: &str| -> Option { @@ -427,6 +427,8 @@ fn test_auto_review_removal_migration_deletes_auto_reviews_and_drops_flag() { INSERT INTO reviews (id, is_auto) VALUES ('user-review', 0), ('auto-review', 1); + -- Only the table the 0027 note column add targets. + CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT); ", ) .unwrap(); @@ -439,7 +441,7 @@ fn test_auto_review_removal_migration_deletes_auto_reviews_and_drops_flag() { let version: i64 = conn .query_row("PRAGMA user_version", [], |row| row.get(0)) .unwrap(); - assert_eq!(version, 26); + assert_eq!(version, 27); assert!(!column_exists(&conn, "reviews", "is_auto")); // Reviews the removed auto-review feature created in the background are @@ -475,8 +477,8 @@ fn test_detecting_pid_migration_clears_orphaned_detection_flags() { INSERT INTO action_contexts (id, detecting_actions) VALUES ('wedged', 1), ('idle', 0); - -- Only the table the 0025 column add targets. - CREATE TABLE notes (id TEXT PRIMARY KEY); + -- Only the table the 0025/0027 note column adds target. + CREATE TABLE notes (id TEXT PRIMARY KEY, session_id TEXT); -- Only the table/column the 0026 auto-review cleanup targets. CREATE TABLE reviews ( id TEXT PRIMARY KEY, @@ -494,7 +496,7 @@ fn test_detecting_pid_migration_clears_orphaned_detection_flags() { let version: i64 = conn .query_row("PRAGMA user_version", [], |row| row.get(0)) .unwrap(); - assert_eq!(version, 26); + assert_eq!(version, 27); assert!(column_exists(&conn, "action_contexts", "detecting_pid")); // No shipped build ever cleared the flag from outside the process that set @@ -511,3 +513,60 @@ fn test_detecting_pid_migration_clears_orphaned_detection_flags() { cleanup_db(&path); } + +#[test] +fn test_note_subtype_migration_backfills_session_less_notes() { + let path = temp_db_path("note-subtype-backfill"); + let conn = Connection::open(&path).unwrap(); + conn.execute_batch( + " + PRAGMA user_version = 25; + CREATE TABLE app_metadata ( + id INTEGER PRIMARY KEY CHECK (id = 1), + app_version TEXT NOT NULL + ); + INSERT INTO app_metadata (id, app_version) VALUES (1, '0.2.9'); + CREATE TABLE notes ( + id TEXT PRIMARY KEY, + session_id TEXT + ); + INSERT INTO notes (id, session_id) VALUES + ('dropped', NULL), + ('agent', 'session-1'); + -- Only the table/column the 0026 auto-review cleanup targets. + CREATE TABLE reviews ( + id TEXT PRIMARY KEY, + is_auto INTEGER NOT NULL DEFAULT 0 + ); + ", + ) + .unwrap(); + drop(conn); + + let store = Store::new(&path).unwrap(); + drop(store); + + let conn = Connection::open(&path).unwrap(); + let version: i64 = conn + .query_row("PRAGMA user_version", [], |row| row.get(0)) + .unwrap(); + assert_eq!(version, 27); + assert!(column_exists(&conn, "notes", "subtype")); + + let subtype = |id: &str| -> Option { + conn.query_row( + "SELECT subtype FROM notes WHERE id = ?1", + params![id], + |row| row.get(0), + ) + .unwrap() + }; + + // Drag-dropped files and saved action output are already user-authored + // notes with no owning session, so they become editable alongside newly + // written ones. Notes an agent produced stay untagged. + assert_eq!(subtype("dropped").as_deref(), Some("written")); + assert_eq!(subtype("agent"), None); + + cleanup_db(&path); +} diff --git a/apps/staged/src-tauri/src/store/migrations/0027-add-note-subtype/up.sql b/apps/staged/src-tauri/src/store/migrations/0027-add-note-subtype/up.sql new file mode 100644 index 000000000..156189d9b --- /dev/null +++ b/apps/staged/src-tauri/src/store/migrations/0027-add-note-subtype/up.sql @@ -0,0 +1,8 @@ +-- Distinguishes user-authored notes (written directly in the editor dialog) +-- from agent/session notes. NULL = produced by a session; 'written' = authored +-- by the user and therefore editable in place. +ALTER TABLE notes ADD COLUMN subtype TEXT; + +-- Existing session-less notes (drag-dropped files, saved action output) are +-- exactly the user-authored class, so they become editable too. +UPDATE notes SET subtype = 'written' WHERE session_id IS NULL; diff --git a/apps/staged/src-tauri/src/store/models.rs b/apps/staged/src-tauri/src/store/models.rs index 2500371cf..6b49a7d34 100644 --- a/apps/staged/src-tauri/src/store/models.rs +++ b/apps/staged/src-tauri/src/store/models.rs @@ -891,9 +891,17 @@ pub struct Note { /// rather than a standalone branch note. Children are hidden from the /// branch timeline and fetched via the parent project-note view. pub parent_project_note_id: Option, + /// How the note's content came to be. `None` means a session produced it; + /// [`Note::SUBTYPE_WRITTEN`] means the user authored it directly and it can + /// be edited in place. + pub subtype: Option, } impl Note { + /// Subtype marking a note the user wrote themselves rather than one an + /// agent session produced. Only these are editable via `update_note`. + pub const SUBTYPE_WRITTEN: &'static str = "written"; + pub fn new(branch_id: &str, title: &str, content: &str) -> Self { let now = now_timestamp(); let has_content = !content.is_empty(); @@ -909,6 +917,7 @@ impl Note { suggested_next_commit_step: None, suggested_next_note_step: None, parent_project_note_id: None, + subtype: None, } } @@ -921,6 +930,15 @@ impl Note { self.parent_project_note_id = Some(id.to_string()); self } + + pub fn with_subtype(mut self, subtype: &str) -> Self { + self.subtype = Some(subtype.to_string()); + self + } + + pub fn is_written(&self) -> bool { + self.subtype.as_deref() == Some(Self::SUBTYPE_WRITTEN) + } } // ============================================================================= diff --git a/apps/staged/src-tauri/src/store/notes.rs b/apps/staged/src-tauri/src/store/notes.rs index 61421029b..6a15d0dae 100644 --- a/apps/staged/src-tauri/src/store/notes.rs +++ b/apps/staged/src-tauri/src/store/notes.rs @@ -27,7 +27,8 @@ impl Store { pub fn create_note_with_unique_title(&self, note: &mut Note) -> Result<(), StoreError> { let conn = self.conn.lock().unwrap(); if !note.title.is_empty() { - note.title = Self::resolve_unique_note_title(&conn, ¬e.branch_id, ¬e.title)?; + note.title = + Self::resolve_unique_note_title(&conn, ¬e.branch_id, ¬e.title, None)?; } Self::insert_note(&conn, note)?; self.publish(StoreChange::Notes { @@ -39,8 +40,8 @@ impl Store { fn insert_note(conn: &rusqlite::Connection, note: &Note) -> Result<(), StoreError> { conn.execute( - "INSERT INTO notes (id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + "INSERT INTO notes (id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)", params![ note.id, note.branch_id, @@ -53,19 +54,25 @@ impl Store { note.suggested_next_commit_step, note.suggested_next_note_step, note.parent_project_note_id, + note.subtype, ], )?; Ok(()) } + /// Pick a title that is unique among the branch's notes. `exclude_id` is the + /// note being renamed, so re-saving a written note under its own title keeps + /// that title instead of collecting a ` (2)` suffix on every save. fn resolve_unique_note_title( conn: &rusqlite::Connection, branch_id: &str, base: &str, + exclude_id: Option<&str>, ) -> Result { - let mut stmt = conn.prepare("SELECT title FROM notes WHERE branch_id = ?1")?; + let mut stmt = + conn.prepare("SELECT title FROM notes WHERE branch_id = ?1 AND id IS NOT ?2")?; let titles: Vec = stmt - .query_map(params![branch_id], |row| row.get(0))? + .query_map(params![branch_id, exclude_id], |row| row.get(0))? .collect::>()?; if !titles.iter().any(|t| t == base) { @@ -90,7 +97,7 @@ impl Store { pub fn get_note(&self, id: &str) -> Result, StoreError> { let conn = self.conn.lock().unwrap(); conn.query_row( - "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype FROM notes WHERE id = ?1", params![id], Self::row_to_note, @@ -106,7 +113,7 @@ impl Store { pub fn list_notes_for_branch(&self, branch_id: &str) -> Result, StoreError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( - "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype FROM notes WHERE branch_id = ?1 AND parent_project_note_id IS NULL ORDER BY COALESCE(completed_at, created_at) DESC, created_at DESC", )?; @@ -120,7 +127,7 @@ impl Store { pub fn list_all_notes_for_branch(&self, branch_id: &str) -> Result, StoreError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( - "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype FROM notes WHERE branch_id = ?1 ORDER BY COALESCE(completed_at, created_at) DESC, created_at DESC", )?; @@ -135,7 +142,7 @@ impl Store { pub fn list_child_notes(&self, parent_project_note_id: &str) -> Result, StoreError> { let conn = self.conn.lock().unwrap(); let mut stmt = conn.prepare( - "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype FROM notes WHERE parent_project_note_id = ?1 ORDER BY COALESCE(completed_at, created_at) DESC, created_at DESC", )?; @@ -147,7 +154,7 @@ impl Store { pub fn get_note_by_session(&self, session_id: &str) -> Result, StoreError> { let conn = self.conn.lock().unwrap(); conn.query_row( - "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype FROM notes WHERE session_id = ?1", params![session_id], Self::row_to_note, @@ -160,7 +167,7 @@ impl Store { pub fn get_empty_note_by_session(&self, session_id: &str) -> Result, StoreError> { let conn = self.conn.lock().unwrap(); conn.query_row( - "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype FROM notes WHERE session_id = ?1 AND content = ''", params![session_id], Self::row_to_note, @@ -226,6 +233,60 @@ impl Store { Ok(()) } + /// Rewrite a user-authored note's title and content, returning the saved row. + /// + /// Unlike [`Store::update_note_title_and_content`] (the session runner's path) + /// this always advances `updated_at` — a user save is an explicit edit, not a + /// re-extraction — and leaves the suggested next steps alone, since nothing + /// regenerates them for a written note. Notes an agent produced are rejected: + /// their content belongs to the session that wrote it. + pub fn update_written_note( + &self, + id: &str, + title: &str, + content: &str, + ) -> Result { + let conn = self.conn.lock().unwrap(); + let existing = conn + .query_row( + "SELECT id, branch_id, session_id, title, content, created_at, updated_at, completed_at, suggested_next_commit_step, suggested_next_note_step, parent_project_note_id, subtype + FROM notes WHERE id = ?1", + params![id], + Self::row_to_note, + ) + .optional()? + .ok_or_else(|| StoreError(format!("Note not found: {id}")))?; + + if !existing.is_written() { + return Err(StoreError(format!( + "Note {id} was produced by a session and cannot be edited" + ))); + } + + let title = if title.is_empty() { + title.to_string() + } else { + Self::resolve_unique_note_title(&conn, &existing.branch_id, title, Some(id))? + }; + let now = now_timestamp(); + let completed_at = + existing + .completed_at + .or(if content.is_empty() { None } else { Some(now) }); + conn.execute( + "UPDATE notes SET title = ?1, content = ?2, updated_at = ?3, completed_at = ?4 WHERE id = ?5", + params![title, content, now, completed_at, id], + )?; + + Ok(Note { + title, + content: content.to_string(), + updated_at: now, + completed_at, + ..existing + }) + } + pub fn mark_note_completed(&self, id: &str) -> Result<(), StoreError> { let conn = self.conn.lock().unwrap(); let now = now_timestamp(); @@ -267,6 +328,7 @@ impl Store { suggested_next_commit_step: row.get(8)?, suggested_next_note_step: row.get(9)?, parent_project_note_id: row.get(10)?, + subtype: row.get(11)?, }) } } diff --git a/apps/staged/src-tauri/src/store/tests.rs b/apps/staged/src-tauri/src/store/tests.rs index c3cb59b77..d0c687076 100644 --- a/apps/staged/src-tauri/src/store/tests.rs +++ b/apps/staged/src-tauri/src/store/tests.rs @@ -2337,6 +2337,100 @@ fn test_create_note_with_unique_title_skips_empty_titles() { assert_eq!(second.title, ""); } +#[test] +fn test_written_note_round_trips_its_subtype() { + let store = Store::in_memory().unwrap(); + let project = Project::new("test-owner/test-repo"); + store.create_project(&project).unwrap(); + let branch = Branch::new(&project.id, "feature", "main"); + store.create_branch(&branch).unwrap(); + + let mut written = Note::new(&branch.id, "Design sketch", "# Design sketch") + .with_subtype(Note::SUBTYPE_WRITTEN); + store.create_note_with_unique_title(&mut written).unwrap(); + let agent = Note::new(&branch.id, "Agent note", "body").with_session("session-1"); + store.create_note(&agent).unwrap(); + + assert!(store.get_note(&written.id).unwrap().unwrap().is_written()); + assert!(!store.get_note(&agent.id).unwrap().unwrap().is_written()); +} + +#[test] +fn test_update_written_note_rewrites_title_and_content() { + let store = Store::in_memory().unwrap(); + let project = Project::new("test-owner/test-repo"); + store.create_project(&project).unwrap(); + let branch = Branch::new(&project.id, "feature", "main"); + store.create_branch(&branch).unwrap(); + + let mut note = Note::new(&branch.id, "Draft", "# Draft").with_subtype(Note::SUBTYPE_WRITTEN); + store.create_note_with_unique_title(&mut note).unwrap(); + let created_completed_at = note.completed_at; + + let updated = store + .update_written_note(¬e.id, "Final", "# Final\n\nbody") + .unwrap(); + assert_eq!(updated.title, "Final"); + assert_eq!(updated.content, "# Final\n\nbody"); + // Completion is write-once: the note has been readable since it was saved. + assert_eq!(updated.completed_at, created_completed_at); + + let stored = store.get_note(¬e.id).unwrap().unwrap(); + assert_eq!(stored.title, "Final"); + assert_eq!(stored.content, "# Final\n\nbody"); + assert!(stored.is_written()); +} + +#[test] +fn test_update_written_note_keeps_its_own_title_but_avoids_others() { + let store = Store::in_memory().unwrap(); + let project = Project::new("test-owner/test-repo"); + store.create_project(&project).unwrap(); + let branch = Branch::new(&project.id, "feature", "main"); + store.create_branch(&branch).unwrap(); + + let mut note = Note::new(&branch.id, "Notes", "body").with_subtype(Note::SUBTYPE_WRITTEN); + store.create_note_with_unique_title(&mut note).unwrap(); + let other = Note::new(&branch.id, "Other", "body"); + store.create_note(&other).unwrap(); + + // Re-saving under the same title must not accumulate " (2)" suffixes. + let resaved = store + .update_written_note(¬e.id, "Notes", "body v2") + .unwrap(); + assert_eq!(resaved.title, "Notes"); + + // Renaming onto another note's title still disambiguates. + let renamed = store + .update_written_note(¬e.id, "Other", "body v3") + .unwrap(); + assert_eq!(renamed.title, "Other (2)"); +} + +#[test] +fn test_update_written_note_rejects_session_produced_notes() { + let store = Store::in_memory().unwrap(); + let project = Project::new("test-owner/test-repo"); + store.create_project(&project).unwrap(); + let branch = Branch::new(&project.id, "feature", "main"); + store.create_branch(&branch).unwrap(); + + // An agent owns this note's content — its session would overwrite any edit + // on the next turn, so the store refuses rather than silently losing it. + let agent = Note::new(&branch.id, "Agent note", "body").with_session("session-1"); + store.create_note(&agent).unwrap(); + + assert!(store + .update_written_note(&agent.id, "Hijacked", "mine now") + .is_err()); + let unchanged = store.get_note(&agent.id).unwrap().unwrap(); + assert_eq!(unchanged.content, "body"); + + assert!(store + .update_written_note("missing", "Title", "body") + .is_err()); +} + #[test] fn test_list_child_notes_returns_children_and_excludes_them_from_branch_timeline() { let store = Store::in_memory().unwrap(); diff --git a/apps/staged/src-tauri/src/timeline.rs b/apps/staged/src-tauri/src/timeline.rs index 67266d4c9..809e75cac 100644 --- a/apps/staged/src-tauri/src/timeline.rs +++ b/apps/staged/src-tauri/src/timeline.rs @@ -529,6 +529,7 @@ fn build_branch_timeline(store: &Arc, branch_id: &str) -> Result Result = opt_arg(&args, "subtype")?; let mut note = crate::store::models::Note::new(&branch_id, &title, &content); + note.subtype = subtype; store .create_note_with_unique_title(&mut note) .map_err(|e| e.to_string())?; - let item = crate::NoteTimelineItem { - id: note.id, - title: note.title, - content: note.content, - session_id: None, - session_status: None, - completion_reason: None, - created_at: note.created_at, - updated_at: note.updated_at, - completed_at: note.completed_at, - suggested_next_commit_step: None, - suggested_next_note_step: None, - }; + let item = crate::note_commands::standalone_note_to_timeline_item(note); + Ok(serde_json::to_value(item).unwrap()) + } + "update_note" => { + let store = get_store(store_mutex)?; + let note_id: String = arg(&args, "noteId")?; + let title: String = arg(&args, "title")?; + let content: String = arg(&args, "content")?; + let note = store + .update_written_note(¬e_id, &title, &content) + .map_err(|e| e.to_string())?; + let item = crate::note_commands::standalone_note_to_timeline_item(note); Ok(serde_json::to_value(item).unwrap()) } "delete_note" => { diff --git a/apps/staged/src/lib/commands.ts b/apps/staged/src/lib/commands.ts index 2dedb910a..5592f365f 100644 --- a/apps/staged/src/lib/commands.ts +++ b/apps/staged/src/lib/commands.ts @@ -1100,13 +1100,34 @@ export function drainQueuedSessions(branchId: string): Promise { // Timeline item deletion // ============================================================================= -/** Create a standalone note (no session) for a branch. */ +/** + * Create a standalone note (no session) for a branch. + * + * `subtype` is `'written'` when the user authored the note in the editor + * dialog; the drag-drop and save-action-output paths leave it unset. + */ export function createNote( branchId: string, title: string, + content: string, + subtype: import('./types').NoteSubtype = null +): Promise { + return invokeCommand('create_note', { branchId, title, content, subtype }); +} + +/** + * Save an edit to a user-authored ("written") note. Rejects notes an agent + * session produced — their content belongs to that session. + * + * Callers invalidate the branch timeline themselves (see `ActionOutputModal`); + * the note id alone doesn't identify which timeline to refresh. + */ +export function updateNote( + noteId: string, + title: string, content: string -): Promise<{ id: string; title: string; content: string; createdAt: number; updatedAt: number }> { - return invokeCommand('create_note', { branchId, title, content }); +): Promise { + return invokeCommand('update_note', { noteId, title, content }); } /** Delete a note and optionally its linked session. */ diff --git a/apps/staged/src/lib/features/branches/BranchCard.svelte b/apps/staged/src/lib/features/branches/BranchCard.svelte index 3eec5a165..008aed19c 100644 --- a/apps/staged/src/lib/features/branches/BranchCard.svelte +++ b/apps/staged/src/lib/features/branches/BranchCard.svelte @@ -51,6 +51,7 @@ ProjectRepo, WorkspaceStatus, } from '../../types'; + import { WRITTEN_NOTE_SUBTYPE } from '../../types'; import * as commands from '../../api/commands'; import BranchTimeline from '../timeline/BranchTimeline.svelte'; import ImageViewerModal from '../timeline/ImageViewerModal.svelte'; @@ -58,6 +59,7 @@ import SessionModal from '../sessions/SessionModal.svelte'; import NewSessionModal from '../sessions/NewSessionModal.svelte'; import NoteModal from '../notes/NoteModal.svelte'; + import WriteNoteModal from '../notes/WriteNoteModal.svelte'; import * as AlertDialog from '$lib/components/ui/alert-dialog'; import { Button } from '$lib/components/ui/button'; import { @@ -529,6 +531,12 @@ // Note modal (opened by clicking a note in the timeline) let openNote = $state(null); + // Write-note editor. `null` while closed; `{ note: null }` for a new note, + // `{ note }` when editing an existing user-written one. + let openWriteNote = $state<{ + note: { id: string; title: string; content: string } | null; + } | null>(null); + // Image viewer modal (opened by clicking an image in the timeline) let viewImageId = $state(null); let viewImageFilename = $state(''); @@ -998,6 +1006,14 @@ } function handleNoteClick(note: NoteClickInfo) { + // The user wrote this one, so clicking it reopens the editor rather than + // the read-only viewer. + if (note.subtype === WRITTEN_NOTE_SUBTYPE) { + openWriteNote = { + note: { id: note.noteId, title: note.title, content: note.content }, + }; + return; + } openNote = { noteId: note.noteId, title: note.title, @@ -1008,6 +1024,17 @@ }; } + async function handleSaveWrittenNote(draft: { title: string; content: string }) { + const existing = openWriteNote?.note; + if (existing) { + await commands.updateNote(existing.id, draft.title, draft.content); + } else { + await commands.createNote(branch.id, draft.title, draft.content, WRITTEN_NOTE_SUBTYPE); + } + commands.invalidateBranchTimeline(branch.id); + await loadTimeline(); + } + async function handleReviewClick(reviewId: string) { const cached = timelineReviewDetailsById[reviewId]; if (cached) { @@ -1994,6 +2021,7 @@ onNewReview={hasCodeChanges || sessionMgr.hasCommitSessionInProgress ? (e) => sessionMgr.openNewSession('review', e) : undefined} + onWriteNote={() => (openWriteNote = { note: null })} onPullOrigin={handlePullOrigin} onPushOrigin={handlePushOrigin} onOpenPushSession={pushSessionId && pushSessionId !== '__pending__' @@ -2108,6 +2136,15 @@ /> {/if} +{#if openWriteNote} + (openWriteNote = null)} + /> +{/if} + {#if viewImageId} + + +
+
+ {#if loadError} + + {/if} +
+ + diff --git a/apps/staged/src/lib/features/notes/WriteNoteModal.svelte b/apps/staged/src/lib/features/notes/WriteNoteModal.svelte new file mode 100644 index 000000000..8b641789c --- /dev/null +++ b/apps/staged/src/lib/features/notes/WriteNoteModal.svelte @@ -0,0 +1,207 @@ + + + + { + if (!next) requestClose(); + }} +> + e.preventDefault()} + > + +
+ + + {isEdit ? 'Edit note' : 'Write note'} + +
+ +
+
+
+ + +
+ {#key editorKey} + (markdown = next)} + /> + {/key} +
+ + +
+
+ + diff --git a/apps/staged/src/lib/features/notes/noteMarkdown.test.ts b/apps/staged/src/lib/features/notes/noteMarkdown.test.ts index cee12ef2e..24667bf60 100644 --- a/apps/staged/src/lib/features/notes/noteMarkdown.test.ts +++ b/apps/staged/src/lib/features/notes/noteMarkdown.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { noteMarkdownWithTitle, renderNoteMarkdown } from './noteMarkdown'; +import { + UNTITLED_NOTE_TITLE, + noteMarkdownWithTitle, + renderNoteMarkdown, + splitNoteMarkdown, +} from './noteMarkdown'; describe('noteMarkdownWithTitle', () => { it('prepends the note title as a markdown H1', () => { @@ -24,6 +29,46 @@ describe('noteMarkdownWithTitle', () => { }); }); +describe('splitNoteMarkdown', () => { + it('takes the leading H1 as the title and the rest as the body', () => { + expect(splitNoteMarkdown('# Release plan\n\nShip on Friday.')).toEqual({ + title: 'Release plan', + body: 'Ship on Friday.', + }); + }); + + it('handles a note that is only a title', () => { + expect(splitNoteMarkdown('# Release plan')).toEqual({ title: 'Release plan', body: '' }); + }); + + it('round-trips with noteMarkdownWithTitle', () => { + const { title, body } = splitNoteMarkdown('# Release plan\n\nShip on Friday.'); + + expect(noteMarkdownWithTitle(title, body)).toBe('# Release plan\n\nShip on Friday.'); + }); + + it('falls back to the first non-empty line when there is no H1', () => { + expect(splitNoteMarkdown('\n\nJust a thought.\n\nMore.')).toEqual({ + title: 'Just a thought.', + body: '\n\nJust a thought.\n\nMore.', + }); + }); + + it('strips heading markers from the fallback title', () => { + expect(splitNoteMarkdown('## Overview\n\nDetails.').title).toBe('Overview'); + }); + + it('clips a long fallback title', () => { + const long = 'x'.repeat(120); + + expect(splitNoteMarkdown(long).title).toBe(`${'x'.repeat(80)}…`); + }); + + it('names a note with nothing usable in it', () => { + expect(splitNoteMarkdown(' \n\n ').title).toBe(UNTITLED_NOTE_TITLE); + }); +}); + describe('renderNoteMarkdown', () => { it('uses the shared markdown renderer', () => { const html = renderNoteMarkdown('```pikchr\nbox "Start" fit\n```'); diff --git a/apps/staged/src/lib/features/notes/noteMarkdown.ts b/apps/staged/src/lib/features/notes/noteMarkdown.ts index 32c688c1e..f863f64c8 100644 --- a/apps/staged/src/lib/features/notes/noteMarkdown.ts +++ b/apps/staged/src/lib/features/notes/noteMarkdown.ts @@ -18,6 +18,47 @@ function startsWithMarkdownH1(content: string): boolean { return /^#[ \t]+\S/.test(content); } +/** Title used when a written note has no heading and no usable first line. */ +export const UNTITLED_NOTE_TITLE = 'Untitled note'; + +const TITLE_MAX_LENGTH = 80; + +/** + * Split a written note's markdown into the `(title, body)` pair the store keeps. + * + * Notes have no separate title field: the leading H1 is the title, and the + * stored content is everything after it — the same shape `resolve_note_title_and_body` + * produces for session notes, so viewers and `#note:` references treat both alike. + * `noteMarkdownWithTitle` is the inverse, recombining them for display or editing. + * + * Without a leading H1 there is no session prompt to fall back on, so the first + * non-empty line stands in, clipped to a title-sized string. + */ +export function splitNoteMarkdown(markdown: string): { title: string; body: string } { + const trimmed = markdown.trimStart(); + const h1 = /^#[ \t]+(.*)/.exec(trimmed); + if (h1) { + const newline = trimmed.indexOf('\n'); + const title = ( + newline === -1 ? h1[1] : trimmed.slice(0, newline).replace(/^#[ \t]+/, '') + ).trim(); + const body = newline === -1 ? '' : trimmed.slice(newline + 1).trimStart(); + if (title) return { title, body }; + } + return { title: titleFromFirstLine(markdown), body: markdown }; +} + +function titleFromFirstLine(markdown: string): string { + const firstLine = markdown + .split('\n') + .map((line) => line.replace(/^#{1,6}[ \t]+/, '').trim()) + .find((line) => line.length > 0); + if (!firstLine) return UNTITLED_NOTE_TITLE; + return firstLine.length > TITLE_MAX_LENGTH + ? `${firstLine.slice(0, TITLE_MAX_LENGTH)}…` + : firstLine; +} + export function renderNoteMarkdown(text: string, options: MarkdownRenderingOptions = {}): string { return renderMarkdown(text, options); } diff --git a/apps/staged/src/lib/features/sessions/hashtagItems.test.ts b/apps/staged/src/lib/features/sessions/hashtagItems.test.ts index 292000eb0..c8c0b67f4 100644 --- a/apps/staged/src/lib/features/sessions/hashtagItems.test.ts +++ b/apps/staged/src/lib/features/sessions/hashtagItems.test.ts @@ -126,6 +126,7 @@ describe('timelineToHashtagItems', () => { completedAt: 1000, suggestedNextCommitStep: null, suggestedNextNoteStep: null, + subtype: null, }, { id: 'new-note', @@ -139,6 +140,7 @@ describe('timelineToHashtagItems', () => { completedAt: 5000, suggestedNextCommitStep: null, suggestedNextNoteStep: null, + subtype: null, }, ], commits: [ diff --git a/apps/staged/src/lib/features/sessions/noteFreshness.ts b/apps/staged/src/lib/features/sessions/noteFreshness.ts index 08bf27de0..605925f2e 100644 --- a/apps/staged/src/lib/features/sessions/noteFreshness.ts +++ b/apps/staged/src/lib/features/sessions/noteFreshness.ts @@ -1,4 +1,4 @@ -import type { Session, SessionMessage } from '../../types'; +import type { NoteSubtype, Session, SessionMessage } from '../../types'; export interface LinkedNoteContext { id: string; @@ -15,6 +15,8 @@ export interface NoteClickInfo { content: string; sessionId?: string; updatedAt?: number; + /** `'written'` routes the click to the editor instead of the read-only viewer. */ + subtype?: NoteSubtype; } export function countAssistantMessagesAfterNote( diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index 88a3345cb..e58fe8324 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -14,11 +14,14 @@ import GitCommitVertical from '@lucide/svelte/icons/git-commit-vertical'; import FileSearch from '@lucide/svelte/icons/file-search'; import Plus from '@lucide/svelte/icons/plus'; + import MoreHorizontal from '@lucide/svelte/icons/more-horizontal'; + import PencilLine from '@lucide/svelte/icons/pencil-line'; import { isResumableReason } from '../../types'; import type { BranchGitState, BranchTimeline as BranchTimelineData, HashtagItem, + NoteSubtype, UpstreamRelation, } from '../../types'; import type { NoteClickInfo } from '../sessions/noteFreshness'; @@ -27,7 +30,9 @@ type TimelineContextMenuAction, } from './TimelineContextMenu.svelte'; import { Button } from '$lib/components/ui/button'; + import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import type { TimelineItemType, TimelineBadge } from './TimelineRow.svelte'; + import { computeFooterOverflow, overflowedActions } from './footerOverflow'; import { escapeHtml, hasHashtagTokens, renderHashtagTokens } from '../sessions/hashtagItems'; import { formatRelativeTime, @@ -89,7 +94,10 @@ >; onNewNote?: () => void; onNewCommit?: () => void; - onNewReview?: (e: MouseEvent) => void; + /** Alt-click skips the prompt dialog; the overflow menu item passes no event. */ + onNewReview?: (e?: MouseEvent) => void; + /** Open the editor for a note the user writes themselves (no agent session). */ + onWriteNote?: () => void; onPullOrigin?: () => void; onPushOrigin?: () => void; onRebaseBranch?: () => void; @@ -175,6 +183,7 @@ onNewNote, onNewCommit, onNewReview, + onWriteNote, onPullOrigin, onPushOrigin, onRebaseBranch, @@ -266,6 +275,7 @@ noteTitle?: string; noteContent?: string; noteUpdatedAt?: number; + noteSubtype?: NoteSubtype; reviewId?: string; imageId?: string; imageFilename?: string; @@ -744,6 +754,7 @@ noteTitle: stripXmlTags(note.title), noteContent: note.content, noteUpdatedAt: note.updatedAt, + noteSubtype: note.subtype, deleteDisabledReason: isDeleting ? 'Deleting...' : undefined, completionReason: note.completionReason, hashtagRef: type === 'note' ? `#note:${note.id}` : undefined, @@ -920,7 +931,7 @@ return actions; }); let actionFooterVisible = $derived( - !!onNewNote || !!onNewCommit || !!onNewReview || !!footerActions + !!onNewNote || !!onNewCommit || !!onNewReview || !!onWriteNote || !!footerActions ); /** True when the timeline has no content and action buttons should be enlarged. */ @@ -928,6 +939,45 @@ items.length === 0 && pendingDropNotes.length === 0 && pendingItems.length === 0 ); + // ── Footer overflow ─────────────────────────────────────────────────── + // + // The label tiers are container queries, but the `…` menu's content is + // portaled outside the `timeline` container, so which items it lists has to + // come from a measured width. See `footerOverflow.ts`. + + let timelineEl = $state(null); + let timelineWidth = $state(0); + + $effect(() => { + const el = timelineEl; + if (!el) { + timelineWidth = 0; + return; + } + const observer = new ResizeObserver((entries) => { + const entry = entries[entries.length - 1]; + // Entry sizes are in local CSS pixels, so an animating ancestor (card + // expand, dialog zoom) can't report a transformed width. + timelineWidth = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width; + }); + observer.observe(el); + return () => observer.disconnect(); + }); + + let footerActionsAvailable = $derived({ + note: !!onNewNote, + commit: !!onNewCommit, + review: !!onNewReview, + }); + // The enlarged empty-timeline state stacks full-width pills and hides the + // right group, so it has room for all three regardless of card width. + let footerOverflow = $derived( + actionButtonsEnlarged + ? computeFooterOverflow(0, footerActionsAvailable) + : computeFooterOverflow(timelineWidth, footerActionsAvailable) + ); + let overflowMenuActions = $derived(overflowedActions(footerOverflow, footerActionsAvailable)); + // ── Handlers ────────────────────────────────────────────────────────── function handleItemClick(item: DisplayItem) { @@ -940,6 +990,7 @@ content: item.noteContent ?? '', sessionId: item.sessionId, updatedAt: item.noteUpdatedAt, + subtype: item.noteSubtype, }); } else if (item.type === 'review' && item.reviewId && onReviewClick) { onReviewClick(item.reviewId); @@ -1040,7 +1091,7 @@ {:else} -
+
{#each normalItems as item, index (item.key)}
- {#if onNewNote} + {#if onNewNote && !footerOverflow.note}
{#if footerActions && !actionButtonsEnlarged} {@render footerActions()} diff --git a/apps/staged/src/lib/features/timeline/footerOverflow.test.ts b/apps/staged/src/lib/features/timeline/footerOverflow.test.ts new file mode 100644 index 000000000..40d6a48bb --- /dev/null +++ b/apps/staged/src/lib/features/timeline/footerOverflow.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; + +import { computeFooterOverflow, overflowedActions } from './footerOverflow'; + +const ALL = { note: true, commit: true, review: true }; + +describe('computeFooterOverflow', () => { + it('keeps every button on a wide card', () => { + expect(computeFooterOverflow(720, ALL)).toEqual({ + note: false, + commit: false, + review: false, + }); + }); + + it('sheds buttons review-first, then commit, then note', () => { + expect(computeFooterOverflow(370, ALL)).toMatchObject({ review: true, commit: false }); + expect(computeFooterOverflow(310, ALL)).toMatchObject({ review: true, commit: true }); + expect(computeFooterOverflow(250, ALL)).toEqual({ note: true, commit: true, review: true }); + }); + + it('reports actions the card is not offering as hidden', () => { + expect(computeFooterOverflow(720, { note: true, commit: true })).toEqual({ + note: false, + commit: false, + review: true, + }); + }); + + it('gives the remaining buttons the space an absent one frees', () => { + // Two buttons still fit at 340px even though three would not. + expect(computeFooterOverflow(340, { note: true, commit: true })).toMatchObject({ + note: false, + commit: false, + }); + expect(computeFooterOverflow(340, ALL)).toMatchObject({ review: true }); + }); + + it('shows everything until the container has been measured', () => { + expect(computeFooterOverflow(0, ALL)).toEqual({ note: false, commit: false, review: false }); + }); +}); + +describe('overflowedActions', () => { + it('lists overflowed actions in button order', () => { + expect(overflowedActions(computeFooterOverflow(250, ALL), ALL)).toEqual([ + 'note', + 'commit', + 'review', + ]); + }); + + it('omits actions the card never offered', () => { + const available = { note: true, commit: true }; + + expect(overflowedActions(computeFooterOverflow(250, available), available)).toEqual([ + 'note', + 'commit', + ]); + }); + + it('is empty while everything fits', () => { + expect(overflowedActions(computeFooterOverflow(720, ALL), ALL)).toEqual([]); + }); +}); diff --git a/apps/staged/src/lib/features/timeline/footerOverflow.ts b/apps/staged/src/lib/features/timeline/footerOverflow.ts new file mode 100644 index 000000000..272588143 --- /dev/null +++ b/apps/staged/src/lib/features/timeline/footerOverflow.ts @@ -0,0 +1,77 @@ +/** + * Which timeline footer buttons collapse into the `…` menu at a given width. + * + * The buttons' label tiers (full label → `+` and short label → icon only) are + * pure CSS container queries. This last tier can't be: the menu's content is + * portaled out of the `timeline` container, so no `@container` query can decide + * which items it should show. Both sides therefore read the same measured width + * through this module, keeping button and menu in sync by construction. + */ + +/** Left-aligned footer actions, in the order they appear on the card. */ +export type FooterAction = 'note' | 'commit' | 'review'; + +/** Which of the three session actions the card is currently offering. */ +export type FooterActionAvailability = Partial>; + +export type FooterOverflowState = Record; + +/** + * Order actions leave the footer in. Review goes first — it is the widest label + * and the most situational — then Commit, leaving Note as the last one standing. + */ +const OVERFLOW_ORDER: readonly FooterAction[] = ['review', 'commit', 'note']; + +/** + * Minimum timeline width (px) that still fits N icon-only buttons alongside the + * `…` trigger and the right-aligned PR/Diff group. Index is the button count, so + * index 0 is the always-fits case. + * + * These sit below the 480px icon-only tier in `BranchTimeline.svelte`, which is + * where the buttons have already shed their labels. + */ +const MIN_WIDTH_FOR_BUTTONS: readonly number[] = [0, 260, 320, 380]; + +/** + * Decide which footer buttons to hide at `widthPx`, given which ones exist. + * + * Actions the card isn't offering are reported as hidden, so callers can render + * the menu straight from this result without re-checking availability. + */ +export function computeFooterOverflow( + widthPx: number, + available: FooterActionAvailability +): FooterOverflowState { + const hidden: FooterOverflowState = { + note: !available.note, + commit: !available.commit, + review: !available.review, + }; + + // A zero width means the container hasn't been measured yet (first paint, + // or an off-screen card). Showing everything matches the pre-overflow + // behaviour and self-corrects on the first ResizeObserver callback. + if (widthPx <= 0) return hidden; + + let visible = OVERFLOW_ORDER.filter((action) => !hidden[action]).length; + for (const action of OVERFLOW_ORDER) { + if (visible === 0 || widthPx >= MIN_WIDTH_FOR_BUTTONS[visible]) break; + if (hidden[action]) continue; + hidden[action] = true; + visible -= 1; + } + return hidden; +} + +/** + * The actions that overflowed into the menu, in their original button order. + * Excludes actions the card never offered. + */ +export function overflowedActions( + overflow: FooterOverflowState, + available: FooterActionAvailability +): FooterAction[] { + return (['note', 'commit', 'review'] as const).filter( + (action) => available[action] && overflow[action] + ); +} diff --git a/apps/staged/src/lib/types.ts b/apps/staged/src/lib/types.ts index 2d023080d..9977c37d1 100644 --- a/apps/staged/src/lib/types.ts +++ b/apps/staged/src/lib/types.ts @@ -184,8 +184,19 @@ export interface NoteTimelineItem { completedAt: number | null; suggestedNextCommitStep: string | null; suggestedNextNoteStep: string | null; + subtype: NoteSubtype; } +/** + * How a note's content came to be. `null` means an agent session produced it; + * `'written'` means the user authored it directly, so it opens in the editor + * rather than the read-only viewer. + */ +export type NoteSubtype = 'written' | null; + +/** Subtype marking a user-authored note. */ +export const WRITTEN_NOTE_SUBTYPE = 'written'; + /** A full branch note record, as returned by `get_branch_note_by_session`. */ export interface BranchNote { id: string; @@ -198,6 +209,7 @@ export interface BranchNote { completedAt: number | null; suggestedNextCommitStep: string | null; suggestedNextNoteStep: string | null; + subtype: NoteSubtype; } export interface ReviewTimelineItem { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26dbf7ff7..56743290d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,6 +174,9 @@ importers: '@builderbot/diff-viewer': specifier: workspace:* version: link:../../packages/diff-viewer + '@milkdown/crepe': + specifier: ^7.22.1 + version: 7.22.1(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(typescript@6.0.3) '@tauri-apps/api': specifier: ^2.11.1 version: 2.11.1 @@ -390,6 +393,10 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} @@ -407,6 +414,11 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-transform-react-jsx-self@7.27.1': resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} engines: {node: '>=6.9.0'} @@ -435,6 +447,10 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -457,6 +473,99 @@ packages: '@chevrotain/utils@11.1.1': resolution: {integrity: sha512-71eTYMzYXYSFPrbg/ZwftSaSDld7UYlS8OQa3lNnn9jzNtpFbaReRRyghzqS7rI3CDaorqpPJJcXGHK+FE1TVQ==} + '@codemirror/autocomplete@6.20.3': + resolution: {integrity: sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==} + + '@codemirror/commands@6.11.0': + resolution: {integrity: sha512-/K4Rl5BN0OtTiPWmJCdqODu38XnDMsDxKY5rgrPnCkutPTJf2wVbkoixLfealF5Kwse/s8P8M5jAiURiwSwnFA==} + + '@codemirror/lang-angular@0.1.4': + resolution: {integrity: sha512-oap+gsltb/fzdlTQWD6BFF4bSLKcDnlxDsLdePiJpCVNKWXSTAbiiQeYI3UmES+BLAdkmIC1WjyztC1pi/bX4g==} + + '@codemirror/lang-cpp@6.0.3': + resolution: {integrity: sha512-URM26M3vunFFn9/sm6rzqrBzDgfWuDixp85uTY49wKudToc2jTHUrKIGGKs+QWND+YLofNNZpxcNGRynFJfvgA==} + + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} + + '@codemirror/lang-go@6.0.1': + resolution: {integrity: sha512-7fNvbyNylvqCphW9HD6WFnRpcDjr+KXX/FgqXy5H5ZS0eC5edDljukm/yNgYkwTsgp2busdod50AOTIy6Jikfg==} + + '@codemirror/lang-html@6.4.12': + resolution: {integrity: sha512-pw2ReWKUqSkbvh76RAT4NYxiogRu+PWkR2ukAwO9uOgrm8uipkzjtKKtNpyeAQwHOqxEeSvAXZ6vr3AfyB9y/w==} + + '@codemirror/lang-java@6.0.2': + resolution: {integrity: sha512-m5Nt1mQ/cznJY7tMfQTJchmrjdjQ71IDs+55d1GAa8DGaB8JXWsVCkVT284C3RTASaY43YknrK2X3hPO/J3MOQ==} + + '@codemirror/lang-javascript@6.2.5': + resolution: {integrity: sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==} + + '@codemirror/lang-jinja@6.0.1': + resolution: {integrity: sha512-P5kyHLObzjtbGj16h+hyvZTxJhSjBEeSx4wMjbnAf3b0uwTy2+F0zGjMZL4PQOm/mh2eGZ5xUDVZXgwP783Nsw==} + + '@codemirror/lang-json@6.0.2': + resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} + + '@codemirror/lang-less@6.0.2': + resolution: {integrity: sha512-EYdQTG22V+KUUk8Qq582g7FMnCZeEHsyuOJisHRft/mQ+ZSZ2w51NupvDUHiqtsOy7It5cHLPGfHQLpMh9bqpQ==} + + '@codemirror/lang-liquid@6.3.2': + resolution: {integrity: sha512-6PDVU3ZnfeYyz1at1E/ttorErZvZFXXt1OPhtfe1EZJ2V2iDFa0CwPqPgG5F7NXN0yONGoBogKmFAafKTqlwIw==} + + '@codemirror/lang-markdown@6.5.2': + resolution: {integrity: sha512-AwBOdkWYuA//WcM0xO5PfHPUcmz/O2i5o0Nsg1U69SII/loCJlFI1Romd9xp2HYb1kYJRGZotyqRghuHH5n8Kw==} + + '@codemirror/lang-php@6.0.2': + resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} + + '@codemirror/lang-python@6.2.1': + resolution: {integrity: sha512-IRjC8RUBhn9mGR9ywecNhB51yePWCGgvHfY1lWN/Mrp3cKuHr0isDKia+9HnvhiWNnMpbGhWrkhuWOc09exRyw==} + + '@codemirror/lang-rust@6.0.2': + resolution: {integrity: sha512-EZaGjCUegtiU7kSMvOfEZpaCReowEf3yNidYu7+vfuGTm9ow4mthAparY5hisJqOHmJowVH3Upu+eJlUji6qqA==} + + '@codemirror/lang-sass@6.0.2': + resolution: {integrity: sha512-l/bdzIABvnTo1nzdY6U+kPAC51czYQcOErfzQ9zSm9D8GmNPD0WTW8st/CJwBTPLO8jlrbyvlSEcN20dc4iL0Q==} + + '@codemirror/lang-sql@6.10.0': + resolution: {integrity: sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==} + + '@codemirror/lang-vue@0.1.3': + resolution: {integrity: sha512-QSKdtYTDRhEHCfo5zOShzxCmqKJvgGrZwDQSdbvCRJ5pRLWBS7pD/8e/tH44aVQT6FKm0t6RVNoSUWHOI5vNug==} + + '@codemirror/lang-wast@6.0.2': + resolution: {integrity: sha512-Imi2KTpVGm7TKuUkqyJ5NRmeFWF7aMpNiwHnLQe0x9kmrxElndyH0K6H/gXtWwY6UshMRAhpENsgfpSwsgmC6Q==} + + '@codemirror/lang-xml@6.1.0': + resolution: {integrity: sha512-3z0blhicHLfwi2UgkZYRPioSgVTo9PV5GP5ducFH6FaHy0IAJRg+ixj5gTR1gnT/glAIC8xv4w2VL1LoZfs+Jg==} + + '@codemirror/lang-yaml@6.1.3': + resolution: {integrity: sha512-AZ8DJBuXGVHybpBQhmZtgew5//4hv3tdkXnr3vDmOUMJRuB6vn/uuwtmTOTlqEaQFg3hQSVeA90NmvIQyUV6FQ==} + + '@codemirror/language-data@6.5.2': + resolution: {integrity: sha512-CPkWBKrNS8stYbEU5kwBwTf3JB1kghlbh4FSAwzGW2TEscdeHHH4FGysREW86Mqnj3Qn09s0/6Ea/TutmoTobg==} + + '@codemirror/language@6.12.4': + resolution: {integrity: sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==} + + '@codemirror/legacy-modes@6.5.3': + resolution: {integrity: sha512-xCsmIzH78MyWkib9jlPaaun57XNkfbMIhagfaZVd0iLTqlpw3jXaIcbZm72MTmmn64eTZpBVNjbyYh+QXnxRsg==} + + '@codemirror/lint@6.9.7': + resolution: {integrity: sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==} + + '@codemirror/search@6.7.1': + resolution: {integrity: sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==} + + '@codemirror/state@6.7.1': + resolution: {integrity: sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==} + + '@codemirror/theme-one-dark@6.1.3': + resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} + + '@codemirror/view@6.43.9': + resolution: {integrity: sha512-sTuUzTpPMFebRhg6dawChoKKgndIwfjmJgKVxBefPElcU2NwQ6AFroupk0SFqEerQyZOGRfDNnSN8Dw/lMAsXw==} + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -754,20 +863,150 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} + + '@lezer/cpp@1.1.6': + resolution: {integrity: sha512-vh9gWWJOXFVY8HBHK3Twzq8MgwG2iN4GSyzBP9sCGTe37P15x2R14VaBQk0VA0ezTRN1KHYBBsHhvpGZ2Xy/pA==} + + '@lezer/css@1.3.6': + resolution: {integrity: sha512-YJE78Wcg+zX8f10hiHWQ4Az48Qr/c13eId0VtRQYLBpxHDmDeSrXIlkbl+fJGW42rWC/uoUco9mhBZeVWP/A1g==} + + '@lezer/go@1.0.1': + resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==} + + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} + + '@lezer/html@1.3.13': + resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==} + + '@lezer/java@1.1.3': + resolution: {integrity: sha512-yHquUfujwg6Yu4Fd1GNHCvidIvJwi/1Xu2DaKl/pfWIA2c1oXkVvawH3NyXhCaFx4OdlYBVX5wvz2f7Aoa/4Xw==} + + '@lezer/javascript@1.5.4': + resolution: {integrity: sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==} + + '@lezer/json@1.0.3': + resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} + + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} + + '@lezer/markdown@1.7.2': + resolution: {integrity: sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==} + + '@lezer/php@1.0.5': + resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} + + '@lezer/python@1.1.19': + resolution: {integrity: sha512-MhQIURHRytsNzP/YXnqpYKW6la6voAH3kyplTOOiCdjyFY6cWWGFVmYVdHIPrElqSDf4iCDktQCockB9FxuhzQ==} + + '@lezer/rust@1.0.2': + resolution: {integrity: sha512-Lz5sIPBdF2FUXcWeCu1//ojFAZqzTQNRga0aYv6dYXqJqPfMdCAI0NzajWUd4Xijj1IKJLtjoXRPMvTKWBcqKg==} + + '@lezer/sass@1.1.0': + resolution: {integrity: sha512-3mMGdCTUZ/84ArHOuXWQr37pnf7f+Nw9ycPUeKX+wu19b7pSMcZGLbaXwvD2APMBDOGxPmpK/O6S1v1EvLoqgQ==} + + '@lezer/xml@1.0.6': + resolution: {integrity: sha512-CdDwirL0OEaStFue/66ZmFSeppuL6Dwjlk8qk153mSQwiSH/Dlri4GNymrNWnUmPl2Um7QfV1FO9KFUyX3Twww==} + + '@lezer/yaml@1.0.4': + resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==} + '@lucide/svelte@1.22.0': resolution: {integrity: sha512-eaNC3GGu9ma7mviB9vPL6OnawXqxdvRnoAQSq5l15mBlsuwD7kozZ7pzPXSlT6OwSl7hz4qTk+ZU3OEewwi5gQ==} peerDependencies: svelte: ^5 + '@marijn/find-cluster-break@1.0.4': + resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==} + '@mermaid-js/parser@1.0.0': resolution: {integrity: sha512-vvK0Hi/VWndxoh03Mmz6wa1KDriSPjS2XMZL/1l19HFwygiObEEoEwSDxOqyLzzAI6J2PU3261JjTMTO7x+BPw==} + '@milkdown/components@7.22.1': + resolution: {integrity: sha512-6IA8fcFcBTm/x1X1typz73yUoT1JuN4srmifGAIeWEtCnayEwRjFxpQOoQrfvMgBYx4DSHcPVLhJUlR/xbBtxg==} + peerDependencies: + '@codemirror/language': ^6 + '@codemirror/state': ^6 + '@codemirror/view': ^6 + + '@milkdown/core@7.22.1': + resolution: {integrity: sha512-X4O3al2aYugpFP/Do7VoPgvVEftjJVi+6UhZmz9fSdqMtpU70AitJ8R/erUA18hay2hWvNO9m3pxIJoR6no6KQ==} + + '@milkdown/crepe@7.22.1': + resolution: {integrity: sha512-0BZTGBRV8lnlKsA3vmPK5Qyomjqd952493AIzfnbhiYwzHpluX03iMtOgHC6E2UcpMXP86P5CXhPUWjzDu+JZQ==} + + '@milkdown/ctx@7.22.1': + resolution: {integrity: sha512-6+LJdo3DcrkO1lzGcje0yWqaG6ZKgTR8kDxyDrrm7iocBjq3M+bkw04f99ZG/BLp1Rbc4yRyin2mUy6m4SEVaA==} + + '@milkdown/exception@7.22.1': + resolution: {integrity: sha512-wXUfn+fpWy0jeJ6AVEjKPieouLfJIDLQ4IRqyEL3d5yZDiZTtKu1Va0+RprrvqANpGJ1YBRQ1uCgDE6sJgTEjQ==} + + '@milkdown/kit@7.22.1': + resolution: {integrity: sha512-gXLMhjqe0j8XRSUe97LBXbpZ0sDi5EM6nnt1zXewUaGDS3mleMjNntSCWrp9ln40kz8tGs0ol50kSAr1n/JBnQ==} + + '@milkdown/plugin-block@7.22.1': + resolution: {integrity: sha512-R0gOuKRqR1DyFULD75Oic0+57DXTthagUZbbB9lsHM59aQmxKJ1nRmujptPeMfDlDfEod2ao5rsow7LO8p+ZWQ==} + + '@milkdown/plugin-clipboard@7.22.1': + resolution: {integrity: sha512-H1meyLEj2fOnFWSopeQ2H7hl6VXM+OQZoU9H7GrlHFM5GKtBFBErFFV4Cl5tUSVFY+xYy/4rEPOA23Ba1u6OfA==} + + '@milkdown/plugin-cursor@7.22.1': + resolution: {integrity: sha512-3tJzpQl5cyn+8vzsHkPBYsW6QacEN7yP2O5GkyUjrNLvMIQ5vVi7GUuztLeoouJKlOc1vjIq3h7ARXCgHAKe8g==} + + '@milkdown/plugin-diff@7.22.1': + resolution: {integrity: sha512-qjuCMx11HwhMlTJuybqbFZT8PQkasSi0qICxq2HdYqH0WQuLO99I2mb17UAJz1aF+JM9rp1StHWr4bxC0ofamA==} + + '@milkdown/plugin-history@7.22.1': + resolution: {integrity: sha512-SRD6emVhfVmA6mrcCsCiWrONN/2Kp+VT5LLojIQxNc/spzjWw15PtkA3r4nUe9cZmNN5rE7XvrPaA0WnjJIyeQ==} + + '@milkdown/plugin-indent@7.22.1': + resolution: {integrity: sha512-s/wmqxaJpIKT01gEXsC9gCkzA3Clm+MNioZLzNCYD8xI/4Hf3AhmIEoEqf3LIQ9w10Zs2yhIxIrknNWfCOgsEw==} + + '@milkdown/plugin-listener@7.22.1': + resolution: {integrity: sha512-6k8JMDrdAL0E0WygpDX1YOR0Q9Wc4pia8pIMN5nN5liEeAyNaM6LwAxXJY+awQJ0OstDKHTfjkh7WzJo4QiXOA==} + + '@milkdown/plugin-slash@7.22.1': + resolution: {integrity: sha512-eFKCjfgXfAQusTSOyzhz4nASquOApdiBy9hnGkPB/SJ6oltTC8SonB3nIxF/X0wPZHfn09vuz/JBqh9cyDtXJA==} + + '@milkdown/plugin-streaming@7.22.1': + resolution: {integrity: sha512-CR0lujyc7ae0sbjNfscvS+b+cUP7b6XJ90ioTor605UV+F8EKh5t4PMJM79IVzSUCeob51a+rtXA2LAA1b/GyA==} + + '@milkdown/plugin-tooltip@7.22.1': + resolution: {integrity: sha512-drdWA/7WlrDr2B+ABYf4tY9xTiwg/CJMUCydfD05dR2d8wOkaF6oOHZwJbWnkWLMtAJWdieOYgfXGPhQRwXxCg==} + + '@milkdown/plugin-trailing@7.22.1': + resolution: {integrity: sha512-Buy8IX3I7wwq0QPfJRep77QnvlkBeg6BvDHkbh5eTMrgnwSiWhN+tV9RVCYpdWXM7QhQZpSjU2SejluLuTyVCg==} + + '@milkdown/plugin-upload@7.22.1': + resolution: {integrity: sha512-vV0bZzdh/PhM+Jk9Nk5cKHDXHY5jt+ePcaitTfvESDYfg83YZ9AglOhw/YclPWklMvswrnCGZZFTZD4daqc4hg==} + + '@milkdown/preset-commonmark@7.22.1': + resolution: {integrity: sha512-it+G0YUG5MDXt0qLB6W083ss5i6tnWAXWCW8SgnmsehGxntkN5//gNM/8vh3rqwl8RvnBvjLWpJ6TgHSlBPhlw==} + + '@milkdown/preset-gfm@7.22.1': + resolution: {integrity: sha512-UPMdHRdMHlVourOOTiwsp3qHu614pDFWlWtVzxe8fyl8uKZUk6IOuewH5ubhqD/opos8OEB+zgEomUGfee1oTw==} + + '@milkdown/prose@7.22.1': + resolution: {integrity: sha512-fqgTHl94G7oDPY94cPYBm6ARxEoLQ10ubkTNLD1nzAlTUfJLULZkIEMSMy4CECncw2go3ExW32CZPpXLJVSxnQ==} + + '@milkdown/transformer@7.22.1': + resolution: {integrity: sha512-rU1IBtxezNg7TSWaq+RP59/t6tfD3jIPGrwAtMKPIGuYEax+8d1fyGisp98TeI3l0VQEIgMpF+KlNDV+Y1HJVA==} + + '@milkdown/utils@7.22.1': + resolution: {integrity: sha512-ye0LGy/Ez8fjtcYyYj93wp4XfH00JYpCiy1IDHYB+9HVIpnbr3ed7PvY4s2000xmDWe0Vlx07TOyv96yvFrnjQ==} + '@napi-rs/wasm-runtime@1.1.6': resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} peerDependencies: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@ocavue/utils@1.7.0': + resolution: {integrity: sha512-yEk9ATNBjTZTtuVFMB/MAIF6zJBvJ2+lVNQvK2+O+ggEBGTgx2tp27d4FPgmD5bRsNHHP3D0SleQia/bvIeV8w==} + '@oxc-project/types@0.137.0': resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} @@ -832,7 +1071,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] - libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.3': resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} @@ -923,6 +1161,7 @@ packages: resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} @@ -976,6 +1215,7 @@ packages: resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} @@ -1557,6 +1797,15 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/katex@0.16.8': + resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.25': + resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1631,6 +1880,33 @@ packages: '@vitest/utils@4.1.9': resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@vue/compiler-core@3.5.41': + resolution: {integrity: sha512-q0Xtv/F9w2YO/7htQhtiL+Ev2WCJbe5N2hc+XfgyKkEKqWpSxknmT8QOuGdEKNdjPq0c3F7rNpFkTo3Kfrm7pg==} + + '@vue/compiler-dom@3.5.41': + resolution: {integrity: sha512-oKacVfNglLvGjnS6BXOlGL7EyG2h8X03pqXCjzotRZUaXGjbrTJUnVAQjrCqUnS+lyu31nwQjZY/d817GmCnfw==} + + '@vue/compiler-sfc@3.5.41': + resolution: {integrity: sha512-XJhip7R2wy6vX3knCxdZN4KracFaZUef58s1KYewqluedHIJaPIVfXoYT7MF1F8nCvv6k8bWWxDC8opMkg1VTQ==} + + '@vue/compiler-ssr@3.5.41': + resolution: {integrity: sha512-U3v5OejKEGqOI0Wy0+Sz7hGuIFZHA4LSXzrNM3IMIeDyJEBBfTpX26n3SDgToRpP2bLc9FfI2j/kSgcJ8Emq5A==} + + '@vue/reactivity@3.5.41': + resolution: {integrity: sha512-rznsqKM0np0x18EjzF8x88MpEhdNsffbvFbckLL5+oUKz1BxAImEmO7J1ArRYSyo6aQaVoBDp7jEkT91OOxydA==} + + '@vue/runtime-core@3.5.41': + resolution: {integrity: sha512-Vcry58hiAKwGen9Z1jUZE0feFsNArPCMOImYI8el48A9Idf6DuQYD0U05zZIF2Iad1hGhPSvcbBbAOhNr55fhg==} + + '@vue/runtime-dom@3.5.41': + resolution: {integrity: sha512-3vVBahVBS9+U6cmXBLyb8nE6/yYo4J/CGI9eVFs3KiMc0YHuudwKyShTD65jtJy/L9PUUxNAFu4cj4LiJ0UFbw==} + + '@vue/server-renderer@3.5.41': + resolution: {integrity: sha512-n6hx/pNFfbD6SuyeuMVkvqox8bwf/ET9JlA/kAz/imw8sw++wkqKe2mHX5KutjPpbKE4Z56yTHszoOjGMI9igQ==} + + '@vue/shared@3.5.41': + resolution: {integrity: sha512-IOnwSCma8j+9xJT6b8H0dEYidC80NsYmNMlZxRsukYcSoGaDBohog5hDxzeUXdFeGWFA++vWvxqOmrr96VlqMA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1745,6 +2021,9 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + codemirror@6.0.2: + resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} @@ -1776,6 +2055,9 @@ packages: cose-base@2.2.0: resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + crelt@1.0.7: + resolution: {integrity: sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -2113,6 +2395,9 @@ packages: estree-util-is-identifier-name@3.0.0: resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2346,6 +2631,10 @@ packages: resolution: {integrity: sha512-q3N5u+1sY9Bu7T4nlXoiRBXWfwSefNGoKeOwekV+gw0cAXQlz2Ww6BLcmBxVDeXBMUDQv6fK5bcNaJLxob3ZQA==} hasBin: true + katex@0.18.4: + resolution: {integrity: sha512-IMPntbRLOU+eu88XDiFKqQ8Akhr9Tv7jDMXqPhjG9SI1JMA4DIgXk4x9k4skJz2NZJXBRbC+2pYBLj9olqcZow==} + hasBin: true + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2491,6 +2780,9 @@ packages: engines: {node: '>= 20'} hasBin: true + mdast-util-definitions@6.0.0: + resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -2515,6 +2807,9 @@ packages: mdast-util-gfm@3.1.0: resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-math@3.0.0: + resolution: {integrity: sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -2566,6 +2861,9 @@ packages: micromark-extension-gfm@3.0.0: resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-math@3.1.0: + resolution: {integrity: sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==} + micromark-factory-destination@2.0.1: resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} @@ -2649,6 +2947,16 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@6.0.1: + resolution: {integrity: sha512-3wVS3i51pE2pi1k5FFL/95BGfVS0kSsvDVuGXHOtxox/TywUmtgq+3qiTOTbs9J7KfHaXPiN171k/A6dBnaXFw==} + engines: {node: ^22 || ^24 || >=26} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -2678,6 +2986,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} @@ -2753,6 +3064,10 @@ packages: resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -2786,6 +3101,65 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.4.2: + resolution: {integrity: sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==} + + prosemirror-commands@1.7.2: + resolution: {integrity: sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==} + + prosemirror-drop-indicator@0.1.4: + resolution: {integrity: sha512-YaRB1pZmU5GCorPVWbc9dbhbwqr4iMBO/AjPu4BTKHCUzxEDUXje2dUyoxOHib/z4uyPUZTJz64h7mHDJZeSzA==} + + prosemirror-dropcursor@1.8.3: + resolution: {integrity: sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==} + + prosemirror-gapcursor@1.4.1: + resolution: {integrity: sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==} + + prosemirror-history@1.5.0: + resolution: {integrity: sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==} + + prosemirror-inputrules@1.5.1: + resolution: {integrity: sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==} + + prosemirror-keymap@1.2.3: + resolution: {integrity: sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==} + + prosemirror-model@1.25.11: + resolution: {integrity: sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==} + + prosemirror-safari-ime-span@1.0.2: + resolution: {integrity: sha512-QJqD8s1zE/CuK56kDsUhndh5hiHh/gFnAuPOA9ytva2s85/ZEt2tNWeALTJN48DtWghSKOmiBsvVn2OlnJ5H2w==} + + prosemirror-schema-list@1.5.1: + resolution: {integrity: sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==} + + prosemirror-state@1.4.4: + resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} + + prosemirror-tables@1.8.5: + resolution: {integrity: sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==} + + prosemirror-transform@1.12.0: + resolution: {integrity: sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==} + + prosemirror-view@1.42.2: + resolution: {integrity: sha512-Pdg0l5kXm8aLDquFAnQFTCITg0q44sLqBlHlpsVLD9segdOao8TOfQdAhCrCXyVgPSRr6UDDROOIWA3bIrN9YQ==} + + prosemirror-virtual-cursor@0.4.2: + resolution: {integrity: sha512-pUMKnIuOhhnMcgIJUjhIQTVJruBEGxfMBVQSrK0g2qhGPDm1i12KdsVaFw15dYk+29tZcxjMeR7P5VDKwmbwJg==} + peerDependencies: + prosemirror-model: ^1.0.0 + prosemirror-state: ^1.0.0 + prosemirror-view: ^1.0.0 + peerDependenciesMeta: + prosemirror-model: + optional: true + prosemirror-state: + optional: true + prosemirror-view: + optional: true + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2861,6 +3235,12 @@ packages: remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-inline-links@7.0.0: + resolution: {integrity: sha512-4uj1pPM+F495ySZhTIB6ay2oSkTsKgmYaKk/q5HIdhX2fuyLEegpjWa0VdJRJ01sgOqAFo7MBKdDUejIYBMVMQ==} + + remark-math@6.0.0: + resolution: {integrity: sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -2870,6 +3250,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -2887,6 +3270,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + roughjs@4.6.6: resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} @@ -2975,6 +3361,9 @@ packages: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -3115,6 +3504,9 @@ packages: unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-remove-position@5.0.0: + resolution: {integrity: sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -3309,6 +3701,17 @@ packages: vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + vue@3.5.41: + resolution: {integrity: sha512-2laE0p+aK+/AOPG/XL/WepOs/GlK755LJ1XECi9kDUrz1FKNw8rb2Xzlw9JS1rqEV55nb0ttsKxVlTCcd+R5cg==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} engines: {node: '>=18'} @@ -3459,6 +3862,8 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} + '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-validator-identifier@7.29.7': {} '@babel/helper-validator-option@7.27.1': {} @@ -3472,6 +3877,10 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': dependencies: '@babel/core': 7.29.0 @@ -3507,6 +3916,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@braintree/sanitize-url@7.1.2': {} '@bramus/specificity@2.4.2': @@ -3530,6 +3944,264 @@ snapshots: '@chevrotain/utils@11.1.1': {} + '@codemirror/autocomplete@6.20.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + + '@codemirror/commands@6.11.0': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + + '@codemirror/lang-angular@0.1.4': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-cpp@6.0.3': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/cpp': 1.1.6 + + '@codemirror/lang-css@6.3.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.6 + + '@codemirror/lang-go@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/go': 1.0.1 + + '@codemirror/lang-html@6.4.12': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.6 + '@lezer/html': 1.3.13 + + '@codemirror/lang-java@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/java': 1.1.3 + + '@codemirror/lang-javascript@6.2.5': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/javascript': 1.5.4 + + '@codemirror/lang-jinja@6.0.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-json@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/json': 1.0.3 + + '@codemirror/lang-less@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-liquid@6.3.2': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-markdown@6.5.2': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/markdown': 1.7.2 + + '@codemirror/lang-php@6.0.2': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/php': 1.0.5 + + '@codemirror/lang-python@6.2.1': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/python': 1.1.19 + + '@codemirror/lang-rust@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/rust': 1.0.2 + + '@codemirror/lang-sass@6.0.2': + dependencies: + '@codemirror/lang-css': 6.3.1 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/sass': 1.1.0 + + '@codemirror/lang-sql@6.10.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-vue@0.1.3': + dependencies: + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-wast@6.0.2': + dependencies: + '@codemirror/language': 6.12.4 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@codemirror/lang-xml@6.1.0': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/xml': 1.0.6 + + '@codemirror/lang-yaml@6.1.3': + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lezer/yaml': 1.0.4 + + '@codemirror/language-data@6.5.2': + dependencies: + '@codemirror/lang-angular': 0.1.4 + '@codemirror/lang-cpp': 6.0.3 + '@codemirror/lang-css': 6.3.1 + '@codemirror/lang-go': 6.0.1 + '@codemirror/lang-html': 6.4.12 + '@codemirror/lang-java': 6.0.2 + '@codemirror/lang-javascript': 6.2.5 + '@codemirror/lang-jinja': 6.0.1 + '@codemirror/lang-json': 6.0.2 + '@codemirror/lang-less': 6.0.2 + '@codemirror/lang-liquid': 6.3.2 + '@codemirror/lang-markdown': 6.5.2 + '@codemirror/lang-php': 6.0.2 + '@codemirror/lang-python': 6.2.1 + '@codemirror/lang-rust': 6.0.2 + '@codemirror/lang-sass': 6.0.2 + '@codemirror/lang-sql': 6.10.0 + '@codemirror/lang-vue': 0.1.3 + '@codemirror/lang-wast': 6.0.2 + '@codemirror/lang-xml': 6.1.0 + '@codemirror/lang-yaml': 6.1.3 + '@codemirror/language': 6.12.4 + '@codemirror/legacy-modes': 6.5.3 + + '@codemirror/language@6.12.4': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 + + '@codemirror/legacy-modes@6.5.3': + dependencies: + '@codemirror/language': 6.12.4 + + '@codemirror/lint@6.9.7': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + crelt: 1.0.7 + + '@codemirror/search@6.7.1': + dependencies: + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + crelt: 1.0.7 + + '@codemirror/state@6.7.1': + dependencies: + '@marijn/find-cluster-break': 1.0.4 + + '@codemirror/theme-one-dark@6.1.3': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@lezer/highlight': 1.2.3 + + '@codemirror/view@6.43.9': + dependencies: + '@codemirror/state': 6.7.1 + crelt: 1.0.7 + style-mod: 4.1.3 + w3c-keyname: 2.2.8 + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -3740,14 +4412,392 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lezer/common@1.5.2': {} + + '@lezer/cpp@1.1.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/css@1.3.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/go@1.0.1': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/highlight@1.2.3': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/html@1.3.13': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/java@1.1.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/javascript@1.5.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/json@1.0.3': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/lr@1.4.10': + dependencies: + '@lezer/common': 1.5.2 + + '@lezer/markdown@1.7.2': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + + '@lezer/php@1.0.5': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/python@1.1.19': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/rust@1.0.2': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/sass@1.1.0': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/xml@1.0.6': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + + '@lezer/yaml@1.0.4': + dependencies: + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + '@lucide/svelte@1.22.0(svelte@5.56.4)': dependencies: svelte: 5.56.4 + '@marijn/find-cluster-break@1.0.4': {} + '@mermaid-js/parser@1.0.0': dependencies: langium: 4.2.1 + '@milkdown/components@7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9)(typescript@6.0.3)': + dependencies: + '@codemirror/language': 6.12.4 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + '@floating-ui/dom': 1.7.6 + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/plugin-diff': 7.22.1 + '@milkdown/plugin-tooltip': 7.22.1 + '@milkdown/preset-commonmark': 7.22.1 + '@milkdown/preset-gfm': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + clsx: 2.1.1 + dompurify: 3.3.1 + lodash-es: 4.17.23 + nanoid: 6.0.1 + unist-util-visit: 5.1.0 + vue: 3.5.41(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + - typescript + + '@milkdown/core@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + '@milkdown/crepe@7.22.1(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2)(typescript@6.0.3)': + dependencies: + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/language-data': 6.5.2 + '@codemirror/state': 6.7.1 + '@codemirror/theme-one-dark': 6.1.3 + '@codemirror/view': 6.43.9 + '@milkdown/kit': 7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9)(typescript@6.0.3) + '@types/lodash-es': 4.17.12 + clsx: 2.1.1 + codemirror: 6.0.2 + dompurify: 3.3.1 + katex: 0.18.4 + lodash-es: 4.17.23 + prosemirror-virtual-cursor: 0.4.2(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2) + remark-math: 6.0.0 + unist-util-visit: 5.1.0 + vue: 3.5.41(typescript@6.0.3) + transitivePeerDependencies: + - prosemirror-model + - prosemirror-state + - prosemirror-view + - supports-color + - typescript + + '@milkdown/ctx@7.22.1': + dependencies: + '@milkdown/exception': 7.22.1 + + '@milkdown/exception@7.22.1': {} + + '@milkdown/kit@7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9)(typescript@6.0.3)': + dependencies: + '@milkdown/components': 7.22.1(@codemirror/language@6.12.4)(@codemirror/state@6.7.1)(@codemirror/view@6.43.9)(typescript@6.0.3) + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/plugin-block': 7.22.1 + '@milkdown/plugin-clipboard': 7.22.1 + '@milkdown/plugin-cursor': 7.22.1 + '@milkdown/plugin-diff': 7.22.1 + '@milkdown/plugin-history': 7.22.1 + '@milkdown/plugin-indent': 7.22.1 + '@milkdown/plugin-listener': 7.22.1 + '@milkdown/plugin-slash': 7.22.1 + '@milkdown/plugin-streaming': 7.22.1 + '@milkdown/plugin-tooltip': 7.22.1 + '@milkdown/plugin-trailing': 7.22.1 + '@milkdown/plugin-upload': 7.22.1 + '@milkdown/preset-commonmark': 7.22.1 + '@milkdown/preset-gfm': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - '@codemirror/language' + - '@codemirror/state' + - '@codemirror/view' + - supports-color + - typescript + + '@milkdown/plugin-block@7.22.1': + dependencies: + '@floating-ui/dom': 1.7.6 + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-clipboard@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-cursor@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + prosemirror-drop-indicator: 0.1.4 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-diff@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-history@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-indent@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-listener@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-slash@7.22.1': + dependencies: + '@floating-ui/dom': 1.7.6 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-streaming@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/plugin-diff': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-tooltip@7.22.1': + dependencies: + '@floating-ui/dom': 1.7.6 + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + '@types/lodash-es': 4.17.12 + lodash-es: 4.17.23 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-trailing@7.22.1': + dependencies: + '@milkdown/ctx': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/plugin-upload@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/utils': 7.22.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/preset-commonmark@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + remark-inline-links: 7.0.0 + unist-util-visit: 5.1.0 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + '@milkdown/preset-gfm@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/preset-commonmark': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + '@milkdown/utils': 7.22.1 + prosemirror-safari-ime-span: 1.0.2 + remark-gfm: 4.0.1 + transitivePeerDependencies: + - supports-color + + '@milkdown/prose@7.22.1': + dependencies: + '@milkdown/exception': 7.22.1 + prosemirror-changeset: 2.4.2 + prosemirror-commands: 1.7.2 + prosemirror-dropcursor: 1.8.3 + prosemirror-gapcursor: 1.4.1 + prosemirror-history: 1.5.0 + prosemirror-inputrules: 1.5.1 + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-schema-list: 1.5.1 + prosemirror-state: 1.4.4 + prosemirror-tables: 1.8.5 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + '@milkdown/transformer@7.22.1': + dependencies: + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + remark: 15.0.1 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + '@milkdown/utils@7.22.1': + dependencies: + '@milkdown/core': 7.22.1 + '@milkdown/ctx': 7.22.1 + '@milkdown/exception': 7.22.1 + '@milkdown/prose': 7.22.1 + '@milkdown/transformer': 7.22.1 + nanoid: 6.0.1 + transitivePeerDependencies: + - supports-color + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': dependencies: '@emnapi/core': 1.11.1 @@ -3755,6 +4805,8 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@ocavue/utils@1.7.0': {} + '@oxc-project/types@0.137.0': {} '@playwright/test@1.58.2': @@ -4423,6 +5475,14 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/katex@0.16.8': {} + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + + '@types/lodash@4.17.25': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -4520,6 +5580,60 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vue/compiler-core@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/shared': 3.5.41 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.41': + dependencies: + '@vue/compiler-core': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/compiler-sfc@3.5.41': + dependencies: + '@babel/parser': 7.29.8 + '@vue/compiler-core': 3.5.41 + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-ssr': 3.5.41 + '@vue/shared': 3.5.41 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.26 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.41': + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/reactivity@3.5.41': + dependencies: + '@vue/shared': 3.5.41 + + '@vue/runtime-core@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/runtime-dom@3.5.41': + dependencies: + '@vue/reactivity': 3.5.41 + '@vue/runtime-core': 3.5.41 + '@vue/shared': 3.5.41 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.41': + dependencies: + '@vue/compiler-ssr': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/shared': 3.5.41 + + '@vue/shared@3.5.41': {} + acorn-jsx@5.3.2(acorn@8.17.0): dependencies: acorn: 8.17.0 @@ -4622,6 +5736,16 @@ snapshots: clsx@2.1.1: {} + codemirror@6.0.2: + dependencies: + '@codemirror/autocomplete': 6.20.3 + '@codemirror/commands': 6.11.0 + '@codemirror/language': 6.12.4 + '@codemirror/lint': 6.9.7 + '@codemirror/search': 6.7.1 + '@codemirror/state': 6.7.1 + '@codemirror/view': 6.43.9 + comma-separated-tokens@2.0.3: {} commander@14.0.3: {} @@ -4644,6 +5768,8 @@ snapshots: dependencies: layout-base: 2.0.1 + crelt@1.0.7: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -5032,6 +6158,8 @@ snapshots: estree-util-is-identifier-name@3.0.0: {} + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 @@ -5298,6 +6426,10 @@ snapshots: dependencies: commander: 8.3.0 + katex@0.18.4: + dependencies: + commander: 8.3.0 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5409,6 +6541,12 @@ snapshots: marked@18.0.5: {} + mdast-util-definitions@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -5490,6 +6628,18 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-math@3.0.0: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + longest-streak: 3.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + unist-util-remove-position: 5.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -5664,6 +6814,16 @@ snapshots: micromark-util-combine-extensions: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-math@3.1.0: + dependencies: + '@types/katex': 0.16.8 + devlop: 1.1.0 + katex: 0.16.33 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + micromark-factory-destination@2.0.1: dependencies: micromark-util-character: 2.1.1 @@ -5797,6 +6957,10 @@ snapshots: nanoid@3.3.15: {} + nanoid@3.3.18: {} + + nanoid@6.0.1: {} + natural-compare@1.4.0: {} node-fetch-native@1.6.7: {} @@ -5830,6 +6994,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + orderedmap@2.1.1: {} + p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 @@ -5906,6 +7072,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + prelude-ls@1.2.1: {} prettier-plugin-svelte@3.5.0(prettier@3.9.4)(svelte@5.56.4): @@ -5930,6 +7102,98 @@ snapshots: property-information@7.1.0: {} + prosemirror-changeset@2.4.2: + dependencies: + prosemirror-transform: 1.12.0 + + prosemirror-commands@1.7.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-drop-indicator@0.1.4: + dependencies: + '@ocavue/utils': 1.7.0 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + + prosemirror-dropcursor@1.8.3: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-gapcursor@1.4.1: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + + prosemirror-history@1.5.0: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.5.1: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-keymap@1.2.3: + dependencies: + prosemirror-state: 1.4.4 + w3c-keyname: 2.2.8 + + prosemirror-model@1.25.11: + dependencies: + orderedmap: 2.1.1 + + prosemirror-safari-ime-span@1.0.2: + dependencies: + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + + prosemirror-schema-list@1.5.1: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-state@1.4.4: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-tables@1.8.5: + dependencies: + prosemirror-keymap: 1.2.3 + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + prosemirror-view: 1.42.2 + + prosemirror-transform@1.12.0: + dependencies: + prosemirror-model: 1.25.11 + + prosemirror-view@1.42.2: + dependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-transform: 1.12.0 + + prosemirror-virtual-cursor@0.4.2(prosemirror-model@1.25.11)(prosemirror-state@1.4.4)(prosemirror-view@1.42.2): + optionalDependencies: + prosemirror-model: 1.25.11 + prosemirror-state: 1.4.4 + prosemirror-view: 1.42.2 + punycode@2.3.1: {} react-dom@19.2.5(react@19.2.5): @@ -6026,6 +7290,21 @@ snapshots: transitivePeerDependencies: - supports-color + remark-inline-links@7.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-definitions: 6.0.0 + unist-util-visit: 5.1.0 + + remark-math@6.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-math: 3.0.0 + micromark-extension-math: 3.1.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -6049,6 +7328,15 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + require-from-string@2.0.2: {} robust-predicates@3.0.2: {} @@ -6105,6 +7393,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + roughjs@4.6.6: dependencies: hachure-fill: 0.5.2 @@ -6207,6 +7497,8 @@ snapshots: dependencies: min-indent: 1.0.1 + style-mod@4.1.3: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -6363,6 +7655,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-remove-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit: 5.1.0 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -6515,6 +7812,18 @@ snapshots: vscode-uri@3.1.0: {} + vue@3.5.41(typescript@6.0.3): + dependencies: + '@vue/compiler-dom': 3.5.41 + '@vue/compiler-sfc': 3.5.41 + '@vue/runtime-dom': 3.5.41 + '@vue/server-renderer': 3.5.41 + '@vue/shared': 3.5.41 + optionalDependencies: + typescript: 6.0.3 + + w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: dependencies: xml-name-validator: 5.0.0 From f8acfb8384d5ea591b06560aab72ade9f739656d Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 26 Aug 2026 14:40:13 +1000 Subject: [PATCH 02/14] fix(staged): restyle footer action buttons off the dashed look MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bottom-left Note/Commit/Review buttons drop the dashed border-subtle outline for a solid border in each button's own theme color (--note-color/--commit-color/--review-color), so the resting state already carries the color the hover state fills in. The now-redundant hover border-color override goes away with it. The `…` overflow trigger read as disabled with its dashed border and faint text; it now uses the standard outline button variant at icon-sm (buttonVariants), matching the right-hand Diff/PR buttons, with the open state handled by the variant's aria-expanded styling. Signed-off-by: Matt Toohey --- .../src/lib/features/timeline/BranchTimeline.svelte | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index e58fe8324..82ad3e55a 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -29,7 +29,7 @@ import TimelineContextMenu, { type TimelineContextMenuAction, } from './TimelineContextMenu.svelte'; - import { Button } from '$lib/components/ui/button'; + import { Button, buttonVariants } from '$lib/components/ui/button'; import * as DropdownMenu from '$lib/components/ui/dropdown-menu'; import type { TimelineItemType, TimelineBadge } from './TimelineRow.svelte'; import { computeFooterOverflow, overflowedActions } from './footerOverflow'; @@ -1262,7 +1262,7 @@ '[&_svg]:transition-colors [&_svg]:duration-300', actionButtonsEnlarged ? 'flex-1 justify-center gap-2 px-1.5 py-2.5 h-auto rounded-lg border border-solid border-transparent bg-[var(--bg-elevated)] text-sm hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[18px] [&_svg]:text-[var(--note-color)]' - : 'gap-[5px] px-2.5 h-8 rounded-md border border-dashed border-[var(--border-subtle)] bg-transparent text-xs hover:not-disabled:border-[var(--note-color)] hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[13px] [&_svg]:text-[var(--note-color)] @max-[480px]/timeline:gap-0.5 @max-[480px]/timeline:px-1.5', + : 'gap-[5px] px-2.5 h-8 rounded-md border border-solid border-[var(--note-color)] bg-transparent text-xs hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[13px] [&_svg]:text-[var(--note-color)] @max-[480px]/timeline:gap-0.5 @max-[480px]/timeline:px-1.5', ]} > From fd6770396431e6ee36bc0cf9abe3bfbff9728ea9 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Wed, 26 Aug 2026 15:08:27 +1000 Subject: [PATCH 03/14] fix(staged): fill footer action buttons with their tint instead of outlining MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The solid theme-colored border from the previous commit read too loud at rest. The Note/Commit/Review buttons now carry no visible outline at all and instead sit on the faint 8%-alpha tint of their own icon color (--note-bg/--commit-bg/--review-bg), with hover stepping up to the matching 15% --*-bg-emphasis token. No explicit border class is needed to hold the 32px box: the shared buttonVariants base already applies `border border-transparent`, so removing the color leaves the metrics unchanged. The `…` overflow trigger keeps the neutral outline variant, since it is a generic affordance matched to the right-hand Diff/PR buttons rather than to a session type. Signed-off-by: Matt Toohey --- apps/staged/src/lib/features/timeline/BranchTimeline.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index 82ad3e55a..265506dca 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -1262,7 +1262,7 @@ '[&_svg]:transition-colors [&_svg]:duration-300', actionButtonsEnlarged ? 'flex-1 justify-center gap-2 px-1.5 py-2.5 h-auto rounded-lg border border-solid border-transparent bg-[var(--bg-elevated)] text-sm hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[18px] [&_svg]:text-[var(--note-color)]' - : 'gap-[5px] px-2.5 h-8 rounded-md border border-solid border-[var(--note-color)] bg-transparent text-xs hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[13px] [&_svg]:text-[var(--note-color)] @max-[480px]/timeline:gap-0.5 @max-[480px]/timeline:px-1.5', + : 'gap-[5px] px-2.5 h-8 rounded-md bg-[var(--note-bg)] text-xs hover:not-disabled:bg-[var(--note-bg-emphasis)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[13px] [&_svg]:text-[var(--note-color)] @max-[480px]/timeline:gap-0.5 @max-[480px]/timeline:px-1.5', ]} > Date: Wed, 26 Aug 2026 15:20:21 +1000 Subject: [PATCH 04/14] fix(staged): restore original dashed styling on footer session buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Revert the Note/Commit/Review footer buttons to their pre-branch look: dashed border-subtle outline at rest, with hover swapping in the button's own theme border color, background tint, and text color. The two restyles on this branch (solid theme-colored border, then borderless 8%-alpha tint fill) are undone. The `…` overflow trigger keeps its buttonVariants outline icon-sm styling, which matches the right-hand Diff/PR buttons. Signed-off-by: Matt Toohey --- apps/staged/src/lib/features/timeline/BranchTimeline.svelte | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte index 265506dca..5d78d3c39 100644 --- a/apps/staged/src/lib/features/timeline/BranchTimeline.svelte +++ b/apps/staged/src/lib/features/timeline/BranchTimeline.svelte @@ -1262,7 +1262,7 @@ '[&_svg]:transition-colors [&_svg]:duration-300', actionButtonsEnlarged ? 'flex-1 justify-center gap-2 px-1.5 py-2.5 h-auto rounded-lg border border-solid border-transparent bg-[var(--bg-elevated)] text-sm hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[18px] [&_svg]:text-[var(--note-color)]' - : 'gap-[5px] px-2.5 h-8 rounded-md bg-[var(--note-bg)] text-xs hover:not-disabled:bg-[var(--note-bg-emphasis)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[13px] [&_svg]:text-[var(--note-color)] @max-[480px]/timeline:gap-0.5 @max-[480px]/timeline:px-1.5', + : 'gap-[5px] px-2.5 h-8 rounded-md border border-dashed border-[var(--border-subtle)] bg-transparent text-xs hover:not-disabled:border-[var(--note-color)] hover:not-disabled:bg-[var(--note-bg)] hover:not-disabled:text-[var(--note-color)] [&_svg]:!size-[13px] [&_svg]:text-[var(--note-color)] @max-[480px]/timeline:gap-0.5 @max-[480px]/timeline:px-1.5', ]} > Date: Wed, 26 Aug 2026 16:14:23 +1000 Subject: [PATCH 05/14] fix(staged): strip Crepe's chrome from the written-note editor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The complaint about the "Write note" editor was Crepe's editor UI, not WYSIWYG editing itself. Those are separate layers: the live formatting — typing `# ` or `**bold**` and watching it take effect — comes from Milkdown's commonmark/GFM presets, which load unconditionally, while each widget is an independently disableable feature flag. So the flags go off: BlockEdit (per-block hover `+`/drag handle and the `/` menu), Toolbar (floating format bar on selection), ImageBlock, Table (cell handles and row/column buttons), and CodeMirror, whose language picker also drags in a hardcoded one-dark theme that ignores our tokens and clashes in light mode. Bold/italic stay reachable through markdown syntax and the preset keymaps, which are core rather than part of any feature. Left on are the quiet ones: ListItem for `- [ ]` checkboxes, LinkTooltip (the only way to edit an href without retyping the markdown), Placeholder, and Cursor. Disabled features take their node views with them, so tables and code blocks now render through the plain schema and need styling that Crepe's widget CSS used to provide. That folds into a typography pass: Crepe sizes its document for a standalone page editor (16px base, 2.6em h1, 60px/120px page margins), and the overrides here resize headings, paragraphs, lists, code, quotes, rules and tables to match NoteModal's `.markdown-content`, so writing a note looks close to reading one. The ListItem marker box in particular is hardcoded to 32px, which at our base font drops the bullet below its own first line. Crepe's `style.css` is still imported whole: every disabled feature's rules are scoped to class names that are now never emitted, so they are inert, and one import stays robust across upgrades. `--crepe-color-inline-code` is remapped from `--ui-danger` to `--text-primary`, matching NoteModal, which leaves inline code in body colour. This is the cheap, reversible half of the plan. If the calm surface still reads wrong, the problem is deeper than chrome and the next step is dropping Crepe for Milkdown core. Signed-off-by: Matt Toohey --- .../notes/MarkdownWysiwygEditor.svelte | 179 +++++++++++++++++- 1 file changed, 175 insertions(+), 4 deletions(-) diff --git a/apps/staged/src/lib/features/notes/MarkdownWysiwygEditor.svelte b/apps/staged/src/lib/features/notes/MarkdownWysiwygEditor.svelte index 757cc72ba..dffde9993 100644 --- a/apps/staged/src/lib/features/notes/MarkdownWysiwygEditor.svelte +++ b/apps/staged/src/lib/features/notes/MarkdownWysiwygEditor.svelte @@ -6,9 +6,17 @@ swappable without touching callers. Crepe and its theme are loaded lazily: they pull in ProseMirror and CodeMirror, which no other view needs. + Crepe ships two separable layers: the live markdown formatting (typing `# ` + or `**bold**` and watching it take effect), which comes from Milkdown's + commonmark/GFM presets and always loads, and a set of optional widgets — + hover handles, slash menu, selection toolbar, table and image chrome. We + want the first and not the second, so most feature flags are off below and + the surface is left as a quiet page. + Crepe's structural CSS is imported as-is; every colour it reads comes from `--crepe-*` variables mapped to our theme tokens below (per AGENTS.md, no - hardcoded colours). + hardcoded colours), and its document typography is resized to match how + NoteModal renders the same markdown. -->