diff --git a/ROADMAP.md b/ROADMAP.md index 1189fd7..349f50c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~25 points of "ready for real work" and ~160–240 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (full re-measure against Excel for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Review comment navigation and notes commands, catalog 263 / 290, #281) · **Target:** Microsoft Excel (Microsoft 365) GridCraft aims at full Microsoft Excel parity, and at being better in places: faster, open (XLSX is the native format), fully scriptable by agents (MCP, control channel, CLI), and on the web. @@ -21,7 +21,7 @@ This page is the summary. The detail is in: | Number | Value | Kind | |---|---|---| -| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 258 / 290 (89.0%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) | +| **Feature breadth** (weighted) | **~75%** | estimated, anchored on two measurements: ribbon catalog 263 / 290 (90.7%), worksheet functions 501 / 523 (95.8%; 492 working, 94.1%) | | **Ready for real work** | **~50%** | estimated from written weights (51.6); [target-app-parity.md](docs/target-app-parity.md) | | **Mainstream practitioner** | **~50%** | estimated: typical analyst/finance user, weekly features, × 0.90 interaction × 0.92 stability × 0.88 file exchange; [method](docs/target-app-parity.md#mainstream-practitioner) | | **Essentials user** | **~63%** | estimated: core features only, × 0.92 launch/stability × 0.95 clarity × 0.90 opening sent files; [method](docs/target-app-parity.md#essentials-user) | @@ -131,6 +131,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | What landed | |---|---| +| 2026-10-11 | Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments (#281) | | 2026-10-10 | Ribbon translated into Japanese, Chinese, Korean, Brazilian Portuguese and Russian with system-language default (#31, #104, #82); CJK/Thai/Hebrew font fallback (#38); clear messages for encrypted and `.xls` files (#17); smooth line charts (#62). Progress docs restructured to the craftrules standard; full re-measure (this page). Initial ribbon key tips (#43), in-place text box editing (#84), copy as HTML table (#36). ODS and XLSB data import (#100, #101); recalc linear in the number of range formulas (#66); RISC-V Linux build; Flatpak repository on Pages; web cell-commit trap fixed | | 2026-10-09 | Community fixes: spill range operator `A1#`, sheet-scoped names, protection refuses locked-cell edits, Insert/Delete Cells adjust references, `DATE(1900,2,29)`, SUMIF sizing, `ROUND` at 15 digits, `_xlpm.` LAMBDA parameters in XLSX, vertical scrollbar, system appearance, border previews; v0.4.0 | | 2026-10-08 | v0.2.0 and v0.3.0 releases; Flatpak, AppImage zsync, branded DMG; desktop log file; DirectX 12 default on Windows | @@ -142,6 +143,7 @@ Ranked; estimates in Opus 5.5 agent-hours. Detail and the full beta-gate list in | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Catalog 258 → 263 / 290: Review comment navigation, Show Comments, Show All Notes, Convert to Comments (#281) | | 2026-10-10 | minor | Readiness-by-audience table with hours to ~95% per audience; full number unchanged (additive weighted sum) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%); languages updated for the 10-10 merges (ribbon in ja, zh, ko, pt-BR, ru); localization 5%→10%; full number re-checked (51.6, unchanged) | | 2026-10-10 | minor | Checked against the alpha gate (six core workflows pass): stays alpha | diff --git a/crates/engine/src/cmd/review.rs b/crates/engine/src/cmd/review.rs index 1613e04..4a2314f 100644 --- a/crates/engine/src/cmd/review.rs +++ b/crates/engine/src/cmd/review.rs @@ -1,6 +1,8 @@ -//! Review tab: protection, spelling (basic), workbook statistics. +//! Review tab: protection, spelling (basic), workbook statistics, comment navigation and the +//! Comments pane, Show All Notes and Convert to Comments. -use gridcraft_model::{SheetProtection, password_hash}; +use gridcraft_core::CellRef; +use gridcraft_model::{SheetProtection, Visibility, password_hash}; use serde_json::{Value as Json, json}; use super::*; @@ -20,9 +22,94 @@ pub fn specs() -> Vec { cmd!("review.protectWorkbook", "Protect Workbook", ["Review", "Protect"], None, "{on?: bool}", has_doc, protect_workbook), cmd!(query "review.workbookStatistics", "Workbook Statistics", ["Review", "Proofing"], None, "{}", has_doc, stats), cmd!(query "review.checkAccessibility", "Check Accessibility", ["Review", "Accessibility"], None, "{} → issues", has_doc, accessibility), + cmd!(noundo "review.previousComment", "Previous Comment", ["Review", "Comments"], None, "{} → {sheet, cell}", has_comments, |s, _| { + step_comment(s, false) + }), + cmd!(noundo "review.nextComment", "Next Comment", ["Review", "Comments"], None, "{} → {sheet, cell}", has_comments, |s, _| step_comment( + s, true + )), + cmd!(query "review.showComments", "Show Comments", ["Review", "Comments"], None, "{on?: bool} (toggles the Comments pane)", has_doc, |s, p| { + s.comments_pane = bool_param(p, "on").unwrap_or(!s.comments_pane); + Ok(json!({"on": s.comments_pane})) + }), + cmd!(query "review.showAllNotes", "Show All Notes", ["Review", "Notes"], None, "{on?: bool}", has_doc, |s, p| { + s.show_all_notes = bool_param(p, "on").unwrap_or(!s.show_all_notes); + Ok(json!({"on": s.show_all_notes})) + }), + cmd!( + "review.convertToComments", + "Convert to Comments", + ["Review", "Notes"], + None, + "{} (every note in the workbook) → {converted}", + has_doc, + convert_notes + ), ] } +/// Threaded comments in navigation order: visible sheets in tab order, then row by row. +fn comment_cells(s: &Session) -> Vec<(usize, CellRef)> { + let Some(d) = s.active() else { return vec![] }; + d.wb.sheets + .iter() + .enumerate() + .filter(|(_, sh)| sh.visibility == Visibility::Visible) + // `CellRef` orders by row, then column: reading order. + .flat_map(|(i, sh)| sh.comments.iter().filter(|(_, c)| c.threaded).map(move |(at, _)| (i, *at))) + .collect() +} + +fn has_comments(s: &Session) -> std::result::Result<(), String> { + has_doc(s)?; + if comment_cells(s).is_empty() { Err("there are no comments in this workbook".into()) } else { Ok(()) } +} + +/// Selects the next (or previous) threaded comment after the active cell, moving on to the next +/// sheet and wrapping around at the end of the workbook. +fn step_comment(s: &mut Session, forward: bool) -> Result { + let list = comment_cells(s); + let d = s.doc()?; + let here = (d.wb.active_sheet, d.selection.active); + let target = if forward { + list.iter().find(|c| **c > here).or_else(|| list.first()) + } else { + list.iter().rev().find(|c| **c < here).or_else(|| list.last()) + }; + let Some(&(sheet, at)) = target else { + return Err(EngineError::Other("There are no comments in this workbook.".into())); + }; + let d = s.doc_mut()?; + d.wb_switch_sheet(sheet); + d.selection = crate::Selection::at(at); + if let Some(sh) = d.wb.active() { + d.selection.expand_merges(sh); + } + let name = d.wb.active().map(|sh| sh.name.clone()).unwrap_or_default(); + Ok(json!({"sheet": name, "cell": at.a1()})) +} + +/// Review › Notes › Convert to Comments: every note in the workbook becomes a threaded comment +/// with the same author and text. +fn convert_notes(s: &mut Session, _: &Json) -> Result { + let sheets: Vec = + s.doc()?.wb.sheets.iter().enumerate().filter(|(_, sh)| sh.comments.values().any(|c| !c.threaded)).map(|(i, _)| i).collect(); + if sheets.is_empty() { + return Ok(json!({"converted": 0})); + } + edit(s, |cx| { + let mut n = 0usize; + for i in &sheets { + for c in cx.sheet_mut(*i)?.comments.values_mut().filter(|c| !c.threaded) { + c.threaded = true; + c.visible = false; + n += 1; + } + } + Ok(json!({"converted": n})) + }) +} + fn protect_sheet(s: &mut Session, p: &Json) -> Result { let sheet = target_sheet(s, p)?; let b = |k: &str| bool_param(p, k).unwrap_or(false); @@ -97,3 +184,81 @@ fn accessibility(s: &mut Session, _: &Json) -> Result { } Ok(Json::Array(issues)) } + +#[cfg(test)] +mod tests { + use super::*; + + fn book() -> Session { + let mut s = Session::new(); + s.new_workbook(); + s.execute("home.insertSheet", json!({"name": "Two"})).unwrap(); + s.execute("sheet.activate", json!({"sheet": 0})).unwrap(); + for (sheet, cell) in [(0, "C1"), (0, "A3"), (1, "B2")] { + s.execute("review.newComment", json!({"sheet": sheet, "cell": cell, "text": "c", "author": "Ann"})).unwrap(); + } + s.execute("review.newNote", json!({"sheet": 0, "cell": "A2", "text": "note", "author": "Bob"})).unwrap(); + s + } + + fn at(s: &Session) -> (usize, String) { + let d = s.doc().unwrap(); + (d.wb.active_sheet, d.selection.active.a1()) + } + + #[test] + fn next_and_previous_comment_go_in_reading_order_across_sheets_and_wrap() { + let mut s = book(); + s.execute("selection.set", json!({"cell": "B1"})).unwrap(); + // Notes are skipped: A2 holds a note, not a comment. + let order = [(0, "C1"), (0, "A3"), (1, "B2"), (0, "C1")]; + for (sheet, cell) in order { + s.execute("review.nextComment", json!({})).unwrap(); + assert_eq!(at(&s), (sheet, cell.to_string())); + } + for (sheet, cell) in [(1, "B2"), (0, "A3"), (0, "C1")] { + s.execute("review.previousComment", json!({})).unwrap(); + assert_eq!(at(&s), (sheet, cell.to_string())); + } + } + + #[test] + fn comment_navigation_is_disabled_without_comments() { + let mut s = Session::new(); + s.new_workbook(); + s.execute("review.newNote", json!({"cell": "A1", "text": "note"})).unwrap(); + assert!(s.execute("review.nextComment", json!({})).is_err()); + assert!(s.execute("review.previousComment", json!({})).is_err()); + assert_eq!(at(&s), (0, "A1".to_string())); + } + + #[test] + fn convert_to_comments_turns_every_note_into_a_comment_and_undoes() { + let mut s = book(); + s.execute("review.newNote", json!({"sheet": 1, "cell": "D4", "text": "second", "author": "Cy"})).unwrap(); + s.execute("review.showNote", json!({"sheet": 1, "cell": "D4"})).unwrap(); + let r = s.execute("review.convertToComments", json!({})).unwrap(); + assert_eq!(r["converted"], 2); + let wb = &s.doc().unwrap().wb; + let a2 = &wb.sheets[0].comments[&CellRef::parse("A2").unwrap()]; + let d4 = &wb.sheets[1].comments[&CellRef::parse("D4").unwrap()]; + assert!(a2.threaded && d4.threaded && !d4.visible); + assert_eq!((a2.author.as_str(), a2.text.as_str(), d4.author.as_str(), d4.text.as_str()), ("Bob", "note", "Cy", "second")); + // Nothing left to convert; one undo restores the notes. + assert_eq!(s.execute("review.convertToComments", json!({})).unwrap()["converted"], 0); + s.execute("edit.undo", json!({})).unwrap(); + assert!(!s.doc().unwrap().wb.sheets[0].comments[&CellRef::parse("A2").unwrap()].threaded); + } + + #[test] + fn show_comments_and_show_all_notes_toggle() { + let mut s = book(); + assert_eq!(s.execute("review.showComments", json!({})).unwrap()["on"], true); + assert_eq!(s.execute("review.showComments", json!({})).unwrap()["on"], false); + assert_eq!(s.execute("review.showComments", json!({"on": true})).unwrap()["on"], true); + assert!(s.comments_pane); + assert_eq!(s.execute("review.showAllNotes", json!({})).unwrap()["on"], true); + assert_eq!(s.execute("review.showAllNotes", json!({"on": false})).unwrap()["on"], false); + assert!(!s.show_all_notes); + } +} diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index de786a1..fa9fc2a 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -200,6 +200,11 @@ pub struct Session { pub draw_tool: String, pub draw_color: String, pub draw_width: f32, + /// Review › Show Comments: the Comments pane is open. + pub comments_pane: bool, + /// Review › Notes › Show All Notes: every note is displayed. An application display setting + /// (like Excel's comment indicator option), so it's not saved in the file. + pub show_all_notes: bool, } impl Session { diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 7dd3acb..b482825 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -571,7 +571,8 @@ impl Dialog { "saveChanges" => Dialog::custom("saveChanges", "Save Changes?", json!({"title": p("title")})), "start" => Dialog::custom("start", "GridCraft", json!({})), "comments" => { - app.grid.pane = Some("comments".into()); + app.grid.pane = None; + app.run_or_alert("review.showComments", json!({"on": true})); return None; } "watch" | "selectionPane" | "formatChart" => { diff --git a/crates/ui-egui/src/grid.rs b/crates/ui-egui/src/grid.rs index 72d33e2..a10fc97 100644 --- a/crates/ui-egui/src/grid.rs +++ b/crates/ui-egui/src/grid.rs @@ -2001,8 +2001,9 @@ fn paint_overlays( } // Notes: shown on hover (or always when marked visible). let hover = app.grid.hover_cell; + let all_notes = app.session.show_all_notes; for (c, cm) in &sh.comments { - let show = cm.visible || hover == Some(*c); + let show = cm.visible || (all_notes && !cm.threaded) || hover == Some(*c); if !show || app.grid.drag != Drag::None { continue; } diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 1e7b212..e5d0c2c 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -321,6 +321,12 @@ const JAPANESE: &[(&str, &str)] = &[ ("Check\nAccessibility", "アクセシビリティ チェック"), ("Threaded Comments", "スレッド化されたコメント"), ("Show\nComments", "コメントの表示"), + ("Previous", "前へ"), + ("Next", "次へ"), + ("Previous Comment", "前のコメント"), + ("Next Comment", "次のコメント"), + ("Show All Notes", "すべてのメモを表示"), + ("Convert to Comments", "コメントに変換"), ("Protect\nWorkbook", "ブックの保護"), ("Unprotect\nSheet", "シート保護の解除"), ("Protect Sheet…", "シートの保護…"), diff --git a/crates/ui-egui/src/i18n/ko.rs b/crates/ui-egui/src/i18n/ko.rs index c37ae3c..a9f532d 100644 --- a/crates/ui-egui/src/i18n/ko.rs +++ b/crates/ui-egui/src/i18n/ko.rs @@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Check\nAccessibility" => "접근성\n검사", "Threaded Comments" => "스레드 댓글", "Show\nComments" => "댓글\n표시", + "Previous" => "이전", + "Next" => "다음", + "Previous Comment" => "이전 댓글", + "Next Comment" => "다음 댓글", + "Show All Notes" => "모든 메모 표시", + "Convert to Comments" => "댓글로 변환", "Protect\nWorkbook" => "통합 문서\n보호", "Unprotect\nSheet" => "시트 보호\n해제", "Protect Sheet…" => "시트 보호…", diff --git a/crates/ui-egui/src/i18n/pt.rs b/crates/ui-egui/src/i18n/pt.rs index 1bdc2c3..8add648 100644 --- a/crates/ui-egui/src/i18n/pt.rs +++ b/crates/ui-egui/src/i18n/pt.rs @@ -173,6 +173,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Check\nAccessibility" => "Verificar\nAcessibilidade", "Threaded Comments" => "Comentários Encadeados", "Show\nComments" => "Mostrar\nComentários", + "Previous" => "Anterior", + "Next" => "Próximo", + "Previous Comment" => "Comentário Anterior", + "Next Comment" => "Próximo Comentário", + "Show All Notes" => "Mostrar Todas as Notas", + "Convert to Comments" => "Converter em Comentários", "Protect\nWorkbook" => "Proteger\nPasta de Trabalho", "Unprotect\nSheet" => "Desproteger\nPlanilha", "Protect Sheet…" => "Proteger Planilha…", diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 8d54856..03050d5 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Check\nAccessibility" => "Проверка\nдоступности", "Threaded Comments" => "Комментарии с ветками", "Show\nComments" => "Показать\nкомментарии", + "Previous" => "Назад", + "Next" => "Далее", + "Previous Comment" => "Предыдущий комментарий", + "Next Comment" => "Следующий комментарий", + "Show All Notes" => "Показать все заметки", + "Convert to Comments" => "Преобразовать в комментарии", "Protect\nWorkbook" => "Защитить\nкнигу", "Unprotect\nSheet" => "Снять защиту\nлиста", "Protect Sheet…" => "Защитить лист…", diff --git a/crates/ui-egui/src/i18n/zh.rs b/crates/ui-egui/src/i18n/zh.rs index 9f04ada..0849866 100644 --- a/crates/ui-egui/src/i18n/zh.rs +++ b/crates/ui-egui/src/i18n/zh.rs @@ -172,6 +172,12 @@ pub(super) fn translate(text: &str) -> Option<&'static str> { "Check\nAccessibility" => "检查\n辅助功能", "Threaded Comments" => "线程式批注", "Show\nComments" => "显示\n批注", + "Previous" => "上一条", + "Next" => "下一条", + "Previous Comment" => "上一条批注", + "Next Comment" => "下一条批注", + "Show All Notes" => "显示所有备注", + "Convert to Comments" => "转换为批注", "Protect\nWorkbook" => "保护\n工作簿", "Unprotect\nSheet" => "撤销\n工作表保护", "Protect Sheet…" => "保护工作表…", diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index fb8a9d7..08b1d4d 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -7,7 +7,15 @@ use crate::SheetApp; use crate::theme::{self, Tokens}; pub fn show(app: &mut SheetApp, ui: &mut Ui) { - let Some(pane) = app.grid.pane.clone() else { return }; + // The Comments pane is engine state (`review.showComments`, so agents can drive it); the other + // panes are UI state. One pane shows at a time: opening another one closes Comments. + if app.grid.pane.as_deref() == Some("comments") { + app.grid.pane = None; + app.run_or_alert("review.showComments", json!({"on": true})); + } else if app.grid.pane.is_some() && app.session.comments_pane { + app.run_or_alert("review.showComments", json!({"on": false})); + } + let Some(pane) = app.grid.pane.clone().or_else(|| app.session.comments_pane.then(|| "comments".to_string())) else { return }; let t = Tokens::get(ui.ctx()); egui::Panel::right("task_pane").default_size(300.0).resizable(true).frame(egui::Frame::NONE.fill(t.ribbon).inner_margin(10)).show(ui, |ui| { ui.horizontal(|ui| { @@ -21,6 +29,9 @@ pub fn show(app: &mut SheetApp, ui: &mut Ui) { ui.label(egui::RichText::new(title).font(theme::ui_bold(15.0))); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if crate::widgets::icon_button(ui, crate::icons::Icon::Close, t.text_dim, "Close", vec2(22.0, 22.0)).clicked() { + if pane == "comments" { + app.run_or_alert("review.showComments", json!({"on": false})); + } app.grid.pane = None; } }); diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index 264479a..395f37b 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1596,15 +1596,43 @@ fn review(app: &mut SheetApp, ui: &mut Ui) { if big_button(ui, Icon::Comment, "New\nComment", "New Comment", false).clicked() { app.open_dialog("comment", json!({"threaded": true})); } - if big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false).clicked() { - app.grid.pane = Some("comments".into()); - } if big_button(ui, Icon::Delete, "Delete", "Delete Comment", false).clicked() { act(app, "review.deleteComment", json!({})); } + ui.vertical(|ui| { + for (icon, label, tip, id) in + [(Icon::Left, "Previous", "Previous Comment", "review.previousComment"), (Icon::Right, "Next", "Next Comment", "review.nextComment")] + { + if small_button(ui, icon, label, tip, false).clicked() { + act(app, id, json!({})); + app.grid.ensure_visible = true; + } + } + }); + let shown = app.session.comments_pane; + let r = big_button(ui, Icon::Comment, "Show\nComments", "Comments pane", false); + if shown { + ui.painter().rect_stroke(r.rect, 5.0, Stroke::new(2.0, Tokens::get(ui.ctx()).accent), StrokeKind::Inside); + } + if r.clicked() { + act(app, "review.showComments", json!({"on": !shown})); + if app.session.comments_pane { + app.grid.pane = None; + } + } let notes = big_button(ui, Icon::Note, "Notes", "Notes", true); egui::Popup::menu(¬es).show(|ui| { - menu_items(app, ui, &[("New Note", "dialog:comment", json!({"threaded": false})), ("Show/Hide Note", "review.showNote", json!({}))]); + menu_items( + app, + ui, + &[ + ("New Note", "dialog:comment", json!({"threaded": false})), + ("Show/Hide Note", "review.showNote", json!({})), + ("Show All Notes", "review.showAllNotes", json!({})), + ("-", "", json!({})), + ("Convert to Comments", "review.convertToComments", json!({})), + ], + ); }); sep(ui); let protected = app.session.active().and_then(|d| d.wb.active().map(|s| s.is_protected())).unwrap_or(false); diff --git a/docs/gaps.md b/docs/gaps.md index aa752b8..6c87d3f 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (created from the full re-measure against Excel for Mac 16.113.4 and the issue tracker) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (comment navigation, Show All Notes and Convert to Comments landed, #281) · **Target:** Microsoft Excel (Microsoft 365) Every known shortfall against Excel, one entry each, ranked by user impact (what blocks someone using GridCraft for real work first). This is the work list: pick from the top, check the open @@ -110,8 +110,8 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- [function-parity.md](function-parity.md) ### 16. Collaboration and review (F) -- No co-authoring, Show Changes, Allow Edit Ranges, comment navigation, notes ↔ comments - conversion; cloud storage requested (#86). **Estimate:** 35–55 h (co-authoring scope needs the owner). +- No co-authoring, Show Changes, Allow Edit Ranges; threaded comments are saved to XLSX as notes + (no `threadedComments` part yet); cloud storage requested (#86). **Estimate:** 35–55 h (co-authoring scope needs the owner). ### 17. Insert objects (F) - SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot; 8 shape kinds vs @@ -149,6 +149,7 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha- | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | §16: comment navigation, Show Comments, Show All Notes and Convert to Comments landed (#281) | | 2026-10-10 | minor | Updated for the 10-10 merges (#17, #31, #38, #62, #82, #104, #116) and the new chart resize bug (#190) | | 2026-10-10 | minor | Noted that no gap blocks the alpha gate | | 2026-10-10 | major | Created: 24 ranked gaps with evidence, from the full re-measure and the 60 open issues | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index e98835d..0ea9567 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -4,7 +4,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as implemented when GridCraft registers a command with the same id. This measures breadth only; the overall assessment (depth, fidelity, effort) is in [target-app-parity.md](target-app-parity.md). -**Overall: 258 / 290 commands implemented (89.0%).** +**Overall: 263 / 290 commands implemented (90.7%).** | Tab | Implemented | Total | Parity | |---|---|---|---| @@ -15,7 +15,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as | Page Layout | 23 | 23 | 100% | | Formulas | 23 | 23 | 100% | | Data | 20 | 28 | 71% | -| Review | 9 | 18 | 50% | +| Review | 14 | 18 | 78% | | View | 18 | 20 | 90% | | Automate | 3 | 3 | 100% | | Table Design | 12 | 13 | 92% | @@ -276,7 +276,7 @@ Missing: - `data.solver` Solver (Analysis) - `data.analysisToolPak` Data Analysis (Analysis) -## Review (9 / 18, 50%) +## Review (14 / 18, 78%) Implemented: @@ -285,8 +285,13 @@ Implemented: - `review.workbookStatistics` Workbook Statistics (Proofing) - `review.checkAccessibility` Check Accessibility (Accessibility) - `review.deleteComment` Delete Comment (Comments) +- `review.previousComment` Previous Comment (Comments) +- `review.nextComment` Next Comment (Comments) +- `review.showComments` Show Comments (Comments) - `review.newNote` New Note (Notes) - `review.showNote` Show/Hide Note (Notes) +- `review.showAllNotes` Show All Notes (Notes) +- `review.convertToComments` Convert to Comments (Notes) - `review.unprotectSheet` Unprotect Sheet (Protect) - `review.protectWorkbook` Protect Workbook (Protect) @@ -294,11 +299,6 @@ Missing: - `review.smartLookup` Smart Lookup (Insights) - `review.translate` Translate (Language) -- `review.previousComment` Previous Comment (Comments) -- `review.nextComment` Next Comment (Comments) -- `review.showComments` Show Comments (Comments) -- `review.showAllNotes` Show All Notes (Notes) -- `review.convertToComments` Convert to Comments (Notes) - `review.allowEditRanges` Allow Edit Ranges (Protect) - `review.showChanges` Show Changes (Changes) diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2e5ed01..fb8151f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Target-app parity: GridCraft vs Microsoft Excel -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** major (first full assessment; re-measure against Excel for Mac 16.113.4, replacing the estimates that lived in ROADMAP.md) · **Target:** Microsoft Excel (Microsoft 365) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (Review comment navigation and notes commands, catalog 263 / 290, #281) · **Target:** Microsoft Excel (Microsoft 365) This is the authoritative parity assessment. [`ROADMAP.md`](../ROADMAP.md) summarizes it, [`gaps.md`](gaps.md) lists every shortfall, and the area checklists hold the detail: @@ -13,7 +13,7 @@ generated ribbon checklist is [`parity-checklist.md`](parity-checklist.md). | Number | Value | Kind | |---|---|---| | Feature breadth (weighted by use) | **~75%** | estimated from the area table below; anchored on two measurements | -| Ribbon and menu catalog | **258 / 290 (89.0%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) | +| Ribbon and menu catalog | **263 / 290 (90.7%)** | measured: `cargo xtask parity` (`crates/engine/src/catalog.rs` ∩ registered command ids) | | Worksheet functions, by name | **501 / 523 (95.8%)** | measured: registered functions vs Microsoft's published function list (method below) | | Worksheet functions that do real work | **492 / 523 (94.1%)** | measured: excludes 9 stubs (7 CUBE functions, WEBSERVICE, FILTERXML) | | **Ready for real work** | **~50%** | estimated; written weights below (51.6 computed) | @@ -90,7 +90,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2 | What-if and analysis | 4 | ~50% | ~40% | 22–35 | Goal Seek, Scenario Manager, data tables. Missing: Solver, Analysis ToolPak, Forecast Sheet, Analyze Data | | Get & Transform, external data, data model | 5 | ~15% | ~10% | 60–100 | Import of CSV/TSV, JSON, HTML. No Power Query editor, no From Web, database or folder sources, no connections or refresh, no relationships or data model, no external workbook links (dropped with a warning) | | Insert objects | 3 | ~55% | ~45% | 25–40 | Pictures, 8 shape kinds (Excel has ~170), text boxes, own icon set, ink with Ink to Shape, hyperlinks, checkboxes. Missing: SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot, pictures in cells (PR #105) | -| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Missing: comment navigation, Show Changes, Allow Edit Ranges, notes-to-comments conversion, accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) | +| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Comment navigation, Show All Notes and Convert to Comments (#281). Missing: Show Changes, Allow Edit Ranges, threaded comments in XLSX (saved as notes), accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) | | Automation | 4 | ~30% | ~20% | 12–20 | Recorded, editable GridCraft scripts (Automate tab). VBA is out of scope by owner decision and is **dropped** when an `.xlsm` is opened (warning shown). No Office Scripts compatibility | | **Weighted** | 100 | **~75%** | **~59%** | **300–485** | | @@ -332,6 +332,7 @@ The inventory carried over from the 2026-10-07 ROADMAP.md, updated for what land | Date | Change | Summary | |---|---|---| +| 2026-10-11 | minor | Review: Previous/Next Comment, Show Comments, Show All Notes, Convert to Comments; catalog 258 → 263 / 290 (#281) | | 2026-10-10 | minor | Readiness-by-audience table: hours to ~95% for each of the three numbers (full 650–1,050, mainstream 200–310, essentials 60–100). Full number confirmed as the additive weighted sum over the written dimension weights (no change) | | 2026-10-10 | minor | Added Mainstream practitioner (~50%) and Essentials user (~63%) with written weights and discounts, and user-evidence counts; re-checked the full number after the 10-10 merges (charts 38→30, localization 5→10; still 51.6, ~50%) | | 2026-10-10 | minor | Stage checked against the new alpha gate (six core workflows, all pass): stays alpha |