diff --git a/ROADMAP.md b/ROADMAP.md index 0f9166099..83881470d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~15 points (60% → 75% ready for real work) and ~150–230 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: All Caps text in PDFs, #1473; previously trivial: progress log: AutoFit in a nested table, whole cells shaded for a selection through a table, Shift+Right at a cell's end, #971; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (title rename and account settings; previously trivial: progress log: All Caps text in PDFs, #1473; previously trivial: progress log: AutoFit in a nested table, whole cells shaded for a selection through a table, Shift+Right at a cell's end, #971; previously trivial: progress log: privacy options on save, #1062; previously trivial: progress log: equation pictures in theme colours on Windows, #1002; previously trivial: symbol fonts, #776; previously trivial: the save prompt answers from the keyboard, #861; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw tab ink, #307, and Draw Table and Eraser, #303, landed: catalog 402/434; previously minor: readiness table with hours per audience; full number back to the additive weighted sum, 60%; mainstream and essentials numbers added; alpha gate checked; previously major: full re-measure against Word for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -136,6 +136,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | What landed | |---|---| +| 2026-10-12 | Document title opens Rename Document; the account badge opens Options › General for user name and initials. `file.rename {name}` preserves file extensions and refuses an existing destination; unsaved names become the suggested save name without changing the Title property. | | 2026-10-13 | Rounded rectangles (#1476): a `roundRect` keeps the corner radius its file gives it (`adj`), on screen, in PDF and when saved; all were drawn with a 16% corner | | 2026-10-13 | PAGEREF in headers and footers (#1477): "Page X of Y" with Y a page reference to a bookmark at each letter's end shows each letter's own total instead of the result stored in the file | | 2026-10-13 | Replace All respects locked content controls with Track Changes on or off; it refuses the entire batch before editing when any match would change locked content or remove a control locked against deletion | @@ -327,6 +328,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. | | 2026-10-13 | trivial | Progress log: rounded rectangle corners (#1476) | | 2026-10-13 | trivial | Progress log: PAGEREF in headers and footers (#1477) | | 2026-10-13 | trivial | Replace All checks content-control locks before editing; a refused batch leaves the document and undo history unchanged; existing deletion-lock rules reused, with tracked/untracked regression coverage | diff --git a/contributors/people.toml b/contributors/people.toml index 8bf8fc61c..c669b474d 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -75,6 +75,9 @@ sync_github_name = false real_name = "Manuel Carmona" display_name = "manucarcor" [people.odedigo] + +[people.neupaneprashant] +real_name = "Prashant Neupane" [people.cyril265] display_name = "cyril265" diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index bc83f5613..c4212a5cc 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -31,8 +31,39 @@ pub(crate) fn set_author(s: &mut Session, v: &Value) -> CmdResult { Ok(r) } +/// Rename the current file in its directory, or name a document before its first save. +fn rename(s: &mut Session, v: &Value) -> CmdResult { + let name = p::req_str(v, "name")?.trim(); + if name.is_empty() + || name.chars().count() > 200 + || name.ends_with(['.', ' ']) + || name.chars().any(|c| c.is_control() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|')) + || matches!(name, "." | "..") + { + return Err(CmdError::Params("Enter a valid document name (up to 200 characters, without path separators).".into())); + } + if let Some(path) = &s.path { + let mut destination = path.with_file_name(name); + // The field edits the stem; retain the original format, including macro/template files. + if let Some(ext) = path.extension() { + destination.set_file_name(format!("{name}.{}", ext.to_string_lossy())); + } + if destination != *path { + #[cfg(not(target_arch = "wasm32"))] + { + crate::io::rename_file(path, &destination).map_err(CmdError::Failed)?; + } + s.path = Some(destination); + } + } else { + s.file_name = Some(name.to_string()); + } + Ok(json!({"name": name, "path": s.path})) +} + pub fn specs() -> Vec { vec![ + CommandSpec::new("file.rename", "Rename Document", "File › Info", rename).params(r#"{"name": string (document name without its extension)}"#).pure(), CommandSpec::new("file.new", "New", "File", new).key("Mod+N").params(r#"{"template"?: "blank|sample|letter|resume|report"}"#).pure(), CommandSpec::new("file.open", "Open", "File", open) .key("Mod+O / Mod+F12") diff --git a/crates/engine/src/cmd/tools.rs b/crates/engine/src/cmd/tools.rs index 3f4dcf2d5..595b36b9a 100644 --- a/crates/engine/src/cmd/tools.rs +++ b/crates/engine/src/cmd/tools.rs @@ -1114,6 +1114,7 @@ fn compare(s: &mut Session, v: &Value) -> CmdResult { if show_in != "original" || p::str(v, "original").is_some() { // The result is a new, untitled document: saving it never overwrites either file. s.path = None; + s.file_name = None; } s.sel = Selection::caret(s.doc.start_of(StoryRef::Body)); Ok(json!({"changes": changes, "author": author, "notHonoured": not_honoured})) diff --git a/crates/engine/src/io.rs b/crates/engine/src/io.rs index c29499d4c..2c4df20f1 100644 --- a/crates/engine/src/io.rs +++ b/crates/engine/src/io.rs @@ -449,6 +449,40 @@ pub fn read_file_capped(path: &str, max: u64, what: &str) -> Result, Str std::fs::read(path).map_err(|e| format!("{path}: {e}")) } +/// Rename without replacing another file, respecting both automation access roots. +#[cfg(not(target_arch = "wasm32"))] +pub fn rename_file(source: &std::path::Path, destination: &std::path::Path) -> Result<(), String> { + crate::file_access::check_read(&source.to_string_lossy())?; + crate::file_access::check_write(&source.to_string_lossy())?; + crate::file_access::check_write(&destination.to_string_lossy())?; + // Creating a sibling hard link refuses collisions atomically and keeps the original bytes. + if let Err(e) = std::fs::hard_link(source, destination) { + if e.kind() == std::io::ErrorKind::AlreadyExists && only_case_differs(source, destination) { + return std::fs::rename(source, destination).map_err(|e| format!("Could not rename document: {e}")); + } + return Err(format!("Could not rename document: {e}")); + } + if let Err(e) = std::fs::remove_file(source) { + let _ = std::fs::remove_file(destination); + return Err(format!("Could not rename document: {e}")); + } + Ok(()) +} + +/// Whether `destination` names `source` in the same directory spelled with different case, and +/// the directory holds no entry spelled exactly as `destination`. +#[cfg(not(target_arch = "wasm32"))] +fn only_case_differs(source: &std::path::Path, destination: &std::path::Path) -> bool { + let (Some(dir), Some(from), Some(to)) = (source.parent(), source.file_name(), destination.file_name()) else { + return false; + }; + if destination.parent() != Some(dir) || from == to || from.to_string_lossy().to_lowercase() != to.to_string_lossy().to_lowercase() { + return false; + } + let dir = if dir.as_os_str().is_empty() { std::path::Path::new(".") } else { dir }; + std::fs::read_dir(dir).is_ok_and(|entries| entries.filter_map(Result::ok).all(|entry| entry.file_name() != to)) +} + /// The first `max` bytes of the file at `path` (fewer when it is shorter), inside the automation /// read root when there is one: enough to tell a file's format by its start. #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index f15030e9e..6387eac8b 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -337,6 +337,9 @@ pub struct Session { /// Formatting picked with a collapsed caret, applied to the next typed text. pub pending: Option, pub path: Option, + /// Name given to a document not saved yet (File › Info › Rename): what Save suggests. A file + /// name, not document metadata, so it is not part of the document or its undo history. + pub file_name: Option, pub dirty: bool, /// Where the custom dictionary is kept on disk, if the host wired one up. Only the desktop /// app does: headless sessions (MCP, `wordcraft-cli run`), tests and the web build leave @@ -586,6 +589,7 @@ impl Session { view: ViewState::default(), pending: None, path: None, + file_name: None, user_dict_path: None, dirty: false, clipboard: None, @@ -985,6 +989,7 @@ impl Session { self.doc = doc; self.doc.ensure_nonempty(); self.password = None; + self.file_name = None; self.sel = Selection::caret(self.doc.start_of(StoryRef::Body)); self.pending = None; self.also_selected.clear(); @@ -1024,6 +1029,7 @@ impl Session { view: _, pending: _, path: _, + file_name: _, // The custom dictionary file, like the file path: host wiring, not an edit. user_dict_path: _, dirty, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 7c0cbb77e..7b366888b 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -5541,6 +5541,68 @@ fn insert_picture_takes_a_windows_metafile() { let _ = std::fs::remove_dir_all(&dir); } +#[test] +fn rename_document_preserves_saved_contents_and_rejects_collisions() { + let dir = std::env::temp_dir().join(format!("wordcraft-rename-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let original = dir.join("original.docm"); + let renamed = dir.join("Notes.docm"); + let existing = dir.join("existing.docm"); + std::fs::write(&original, b"original bytes").unwrap(); + std::fs::write(&existing, b"keep these bytes").unwrap(); + let mut s = Session::new(wordcraft_doc::Document::new()); + s.path = Some(original.clone()); + s.dirty = true; + assert!(s.run("file.rename", &json!({"name": "existing"})).is_err()); + assert_eq!(s.path.as_ref(), Some(&original)); + assert_eq!(std::fs::read(&existing).unwrap(), b"keep these bytes"); + let roots = crate::file_access::AutomationRoots::new(Some(&dir), None).unwrap(); + assert!(crate::file_access::confine(roots.as_ref(), || s.run("file.rename", &json!({"name": "Notes"}))).is_err()); + assert!(original.exists()); + assert!(!renamed.exists()); + s.run("file.rename", &json!({"name": " Notes "})).unwrap(); + assert_eq!(s.path.as_ref(), Some(&renamed)); + assert!(s.dirty); + assert!(!original.exists()); + assert_eq!(std::fs::read(&renamed).unwrap(), b"original bytes"); + s.run("file.rename", &json!({"name": "Notes"})).unwrap(); + s.run("file.rename", &json!({"name": "notes"})).unwrap(); + let lower = dir.join("notes.docm"); + assert_eq!(s.path.as_ref(), Some(&lower)); + let names: Vec<_> = std::fs::read_dir(&dir).unwrap().map(|e| e.unwrap().file_name()).collect(); + assert!(names.contains(&"notes.docm".into()) && !names.contains(&"Notes.docm".into()), "{names:?}"); + assert_eq!(std::fs::read(&lower).unwrap(), b"original bytes"); + assert!(s.run("file.rename", &json!({"name": "existing"})).is_err()); + assert_eq!(s.path.as_ref(), Some(&lower)); + assert_eq!(std::fs::read(&existing).unwrap(), b"keep these bytes"); + std::fs::remove_dir_all(dir).unwrap(); +} + +#[test] +fn rename_unsaved_document_names_the_file_not_the_document() { + let mut s = Session::new(wordcraft_doc::Document::new()); + s.doc.core.title = "Quarterly report".into(); + for name in ["", " ", "../report", "a/b", "a\\b", "a\nb", "name."] { + assert!(s.run("file.rename", &json!({"name": name})).is_err(), "{name:?}"); + assert!(s.file_name.is_none()); + } + s.run("file.rename", &json!({"name": " Résumé notes "})).unwrap(); + assert_eq!(s.file_name.as_deref(), Some("Résumé notes")); + assert_eq!(s.doc.core.title, "Quarterly report"); + assert!(s.path.is_none()); + assert!(!s.dirty); + assert!(!s.can_undo()); + s.run("file.new", &json!({})).unwrap(); + assert!(s.file_name.is_none()); + s.run("file.rename", &json!({"name": "Draft"})).unwrap(); + s.run("file.open", &json!({"path": "other.txt", "data": "aGk="})).unwrap(); + assert!(s.file_name.is_none()); + s.run("file.new", &json!({})).unwrap(); + s.run("file.rename", &json!({"name": "Draft"})).unwrap(); + s.run("review.compare", &json!({"text": "changed", "showIn": "new"})).unwrap(); + assert!(s.file_name.is_none() && s.path.is_none()); +} + /// #1344: File › Info › Inspect Document finds hidden text by its effective formatting (direct, /// character style, paragraph style) in every story — body, header, footer, footnote, comment, /// text box — and Remove All deletes exactly that text, in one undo step, through a DOCX save. diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index c89f53266..cf187a016 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -286,9 +286,12 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) { // Account / community. let (r, resp) = ui.allocate_exact_size(vec2(control_h, control_h), Sense::click()); ui.painter().circle_filled(r.center(), control_h / 2.0 - 1.0, t.accent); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Account"))); let initials: String = app.session.user_initials().chars().take(2).collect(); ui.painter().text(r.center(), Align2::CENTER_CENTER, initials, semibold(10.5), t.on_accent); - resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author)); + if resp.on_hover_text(format!("{} — set your name in File › Options", app.session.author)).clicked() { + let _ = app.run("ui.backstage", json!({"value": true, "page": "options", "pane": "general"})); + } ui.add_space(8.0); if app.ui.show_discord { let (r, resp) = ui.allocate_exact_size(vec2(84.0, 22.0), Sense::click()); @@ -360,14 +363,29 @@ pub fn title_bar(app: &mut WordApp, ui: &mut Ui) { }; let font = TypeRung::Chrome.semibold(); let track = TypeRung::Chrome.tracking(); - let w = text_width(ui.painter(), &title, &font, track, t.text); let cx = full.center().x; - if cx - w / 2.0 > qat_end + 10.0 && cx + w / 2.0 < right_start - 10.0 { + let available = 2.0 * (cx - qat_end - 10.0).min(right_start - 10.0 - cx); + let mut shown: String = title.chars().take(240).collect(); + if text_width(ui.painter(), &shown, &font, track, t.text) > available { + while !shown.is_empty() && text_width(ui.painter(), &format!("{shown}…"), &font, track, t.text) > available { + shown.pop(); + } + shown.push('…'); + } + let w = text_width(ui.painter(), &shown, &font, track, t.text); + if available > 24.0 { + let rect = Rect::from_center_size(pos2(cx, full.center().y), vec2(w + 8.0, control_h)); + let response = ui.interact(rect, ui.id().with("document_name"), Sense::click()).on_hover_text(tl!("Rename Document")); + response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, &title)); + if response.clicked() { + let _ = app.run("file.rename", json!({})); + } + paint_text( ui.painter(), pos2(cx - w / 2.0, full.center().y - TypeRung::Chrome.line_height() / 2.0), Align2::LEFT_TOP, - &title, + &shown, &font, track, t.text, diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index dddf40d01..9f8e73299 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -140,6 +140,11 @@ fn special_menu(ui: &mut Ui, extra: &FindExtra) -> Option<&'static str> { #[derive(Clone, Debug, Serialize)] #[serde(tag = "dialog", rename_all = "camelCase")] pub enum Dialog { + RenameDocument { + name: String, + message: String, + focus: bool, + }, Font { /// The font (Latin text) and the Asian text font, as the font lists name them, and their /// values when the dialog opened: OK changes only the ones changed (#505). @@ -879,6 +884,7 @@ pub(crate) fn color_field(ui: &mut Ui, theme: &[wordcraft_doc::Rgb], value: &mut impl Dialog { pub fn name(&self) -> &'static str { match self { + Dialog::RenameDocument { .. } => "renameDocument", Dialog::Font { .. } => "font", Dialog::Paragraph { .. } => "paragraph", Dialog::Tabs { .. } => "tabs", @@ -1149,6 +1155,7 @@ impl Dialog { } } "pasteSpecial" => Dialog::paste_special(app, &json!({})), + "renameDocument" => Dialog::RenameDocument { name: app.title_stem(), message: String::new(), focus: true }, "about" => Dialog::About { tab: 0 }, "contributors" => Dialog::About { tab: 1 }, "models" => Dialog::About { tab: 2 }, @@ -1335,6 +1342,7 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) { let mut open = true; let mut close = false; let title = match &d { + Dialog::RenameDocument { .. } => "Rename Document", Dialog::Font { .. } => "Font", Dialog::Paragraph { .. } => "Paragraph", Dialog::Tabs { .. } => "Tabs", @@ -1607,6 +1615,32 @@ fn apply_font(app: &mut WordApp, d: &Dialog) { /// Returns true to close. fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { match d { + Dialog::RenameDocument { name, message, focus } => { + ui.label(tl!("Document name")); + let r = ui.add(egui::TextEdit::singleline(name).desired_width(320.0).char_limit(200)); + if *focus { + r.request_focus(); + *focus = false; + } + if !message.is_empty() { + ui.colored_label(ui.visuals().error_fg_color, message.as_str()); + } + let (ok, cancel) = buttons(ui, tl!("Rename")); + if cancel { + return true; + } + if ok && name.trim() == app.title_stem() { + return true; + } + if ok { + match app.run("file.rename", json!({"name": name})) { + Ok(_) => return true, + Err(e) => *message = e, + } + } + false + } + Dialog::Font { font, font_ea, asian, size, bold, italic, underline, strike, sup, sub, small_caps, caps, hidden, color, spacing, .. } => { // Build the sample text format for the preview pane below. let preview_size = size.trim().parse::().unwrap_or(12.0).clamp(6.0, 72.0); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f135f6a0d..2928a8060 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2462,6 +2462,12 @@ Table Properties… Свойства на таблица… Search Търсене Title Bar Заглавна лента + +# Document naming and account controls. + Rename Document Преименуване на документ + Document name Име на документа + Rename Преименуване + Account Акаунт Privacy Options Настройки за поверителност Trust Center Център за сигурност Privacy for this document Поверителност на този документ diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index a9add49f6..5d6631000 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2087,6 +2087,12 @@ Automatic ({region}) Automaticky ({region}) Search Hledat Title Bar Záhlaví okna + +# Document naming and account controls. + Rename Document Přejmenovat dokument + Document name Název dokumentu + Rename Přejmenovat + Account Účet Privacy Options Možnosti ochrany osobních údajů Trust Center Centrum zabezpečení Privacy for this document Soukromí tohoto dokumentu diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1f6ad51cb..961a5bf3a 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2081,6 +2081,12 @@ Automatic ({region}) Automatisch ({region}) Search Suchen Title Bar Titelleiste + +# Document naming and account controls. + Rename Document Dokument umbenennen + Document name Dokumentname + Rename Umbenennen + Account Konto Privacy Options Datenschutzoptionen Trust Center Sicherheitscenter Privacy for this document Datenschutz für dieses Dokument diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 94409af75..ecb0b04fa 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2448,6 +2448,12 @@ preview Title Título Start-up Inicio de la aplicación Show the Start screen when WordCraft starts Mostrar la pantalla de inicio al abrir WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Activado: WordCraft se abre en Archivo › Inicio, con un documento en blanco, las plantillas y tus documentos recientes. Desactivado: abre un documento en blanco. + +# Document naming and account controls. + Rename Document Cambiar nombre del documento + Document name Nombre del documento + Rename Cambiar nombre + Account Cuenta Privacy Options Opciones de privacidad Trust Center Centro de confianza Privacy for this document Privacidad de este documento diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index a620b2742..ec479f1e5 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -2116,6 +2116,12 @@ Start-up Käivitamine Show the Start screen when WordCraft starts Näita WordCrafti käivitamisel avakuva On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Sees: WordCraft avaneb vaates Fail › Avaleht, kus on tühi dokument, mallid ja hiljutised dokumendid. Väljas: avaneb tühi dokument. + +# Document naming and account controls. + Rename Document Nimeta dokument ümber + Document name Dokumendi nimi + Rename Nimeta ümber + Account Konto Privacy Options Privaatsussuvandid Trust Center Usalduskeskus Privacy for this document Selle dokumendi privaatsus diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index aa5feaeaa..f80080aad 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2090,6 +2090,12 @@ Table Properties… Propriétés du tableau… Search Rechercher Title Bar Barre de titre + +# Document naming and account controls. + Rename Document Renommer le document + Document name Nom du document + Rename Renommer + Account Compte Privacy Options Options de confidentialité Trust Center Centre de gestion de la confidentialité Privacy for this document Confidentialité de ce document diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 5d78a83b2..03ac09ce4 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2470,6 +2470,12 @@ Insert Caption… Felirat beszúrása… Table Properties… Táblázat tulajdonságai… Search Keresés + +# Document naming and account controls. + Rename Document Dokumentum átnevezése + Document name Dokumentum neve + Rename Átnevezés + Account Fiók Privacy Options Adatvédelmi beállítások Trust Center Adatvédelmi központ Privacy for this document A dokumentum adatvédelme diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 2189b5720..f66b05af2 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2457,6 +2457,12 @@ preview Title Esempio Automatic ({region}) Automatico ({region}) Search Cerca Title Bar Barra del titolo + +# Document naming and account controls. + Rename Document Rinomina documento + Document name Nome del documento + Rename Rinomina + Account Account Privacy Options Opzioni di privacy Trust Center Centro protezione Privacy for this document Privacy di questo documento diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 988d2f8d4..53c559721 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2082,6 +2082,12 @@ Start-up 起動 Show the Start screen when WordCraft starts WordCraft の起動時にスタート画面を表示する On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. オン: WordCraft は [ファイル] › [ホーム] で開き、白紙の文書、テンプレート、最近使った文書を表示します。オフ: 白紙の文書を開きます。 + +# Document naming and account controls. + Rename Document ドキュメント名の変更 + Document name ドキュメント名 + Rename 名前の変更 + Account アカウント Privacy Options プライバシー オプション Trust Center セキュリティ センター Privacy for this document このドキュメントのプライバシー diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 548db835e..915299dfa 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2466,6 +2466,12 @@ preview Title Tytuł Typing Pisanie Search Wyszukaj Title Bar Pasek tytułu + +# Document naming and account controls. + Rename Document Zmień nazwę dokumentu + Document name Nazwa dokumentu + Rename Zmień nazwę + Account Konto Privacy Options Opcje prywatności Trust Center Centrum zaufania Privacy for this document Prywatność tego dokumentu diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a9ff8489e..c85fe6017 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2094,6 +2094,12 @@ Start-up Inicialização Show the Start screen when WordCraft starts Mostrar a tela inicial ao abrir o WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Ativado: o WordCraft abre em Arquivo › Início, com um documento em branco, os modelos e seus documentos recentes. Desativado: abre um documento em branco. + +# Document naming and account controls. + Rename Document Renomear documento + Document name Nome do documento + Rename Renomear + Account Conta Privacy Options Opções de privacidade Trust Center Central de confiabilidade Privacy for this document Privacidade deste documento diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index e2a3a32f4..d17aba7ab 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2472,6 +2472,12 @@ preview Title Пример Level 7 Уровень 7 Level 8 Уровень 8 Level 9 Уровень 9 + +# Document naming and account controls. + Rename Document Переименовать документ + Document name Имя документа + Rename Переименовать + Account Учётная запись Privacy Options Параметры конфиденциальности Trust Center Центр управления безопасностью Privacy for this document Конфиденциальность этого документа diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 82ebf59af..75ed6ca10 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -2092,6 +2092,12 @@ Start-up Pokretanje Show the Start screen when WordCraft starts Prikaži početni ekran pri pokretanju programa WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Uključeno: WordCraft se otvara na Fajl › Početak, sa praznim dokumentom, šablonima i nedavnim dokumentima. Isključeno: otvara prazan dokument. + +# Document naming and account controls. + Rename Document Preimenuj dokument + Document name Ime dokumenta + Rename Preimenuj + Account Nalog Privacy Options Opcije privatnosti Trust Center Centar za bezbednost Privacy for this document Privatnost ovog dokumenta diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index 3f3d3ab2a..c75032ad3 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -2096,6 +2096,12 @@ Start-up Покретање Show the Start screen when WordCraft starts Прикажи почетни екран при покретању програма WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Укључено: WordCraft се отвара на Фајл › Почетак, са празним документом, шаблонима и недавним документима. Искључено: отвара празан документ. + +# Document naming and account controls. + Rename Document Преименуј документ + Document name Име документа + Rename Преименуј + Account Налог Privacy Options Опције приватности Trust Center Центар за безбедност Privacy for this document Приватност овог документа diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 46d687066..c42ec2a55 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2471,6 +2471,12 @@ preview Title Örnek Level 7 Düzey 7 Level 8 Düzey 8 Level 9 Düzey 9 + +# Document naming and account controls. + Rename Document Belgeyi yeniden adlandır + Document name Belge adı + Rename Yeniden adlandır + Account Hesap Privacy Options Gizlilik Seçenekleri Trust Center Güven Merkezi Privacy for this document Bu belgenin gizliliği diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index ee532c3a7..084548283 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2094,6 +2094,12 @@ Start-up Запуск Show the Start screen when WordCraft starts Показувати початковий екран під час запуску WordCraft On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. Увімкнено: WordCraft відкривається на сторінці Файл › Основне з порожнім документом, шаблонами та нещодавніми документами. Вимкнено: відкривається порожній документ. + +# Document naming and account controls. + Rename Document Перейменувати документ + Document name Ім’я документа + Rename Перейменувати + Account Обліковий запис Privacy Options Параметри конфіденційності Trust Center Центр безпеки Privacy for this document Конфіденційність цього документа diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index e51c4c276..2151cb7fe 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2539,3 +2539,9 @@ Heading text Văn bản đề mục Insert reference to: Chèn tham chiếu tới: Reference type: Loại tham chiếu: + +# Rename Document (#1016) + Account Tài khoản + Document name Tên tài liệu + Rename Đổi tên + Rename Document Đổi tên tài liệu diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index a9a5443f2..3ba6f3e3e 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2094,6 +2094,12 @@ Start-up 启动 Show the Start screen when WordCraft starts 启动 WordCraft 时显示开始屏幕 On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. 开:WordCraft 打开时显示“文件 › 开始”,其中有空白文档、模板和最近使用的文档。关:直接打开空白文档。 + +# Document naming and account controls. + Rename Document 重命名文档 + Document name 文档名称 + Rename 重命名 + Account 账户 Privacy Options 隐私选项 Trust Center 信任中心 Privacy for this document 此文档的隐私 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index bfc522d85..893048caf 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2083,6 +2083,12 @@ Start-up 啟動 Show the Start screen when WordCraft starts 啟動 WordCraft 時顯示開始畫面 On: WordCraft opens on File › Home, with a blank document, the templates and your recent documents. Off: it opens a blank document. 開:WordCraft 開啟時顯示「檔案 › 常用」,其中有空白文件、範本和最近使用的文件。關:直接開啟空白文件。 + +# Document naming and account controls. + Rename Document 重新命名文件 + Document name 文件名稱 + Rename 重新命名 + Account 帳戶 Privacy Options 隱私權選項 Trust Center 信任中心 Privacy for this document 此文件的隱私 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index a8bc0d55a..21961482b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -838,6 +838,7 @@ impl WordApp { return Ok(json!({"downloaded": name, "bytes": bytes.len()})); } let document = self.session.document_id(); + let old_path = self.session.path.clone(); let r = self.session.run(id, ¶ms).map_err(|e| e.to_string()); // When the document has been replaced (not when Open only showed its picker), a pending // "Save changes?" and AutoSave's go-ahead were both about the old one. @@ -856,6 +857,17 @@ impl WordApp { { self.autosave_path = self.session.path.clone(); } + // A renamed file is the same file: AutoSave keeps covering it, and Recent drops the old name. + if r.is_ok() + && id == "file.rename" + && let Some(old) = old_path.filter(|old| self.session.path.as_ref() != Some(old)) + { + if self.autosave_path.as_ref() == Some(&old) { + self.autosave_path = self.session.path.clone(); + } + let old = old.to_string_lossy(); + self.ui.recent.retain(|r| *r != old); + } if let Ok(v) = &r { self.keep_web_font(id, ¶ms, v); } @@ -1557,13 +1569,11 @@ impl WordApp { pub fn title_stem(&self) -> String { match &self.session.path { Some(p) => p.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_else(|| tl!("Document1").into()), - None => { - if self.session.doc.core.title.is_empty() { - tl!("Document1").into() - } else { - self.session.doc.core.title.clone() - } - } + None => match &self.session.file_name { + Some(name) => name.clone(), + None if self.session.doc.core.title.is_empty() => tl!("Document1").into(), + None => self.session.doc.core.title.clone(), + }, } } @@ -2075,6 +2085,7 @@ fn input_dialog(id: &str, params: &Value) -> Option<&'static str> { let has = |k: &str| params.get(k).is_some_and(|v| !v.is_null()); let rule = params.get("rule").and_then(Value::as_str).map(str::to_ascii_uppercase); match id { + "file.rename" if !has("name") => Some("renameDocument"), "mailings.rules" if !has("field") => match rule.as_deref() { None | Some("IF") => Some("ruleIf"), Some("SKIPIF") => Some("ruleSkipIf"), @@ -2306,6 +2317,63 @@ mod tests { WordApp::new(Session::new(wordcraft_doc::Document::new()), Services::default()) } + #[test] + fn title_and_account_buttons_work() { + use egui_kittest::kittest::Queryable; + let mut h = egui_kittest::Harness::builder().with_size(egui::vec2(1440.0, 900.0)).build_ui_state( + |ui, app: &mut WordApp| { + let ctx = ui.ctx().clone(); + app.logic(&ctx); + app.ui(ui); + }, + app(), + ); + h.run_steps(6); + h.get_by_label("Document1 — Not saved").click(); + h.run_steps(2); + assert_eq!(h.state().dialog.as_ref().map(|d| d.name()), Some("renameDocument")); + h.get_by_role_and_label(egui::accesskit::Role::Button, "Rename").click(); + h.run_steps(2); + assert!(h.state().dialog.is_none()); + assert!(h.state().session.file_name.is_none()); + assert!(!h.state().session.dirty); + h.get_by_label("Document1 — Not saved").click(); + h.run_steps(2); + h.event(egui::Event::Key { + key: egui::Key::A, + physical_key: None, + pressed: true, + repeat: false, + modifiers: egui::Modifiers { command: true, ..Default::default() }, + }); + h.event(egui::Event::Text("Project notes".into())); + h.run_steps(2); + h.get_by_role_and_label(egui::accesskit::Role::Button, "Rename").click(); + h.run_steps(2); + assert!(h.state().dialog.is_none()); + assert_eq!(h.state().title_stem(), "Project notes"); + assert_eq!(h.state().default_save_name(), "Project notes.docx"); + assert!(h.state().session.doc.core.title.is_empty()); + assert!(!h.state().session.dirty); + // A long name remains clickable, and Escape leaves it unchanged. + let long_name = "Notes ".repeat(25); + h.state_mut().run("file.rename", json!({"name": long_name})).unwrap(); + h.run_steps(2); + h.get_by_label(&format!("{} — Not saved", long_name.trim())).click(); + h.run_steps(2); + assert!(h.state().dialog.is_some()); + h.event(egui::Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() }); + h.run_steps(2); + assert!(h.state().dialog.is_none()); + assert_eq!(h.state().title_stem(), long_name.trim()); + h.get_by_label("Account").click(); + h.run_steps(2); + assert!(h.state().ui.backstage); + assert_eq!(h.state().ui.backstage_page, "options"); + assert_eq!(h.state().ui.options_pane, "general"); + assert!(h.query_by_label("User name:").is_some()); + } + /// File › Options › Advanced › Editing: alignment guides are a UI command, on by default and /// kept between runs. #[test] @@ -4161,6 +4229,35 @@ mod tests { } } + #[test] + fn renaming_a_saved_file_keeps_autosave_and_replaces_its_recent_entry() { + let dir = scratch("rename-autosave"); + let path = dir.join("notes.docx"); + let renamed = dir.join("Notes v2.docx"); + let mut a = typed(); + a.run("file.save", json!({"path": path.to_string_lossy()})).unwrap(); + assert!(a.autosaves()); + a.run("file.rename", json!({"name": "Notes v2"})).unwrap(); + assert_eq!(a.session.path.as_deref(), Some(renamed.as_path())); + assert!(a.autosaves(), "AutoSave still covers the renamed file"); + assert_eq!(a.ui.recent.first(), Some(&renamed.to_string_lossy().to_string())); + assert!(!a.ui.recent.contains(&path.to_string_lossy().to_string()), "{:?}", a.ui.recent); + a.run("text.insert", json!({"text": "More "})).unwrap(); + a.autosave_tick(now_ms() + 10_000.0); + assert!(!a.session.dirty, "AutoSave wrote the renamed file"); + let saved = wordcraft_engine::io::open_path(&renamed).unwrap(); + assert!(saved.plain_text(wordcraft_doc::StoryRef::Body).contains("More ")); + assert!(!path.exists()); + + // A file opened but not saved in this session doesn't gain AutoSave by being renamed. + let mut b = app(); + b.run("file.open", json!({"path": renamed.to_string_lossy()})).unwrap(); + assert!(!b.saved_here()); + b.run("file.rename", json!({"name": "Notes v3"})).unwrap(); + assert!(!b.saved_here() && !b.autosaves()); + let _ = std::fs::remove_dir_all(&dir); + } + /// Web: the download callback couldn't report a failure, so Save marked the document saved /// and the prompt went ahead even when no download started. #[test] diff --git a/docs/gaps.md b/docs/gaps.md index ba11b7a91..4ba224f6b 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where WordCraft falls short of Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (ODT tracked insertions and deletions, gap 19; previously trivial: RTF tracked insertions and deletions, gap 19; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (title rename and account settings; previously trivial: ODT tracked insertions and deletions, gap 19; previously trivial: RTF tracked insertions and deletions, gap 19; previously trivial: the Font list no longer keeps every previewed font in memory, #848; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously trivial: formatting revisions landed, #41; previously trivial: Draw Table and Eraser landed, #303; previously minor: beta distance restated: ~15 points, ~150–230 h; previously: first version; every known shortfall from the 2026-10-10 re-measure, ranked) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 > **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below. @@ -11,6 +11,8 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes. Native save reliability: synthetic cases on macOS reproduced loss through the predictable temporary-file name. Saves now use owned, exclusively created siblings, synchronize before replacement and clean up on failure; originals survive pre-replacement errors. This narrows save-path reliability under gap #3; the listed field failures and broader stability work remain open. See [save-path guarantees](file-format-parity.md#native-save-path-guarantees). +Document renaming: desktop renaming requires a filesystem that supports hard links (exFAT/FAT drives and some network shares refuse it). Behaviour: [UI parity](ui-parity.md). + ## Ranked | # | Gap | B | Evidence | Impact | Hours | Doc | @@ -80,6 +82,7 @@ The same gaps grouped the way the parity documents are, for agents working in on | Date | Change | Summary | |---|---|---| +| 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. | | 2026-10-13 | trivial | Replace All checks content-control locks before editing; a refused batch leaves the document and undo history unchanged; existing deletion-lock rules reused, with tracked/untracked regression coverage | | 2026-10-12 | trivial | Sample-text formulas on Enter (#1067) implemented; numerical limits not compared in Word | | 2026-10-12 | minor | Italian proofing landed (it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar); gap #8 narrowed to the remaining languages | diff --git a/docs/mcp.md b/docs/mcp.md index ed539f138..65d500d6c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -45,8 +45,8 @@ config works across the suite) keep the agent to the folders you give it: `styles.organizer` and `styles.copyFrom` read. The pictures an opened HTML file names are read from beside that file, so they are inside the read root too (a link that leads out is not followed). - Every file a command writes must lie inside the **write root**: `save_document`, `file.save`, - `file.saveAs`, `file.exportPdf`, `file.exportPng`, `file.printPdf`, `file.saveTemplate`, and `mailings.finish` with - a `path`. + `file.saveAs`, `file.rename` (the file and its new name), `file.exportPdf`, `file.exportPng`, `file.printPdf`, + `file.saveTemplate`, and `mailings.finish` with a `path`. - Read and write access are separate. Give the same folder twice for both; a root left out grants none of its access (with only `--automation-read-root`, nothing can be written to disk). Without either flag nothing changes. diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 171d962d1..29639f689 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,11 +1,13 @@ # UI parity with Microsoft Word -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Cross-reference dialog, #916; previously trivial: web Save Changes prompt waits for the save picker, #1315; previously trivial: AutoFit in a nested table, whole cells shaded for a selection through a table, #971; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Cross-reference dialog, #916; previously trivial: title rename and account settings; previously trivial: web Save Changes prompt waits for the save picker, #1315; previously trivial: AutoFit in a nested table, whole cells shaded for a selection through a table, #971; previously trivial: no document behind the Start screen, #1129; previously trivial: equation pictures share a few textures, #1002; previously trivial: selection bar in the left margin, #856; previously trivial: table column and row markers on the rulers, #700; previously trivial: table borders and the resize handle drag on the page, #217; previously major: first version; ribbon, dialogs, shortcuts and on-canvas interaction measured from `crates/ui-egui` and the command registry) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 How WordCraft looks and feels next to Word: ribbon, dialogs, panes, keyboard, mouse and touch. Typing behaviour has its own checklist ([`typing-parity.md`](typing-parity.md)). All assets are our own (`AGENTS.md`: no Microsoft iconography, ever). +Document title opens Rename Document; the account badge opens Options › General for user name and initials. Saved documents retain their format and refuse conflicting names; desktop renaming requires a filesystem that supports hard links. Web renaming changes the next download name. An unsaved document's name is only the name Save suggests: its Title property is left alone. + **Dimension: ~65% (estimated), 59–92 h to full** (the sum of the rows below; dialog depth is counted here, not under features). | Area | WordCraft | Word | Parity | Hours | @@ -50,6 +52,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever). | Date | Change | Summary | |---|---|---| | 2026-10-12 | trivial | Cross-reference dialog: reference type (heading, bookmark, equation, figure, table), what to insert and the item; the ribbon button opened nothing before (#916); modal dialog count 55 | +| 2026-10-12 | trivial | Document title opens Rename Document; the account badge opens Options › General for user name and initials. | | 2026-10-13 | trivial | The caret is black or white by contrast with the paper, page colour, dark page, shading or highlight under it (#299) | | 2026-10-13 | trivial | Clicks on ribbon and status-bar chrome that run no command button (Styles and style-set gallery tiles, tab names, empty ribbon space, View check boxes, the status bar's view buttons and language menu) no longer take keyboard focus from the page: the caret stays and typing continues, as in Word; text fields such as the font size box still take focus. Tabs, gallery tiles and the status bar's view buttons report their name and state to screen readers (#1093) | | 2026-10-13 | trivial | Interface size: a chosen size is absolute ("100%" is one screen pixel per point, even where the system reports 200 % under a bare window manager) and the new default, Auto, keeps the display scale and shows the size it draws at; 75–300 %; an old saved size becomes the size it drew at; on the web a chosen size cancels browser zoom, Auto follows it (#822) |