mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 12:27:11 -06:00
feat(engine): rename documents from the title bar and open account settings from the badge (#1016)
* M10: enable document renaming and account settings * no-mistakes(review): Allow case-only document renames without overwriting files * no-mistakes(review): Keep unsaved rename as session file name, not Title * no-mistakes(review): Keep AutoSave and Recent across renames; Compare clears name * no-mistakes(document): Document file.rename write-root rule; dedupe rename gap note * vi: Rename Document strings Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
5597351213
commit
aedbbc9325
+3
-1
@@ -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 |
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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<CommandSpec> {
|
||||
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")
|
||||
|
||||
@@ -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}))
|
||||
|
||||
@@ -449,6 +449,40 @@ pub fn read_file_capped(path: &str, max: u64, what: &str) -> Result<Vec<u8>, 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"))]
|
||||
|
||||
@@ -337,6 +337,9 @@ pub struct Session {
|
||||
/// Formatting picked with a collapsed caret, applied to the next typed text.
|
||||
pub pending: Option<CharProps>,
|
||||
pub path: Option<std::path::PathBuf>,
|
||||
/// 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<String>,
|
||||
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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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::<f32>().unwrap_or(12.0).clamp(6.0, 72.0);
|
||||
|
||||
@@ -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 Поверителност на този документ
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 459 and column 13.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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 このドキュメントのプライバシー
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 566 and column 13.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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 Конфиденциальность этого документа
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 25.
|
@@ -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 Приватност овог документа
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 25.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 440 and column 13.
|
@@ -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 Конфіденційність цього документа
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 31.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -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 此文档的隐私
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 25.
|
@@ -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 此文件的隱私
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 567 and column 13.
|
+104
-7
@@ -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]
|
||||
|
||||
+4
-1
@@ -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 |
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
+4
-1
@@ -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) |
|
||||
|
||||
Reference in New Issue
Block a user