collective

Browse / github.com/storytold/cadcraft / patch

feat(ui): customize persistent status bar item visibility

feat(ui): customize persistent status bar item visibility ## Change Add persistent status-bar item visibility (CC-037). The customization button and status-background context menu show/hide readouts and drafting controls without changing their settings or the drawing. The button remains available with all items hidden; Window > Show All Status Items restores defaults. Checkbox customization stays open for repeated changes. Focused extraction from historical fork commit fc0c091, adapted to current egui and app preferences. Includes command routing, validated persistence, Ukrainian labels and a UI parity record. ## Dependencies and overlap - Ba

Status
open
Score
0.00
Adopters
0 other operators
Author
smacdmac101 (imported from pull request #2218, open)
Base
d3ae8a6af0b76185e33f6dc2735679fa5c2be77c
Size
+325 −21 in 11 files
Review
pending
Shared
2026-10-28
Id
552a41bcb529a4d33bfd065c

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/chrome.rs crates/ui-egui/src/chrome/status.rs crates/ui-egui/src/control.rs crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/lib.rs crates/ui-egui/src/menus.rs crates/ui-egui/tests/it/main.rs crates/ui-egui/tests/it/status_customization.rs docs/ui-parity.md xtask/src/parity.rs xtask/src/stats.rs

diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs--- a/crates/ui-egui/src/chrome.rs+++ b/crates/ui-egui/src/chrome.rs@@ -8,6 +8,7 @@ use crate::icons::{self, Icon}; use crate::theme::Tokens;  pub mod annoscale;+pub mod status;  const TOOLBAR: &[&[(Icon, &str, &str, Option<&str>)]] = &[     &[@@ -296,6 +297,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {     let t = Tokens::get();     egui::Panel::bottom("cc_status").exact_size(26.0).frame(egui::Frame::NONE.fill(t.chrome)).show(ui, |ui| {         let r = ui.max_rect();+        let background = ui.interact(r, ui.scope_id().with("status_context"), Sense::click());         let p = ui.painter().clone();         let mut x = r.left() + 8.0;         // Clear the status message on time even when no input arrives (the app only repaints on input).@@ -326,7 +328,7 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {         let s = app.session.settings.clone();         let anno_all = app.session.doc().is_ok_and(|d| d.anno_all_visible());         use cadcraft_engine::snap::mode;-        let toggles: Vec<(Icon, bool, &str, &str)> = vec![+        let mut toggles: Vec<(Icon, bool, &str, &str)> = vec![             (Icon::Grid, s.gridmode, "grid", "Grid (F7)"),             (Icon::Snap, s.snapmode, "snap", "Snap Mode (F9)"),             (Icon::Ortho, s.orthomode, "ortho", "Ortho Mode (F8)"),@@ -342,10 +344,11 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {             (Icon::Workspace, false, "ui.noop", "Workspace Switching"),             (Icon::Gear, false, "ui.dialog.dsettings", "Drafting Settings"),         ];+        toggles.retain(|(_, _, command, _)| status::visible(app, status::command_id(command)));         let size = 20.0;-        let mut rx = r.right() - 8.0 - toggles.len() as f32 * (size + 4.0);+        let mut rx = r.right() - 32.0 - toggles.len() as f32 * (size + 4.0);         // Coordinates.-        let coord = app.canvas.cursor.map(|c| {+        let coord = app.canvas.cursor.filter(|_| status::visible(app, "coordinates")).map(|c| {             let (lu, lp) = app.session.doc().map(|d| (d.header.i64("LUNITS", 2), d.header.i64("LUPREC", 4))).unwrap_or((2, 4));             format!(                 "{}, {}, {}",@@ -357,22 +360,28 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {         // MODEL / PAPER (layouts only): toggles MSPACE and PSPACE like AutoCAD's status button.         let mut coord_right = rx - 8.0;         // The annotation scale list (model space).-        coord_right = annoscale::status(app, ui, coord_right, r.center().y);+        if status::visible(app, "annotationScale") {+            coord_right = annoscale::status(app, ui, coord_right, r.center().y);+        }         if matches!(app.session.layout_space(), cadcraft_doc::Space::Paper(_)) {-            let model = app.session.state().is_ok_and(|st| st.mspace.is_some());-            let label = if model { "MODEL" } else { "PAPER" };-            let br = Rect::from_min_max(pos2(rx - 60.0, r.center().y - 9.0), pos2(rx - 6.0, r.center().y + 9.0));-            let resp = ui.interact(br, ui.scope_id().with("mspace_toggle"), Sense::click());-            p.rect_filled(br, 3.0, if resp.hovered() { t.control_hover } else { t.toggle_on.gamma_multiply(0.25) });-            p.text(br.center(), egui::Align2::CENTER_CENTER, crate::i18n::t(label), crate::theme::small(), t.text);-            icons::describe_control(ui, &resp, label, None, false);-            if resp.on_hover_text(crate::tl!("Switch between model space in a viewport and paper space")).clicked() {-                let _ = app.run(if model { "pspace" } else { "mspace" }, json!({}));-                app.canvas.list = None;+            if status::visible(app, "paperSpace") {+                let model = app.session.state().is_ok_and(|st| st.mspace.is_some());+                let label = if model { "MODEL" } else { "PAPER" };+                let br = Rect::from_min_max(pos2(rx - 60.0, r.center().y - 9.0), pos2(rx - 6.0, r.center().y + 9.0));+                let resp = ui.interact(br, ui.scope_id().with("mspace_toggle"), Sense::click());+                p.rect_filled(br, 3.0, if resp.hovered() { t.control_hover } else { t.toggle_on.gamma_multiply(0.25) });+                p.text(br.center(), egui::Align2::CENTER_CENTER, crate::i18n::t(label), crate::theme::small(), t.text);+                icons::describe_control(ui, &resp, label, None, false);+                if resp.on_hover_text(crate::tl!("Switch between model space in a viewport and paper space")).clicked() {+                    let _ = app.run(if model { "pspace" } else { "mspace" }, json!({}));+                    app.canvas.list = None;+                }+                coord_right = br.left() - 10.0;             }-            coord_right = br.left() - 10.0;             // Scale and lock of the active or selected viewport.-            coord_right = crate::vpscale::status(app, ui, coord_right + 4.0, r.center().y) - 10.0;+            if status::visible(app, "viewportScale") {+                coord_right = crate::vpscale::status(app, ui, coord_right + 4.0, r.center().y) - 10.0;+            }         }         // Layout tabs: Model, "+", then the layouts that fit before the right-hand controls (the         // current one always); the layout list reaches the rest.@@ -512,6 +521,13 @@ pub fn status_bar(app: &mut CadApp, ui: &mut egui::Ui) {             Some(c) => crate::menus::run_transparent(app, c),             None => {}         }+        let br = Rect::from_min_size(pos2(r.right() - 28.0, r.center().y - 10.0), vec2(20.0, 20.0));+        let customize = ui.interact(br, ui.scope_id().with("status_customize"), Sense::click());+        icons::paint(&p, br.shrink(2.5), Icon::Menu, false);+        icons::describe_control(ui, &customize, "Status Bar Customization", None, false);+        egui::Popup::menu(&customize).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| status::menu(app, ui));+        customize.on_hover_text(crate::tl!("Status Bar Customization"));+        egui::Popup::context_menu(&background).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| status::menu(app, ui));     }); } diff --git a/crates/ui-egui/src/chrome/status.rs b/crates/ui-egui/src/chrome/status.rsnew file mode 100644--- /dev/null+++ b/crates/ui-egui/src/chrome/status.rs@@ -0,0 +1,85 @@+//! Status-item visibility is an application preference, independent of drafting settings.+use crate::CadApp;+use serde_json::{Value, json};++/// Stable preference IDs paired with translated status-item labels.+pub const ITEMS: &[(&str, &str)] = &[+    ("coordinates", "Coordinates"),+    ("paperSpace", "Paper Space"),+    ("annotationScale", "Annotation scale"),+    ("viewportScale", "Viewport scale"),+    ("grid", "Grid (F7)"),+    ("snap", "Snap Mode (F9)"),+    ("ortho", "Ortho Mode (F8)"),+    ("polar", "Polar Tracking (F10)"),+    ("isodraft", "Isometric Drafting"),+    ("otrack", "Object Snap Tracking (F11)"),+    ("osnap", "Object Snap (F3)"),+    ("lwdisplay", "Show/Hide Lineweight"),+    ("transparencydisplay", "Show/Hide Transparency"),+    ("dynmode", "Dynamic Input (F12)"),+    ("qpmode", "Quick Properties"),+    ("annotationVisibility", "Annotation Visibility"),+    ("workspace", "Workspace Switching"),+    ("draftingSettings", "Drafting Settings"),+];+/// Reject unknown or duplicate IDs before replacing stored preferences.+pub fn validate(hidden: &[String]) -> Result<(), String> {+    if hidden.len() > ITEMS.len()+        || hidden.iter().enumerate().any(|(index, id)| !ITEMS.iter().any(|(known, _)| id == known) || hidden[..index].contains(id))+    {+        return Err("Invalid status bar items".into());+    }+    Ok(())+}+/// Whether a status item is shown, independently of its drafting setting.+pub fn visible(app: &CadApp, id: &str) -> bool {+    !app.ui.hidden_status_items.iter().any(|hidden| hidden == id)+}+pub(super) fn command_id(command: &str) -> &str {+    match command {+        "annovisible.toggle" => "annotationVisibility",+        "ui.noop" => "workspace",+        "ui.dialog.dsettings" => "draftingSettings",+        _ => command,+    }+}+pub(crate) fn route(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {+    if id == "ui.status.reset" {+        app.ui.hidden_status_items.clear();+        return Some(Ok(json!({"hiddenStatusItems":[]})));+    }+    if id != "ui.status.item" {+        return None;+    }+    Some((|| {+        let item =+            params.get("item").and_then(Value::as_str).filter(|id| ITEMS.iter().any(|(known, _)| known == id)).ok_or("Unknown status bar item")?;+        let shown = match params.get("visible") {+            None => !visible(app, item),+            Some(v) => v.as_bool().ok_or("visible must be a boolean")?,+        };+        let mut hidden = app.ui.hidden_status_items.clone();+        hidden.retain(|id| id != item);+        if !shown {+            hidden.push(item.to_string());+        }+        validate(&hidden)?;+        app.ui.hidden_status_items = hidden;+        Ok(json!({"item":item,"visible":shown}))+    })())+}+pub(crate) fn menu(app: &mut CadApp, ui: &mut egui::Ui) {+    if ui.button(crate::tl!("Show All Status Items")).clicked() {+        let _ = app.run("ui.status.reset", json!({}));+    }+    ui.separator();+    egui::ScrollArea::vertical().max_height(360.0).show(ui, |ui| {+        for (id, label) in ITEMS {+            let mut shown = visible(app, id);+            if ui.checkbox(&mut shown, crate::i18n::t(label)).changed() {+                let _ = app.run("ui.status.item", json!({"item":id,"visible":shown}));+            }+        }+    });+}diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs--- a/crates/ui-egui/src/control.rs+++ b/crates/ui-egui/src/control.rs@@ -457,6 +457,9 @@ pub fn handle(app: &mut CadApp, ctx: &egui::Context, req: &ControlRequest) -> Ou                     if let Err(error) = crate::docking::validate(&u.docking) {                         return err(error);                     }+                    if let Err(error) = crate::chrome::status::validate(&u.hidden_status_items) {+                        return err(error);+                    }                     app.ui = u;                     ok(serde_json::to_value(&app.ui).unwrap_or_default())                 }diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv--- a/crates/ui-egui/src/i18n/uk.tsv+++ b/crates/ui-egui/src/i18n/uk.tsv@@ -1371,3 +1371,7 @@ 	Check for Updates…	Перевірити оновлення… 	CADCraft is up to date.	Встановлено найновішу версію CADCraft. 	Could not check for updates.	Не вдалося перевірити оновлення.+	Coordinates	Координати+	Show All Status Items	Показати всі елементи рядка стану+	Status Bar Customization	Налаштування рядка стану+	Status Bar Item	Елемент рядка стануdiff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs--- a/crates/ui-egui/src/lib.rs+++ b/crates/ui-egui/src/lib.rs@@ -81,6 +81,7 @@ pub struct UiState {     pub show_toolbar: bool,     pub show_file_tabs: bool,     pub show_status_bar: bool,+    pub hidden_status_items: Vec<String>,     pub show_command_line: bool,     pub show_viewcube: bool,     pub show_ucs_icon: bool,@@ -169,6 +170,7 @@ impl Default for UiState {             show_toolbar: true,             show_file_tabs: true,             show_status_bar: true,+            hidden_status_items: Vec::new(),             show_command_line: true,             show_viewcube: true,             show_ucs_icon: true,@@ -437,12 +439,21 @@ impl CadApp {         if let Some(dir) = &self.ui.last_lisp_dir {             prefs["lispDir"] = json!(dir);         }+        if !self.ui.hidden_status_items.is_empty() {+            prefs["hiddenStatusItems"] = json!(self.ui.hidden_status_items);+        }         prefs.to_string()     }      /// Restore preferences saved by [`Self::prefs_json`]; unknown or malformed values are ignored.     pub fn load_prefs(&mut self, json: &str) {         let Ok(v) = serde_json::from_str::<Value>(json) else { return };+        if let Some(value) = v.get("hiddenStatusItems")+            && let Ok(hidden) = serde_json::from_value::<Vec<String>>(value.clone())+            && chrome::status::validate(&hidden).is_ok()+        {+            self.ui.hidden_status_items = hidden;+        }         if let Some(prefs) = v.get("spacemouse") {             let _ = self.spacemouse.load_settings(prefs);         }diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs--- a/crates/ui-egui/src/menus.rs+++ b/crates/ui-egui/src/menus.rs@@ -72,6 +72,8 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[     ("ui.cmdline.clear", "Clear Command History", &[], None),     ("ui.dialog.language", "Interface Language", &["Edit", "Interface Language"], None),     ("ui.language", "Language", &[], None),+    ("ui.status.item", "Status Bar Item", &[], None),+    ("ui.status.reset", "Show All Status Items", &["Window", "Show All Status Items"], None),     ("ui.noop", "", &[], None),     ("ui.quit", "Quit CADCraft", &[], Some("Cmd+Q")), ];@@ -86,6 +88,9 @@ pub fn run_ui_command(app: &mut CadApp, id: &str, params: &Value) -> Option<Resu }  fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Result<Value, String>> {+    if let Some(result) = crate::chrome::status::route(app, id, params) {+        return Some(result);+    }     if let Some(result) = crate::spacemouse::route(app, id, params) {         return Some(result);     }@@ -339,7 +344,17 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re         "ui.resetpalettes" => {             let (menu, theme, interface_language, save_format) = (app.ui.in_window_menu, app.ui.theme, app.ui.interface_language, app.ui.save_format);             let (animations, save_version) = (app.ui.animations, std::mem::take(&mut app.ui.save_version));-            app.ui = crate::UiState { in_window_menu: menu, theme, animations, interface_language, save_format, save_version, ..Default::default() };+            let hidden_status_items = std::mem::take(&mut app.ui.hidden_status_items);+            app.ui = crate::UiState {+                in_window_menu: menu,+                theme,+                animations,+                interface_language,+                save_format,+                save_version,+                hidden_status_items,+                ..Default::default()+            };             Ok(Value::Null)         }         "ui.start" => {diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs--- a/crates/ui-egui/tests/it/main.rs+++ b/crates/ui-egui/tests/it/main.rs@@ -28,6 +28,7 @@ mod selection_cycling; mod shared_controls; mod shortcut_menus; mod status_bar_toggles;+mod status_customization; mod style_dialogs; mod system_cursor; mod text_double_click;diff --git a/crates/ui-egui/tests/it/status_customization.rs b/crates/ui-egui/tests/it/status_customization.rsnew file mode 100644--- /dev/null+++ b/crates/ui-egui/tests/it/status_customization.rs@@ -0,0 +1,143 @@+//! The drafting toggles on the status bar are transparent, as F7-F12 are: clicking Grid or Snap+//! while drawing a line changes the setting and leaves LINE running (#1108).++use cadcraft_engine::doc::{Drawing, EntityKind, Space};+use cadcraft_engine::geom::Vec2;+use cadcraft_engine::{Input, Session};+use cadcraft_ui_egui::{CadApp, Services};+use egui::vec2;+use egui_kittest::Harness;+use egui_kittest::kittest::Queryable;+use serde_json::json;++fn harness() -> Harness<'static, CadApp> {+    let mut session = Session::empty();+    session.open_drawing(Drawing::new_imperial(), "Status bar toggles", None);+    let app = CadApp::new(session, Services::default());+    let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state(+        |ui, app: &mut CadApp| {+            app.logic(ui.ctx());+            app.ui(ui);+        },+        app,+    );+    h.run_steps(3);+    h+}++fn running(h: &Harness<'_, CadApp>) -> Option<String> {+    h.state().session.running.as_ref().map(|r| r.id.clone())+}++#[test]+fn status_readout_right_click_opens_customization() {+    let mut h = harness();+    h.state_mut().canvas.cursor = Some(Vec2::new(1.0, 2.0));+    h.run_steps(3);+    let at = egui::pos2(800.0, 987.0);+    h.event(egui::Event::PointerMoved(at));+    h.run_steps(2);+    for pressed in [true, false] {+        h.event(egui::Event::PointerButton { pos: at, button: egui::PointerButton::Secondary, pressed, modifiers: egui::Modifiers::NONE });+        h.run_steps(2);+    }+    assert!(h.query_by_label("Show All Status Items").is_some());+    h.get_by_role_and_label(egui::accesskit::Role::CheckBox, "Grid (F7)").click();+    h.run_steps(4);+    assert!(h.state().ui.hidden_status_items.iter().any(|id| id == "grid"));+    assert!(h.query_by_label("Show All Status Items").is_some(), "right-click customization stays open after toggling an item");+}++#[test]+fn status_visibility_is_atomic_persistent_and_separate_from_drafting_settings() {+    use serde_json::json;+    let mut h = harness();+    let settings = h.state().session.settings.clone();+    let dirty = h.state().session.state().unwrap().is_dirty();+    for (item, _) in cadcraft_ui_egui::chrome::status::ITEMS {+        h.state_mut().run("ui.status.item", json!({"item":item,"visible":false})).unwrap();+    }+    let before = h.state().ui.hidden_status_items.clone();+    for params in [json!({"item":"unknown"}), json!({"item":"grid","visible":"no"}), json!({"visible":true})] {+        assert!(h.state_mut().run("ui.status.item", params).is_err());+        assert_eq!(h.state().ui.hidden_status_items, before);+    }+    let restored: cadcraft_ui_egui::UiState = serde_json::from_value(serde_json::to_value(&h.state().ui).unwrap()).unwrap();+    assert_eq!(restored.hidden_status_items, before);+    let prefs = h.state().prefs_json();+    let mut restarted = CadApp::new(Session::new(), Services::default());+    restarted.load_prefs(&prefs);+    assert_eq!(restarted.ui.hidden_status_items, before);+    for invalid in [json!(["grid", "grid"]), json!(["unknown"]), json!([false]), json!("grid")] {+        restarted.load_prefs(&json!({"hiddenStatusItems":invalid}).to_string());+        assert_eq!(restarted.ui.hidden_status_items, before, "invalid preferences must be ignored atomically");+    }+    restarted.load_prefs("{\"hiddenStatusItems\":[]}");+    assert!(restarted.ui.hidden_status_items.is_empty());+    assert!(serde_json::from_value::<cadcraft_ui_egui::UiState>(json!({})).unwrap().hidden_status_items.is_empty());+    assert_eq!(h.state().session.settings.gridmode, settings.gridmode);+    assert_eq!(h.state().session.settings.snapmode, settings.snapmode);+    assert_eq!(h.state().session.state().unwrap().is_dirty(), dirty);+    h.run_steps(4);+    assert!(h.query_by_label("Grid (F7)").is_none());+    h.get_by_label("Status Bar Customization").click();+    h.run_steps(4);+    h.get_by_label("Show All Status Items").click();+    h.run_steps(4);+    assert!(h.state().ui.hidden_status_items.is_empty());+}++#[test]+fn status_customization_does_not_cancel_or_feed_a_point_to_a_running_line() {+    use egui::accesskit::Role;+    let mut h = harness();+    h.state_mut().session.start("line").unwrap();+    h.state_mut().session.input(Input::Point(Vec2::new(0.0, 0.0))).unwrap();+    h.run_steps(3);+    let grid = h.state().session.settings.gridmode;+    h.get_by_label("Status Bar Customization").click();+    h.run_steps(4);+    h.get_by_role_and_label(Role::CheckBox, "Grid (F7)").click();+    h.run_steps(4);+    assert!(h.state().ui.hidden_status_items.iter().any(|id| id == "grid"));+    assert_eq!(h.state().session.settings.gridmode, grid);+    assert_eq!(running(&h).as_deref(), Some("line"));+    h.get_by_label("Show All Status Items").click();+    h.run_steps(3);+    assert_eq!(running(&h).as_deref(), Some("line"));+    h.state_mut().session.input(Input::Point(Vec2::new(10.0, 0.0))).unwrap();+    let count = h.state().session.doc().unwrap().space(&Space::Model).unwrap().iter().filter(|e| matches!(e.kind, EntityKind::Line(_))).count();+    assert_eq!(count, 1);+}++/// Resetting side palettes must not reset the independent status preferences.+#[test]+fn status_visibility_survives_palette_reset() {+    let mut h = harness();+    h.state_mut().run("ui.status.item", serde_json::json!({"item":"grid","visible":false})).unwrap();+    h.state_mut().run("ui.resetpalettes", serde_json::json!({})).unwrap();+    h.run_steps(4);+    assert_eq!(h.state().ui.hidden_status_items, ["grid"]);+    assert!(h.query_by_label("Grid (F7)").is_none());+    h.state_mut().run("ui.status.reset", serde_json::json!({})).unwrap();+    h.run_steps(4);+    assert!(h.query_by_label("Grid (F7)").is_some());+}+/// The generic control route must reject an invalid list before changing any UI field.+#[test]+fn ui_set_rejects_invalid_status_items_atomically() {+    use cadcraft_ui_egui::control::{ControlRequest, Outcome, handle};+    let mut app = CadApp::new(Session::default(), Services::default());+    let ctx = egui::Context::default();+    let (request, _) = ControlRequest::new("ui.set", json!({"hiddenStatusItems":["grid"]}));+    assert!(matches!(handle(&mut app, &ctx, &request), Outcome::Done(v) if v["ok"] == true));+    let before = serde_json::to_value(&app.ui).unwrap();+    for invalid in [json!(["grid", "grid"]), json!(["unknown"]), json!(vec!["grid"; 19]), json!("grid")] {+        let (request, _) = ControlRequest::new("ui.set", json!({"hiddenStatusItems":invalid,"showToolbar":!app.ui.show_toolbar}));+        assert!(matches!(handle(&mut app, &ctx, &request), Outcome::Done(v) if v["ok"] == false));+        assert_eq!(serde_json::to_value(&app.ui).unwrap(), before);+    }+    let mut restored = CadApp::new(Session::default(), Services::default());+    restored.load_prefs(&app.prefs_json());+    assert_eq!(restored.ui.hidden_status_items, ["grid"]);+}diff --git a/docs/ui-parity.md b/docs/ui-parity.md--- a/docs/ui-parity.md+++ b/docs/ui-parity.md@@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (distinct polyline midpoint grips) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-26 · **Change:** minor (status item customization) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)  How CADCraft's interaction compares with AutoCAD's: point entry, object snaps, tracking, dynamic input, grips, selection, the command line, chrome and navigation. A drafter's speed in AutoCAD@@ -199,10 +199,30 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.   have menus too (#512). Still missing: in-place layout tab rename, Move or Copy, Select All   Layouts. +## Status item customization++The status-bar customization menu shows or hides individual readouts and drafting controls.+Visibility is an application preference: hiding Grid does not disable the drawing grid, change+system variables, dirty the drawing, or cancel an active command. The customization button+remains accessible when all items are hidden; Window > Show All Status Items also restores them.+Unknown, duplicate or malformed saved item lists are ignored atomically.+Both customization entry points keep the menu open when an item is toggled. Reset Palettes+preserves status-item visibility; Show All Status Items is the dedicated recovery action.++AutoCAD 2027 on Windows was observed black-box on 2026-10-26: toggling Coordinates in its+customization menu updates the status bar while keeping the menu open. CADCraft's customization+button follows that interaction. Native restart and browser reload preserve visibility; Windows+right-click on the status readout also opens customization. Browser right-click was not confirmed.+Actual release browser bundles were exercised with the default GPU backend and forced WebGL2;+these runtime checks are separate from the WASM compilation gate.+ ## Revision history  | Date | Change | Summary | |---|---|---|+| 2026-10-26 | patch | Reject invalid status visibility through ui.set before changing any UI state; regression covers duplicates, unknown identifiers, oversized lists and malformed values (CC-037 review follow-up) |+| 2026-10-26 | patch | Keep right-click status customization open for consecutive toggles and preserve hidden status items through Reset Palettes (CC-037 review corrections) |+| 2026-10-26 | minor | Status item visibility, persistent application preferences, recovery command and command-isolation regressions (CC-037); no ribbon dependency | | 2026-10-22 | minor | Polyline segment midpoint grips use rectangles, distinct from square vertex grips, with matching hover and pick bounds (#1950) | | 2026-10-13 | minor | Filled-region highlight boundaries are traversed in deterministic coordinate order, fixing dash-phase jumps on CPU hover/selection redraws and GPU highlight rebuilds; CPU dash-position, pixel and GPU mesh regressions | | 2026-10-14 | minor | Selection cycling (#1143): the list and badge of SELECTIONCYCLING, Shift+Space through overlapping objects (#1145) |diff --git a/xtask/src/parity.rs b/xtask/src/parity.rs--- a/xtask/src/parity.rs+++ b/xtask/src/parity.rs@@ -136,7 +136,13 @@ mod tests {         assert_eq!(day(20_736), "2026-10-10");         assert_eq!(date(std::time::UNIX_EPOCH - std::time::Duration::from_secs(86_400)), "1970-01-01");         // Past chrono's range keeps the proleptic Gregorian date, not the epoch (old `civil_from_days`).-        assert_eq!(day(115_740_740), "318857-05-20");+        // Checked on the day count: Windows' SystemTime ends in the year 30828, so building that+        // SystemTime panics there.+        assert_eq!(civil_from_days(115_740_740), (318_857, 5, 20));+        // Also exercise the date formatter where the platform can represent this instant.+        if let Some(large) = std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(115_740_740 * 86_400)) {+            assert_eq!(date(large), "318857-05-20");+        }         // Windows' SystemTime can't hold u64::MAX / 2 seconds, so it is only checked where it exists.         if let Some(far) = std::time::UNIX_EPOCH.checked_add(std::time::Duration::from_secs(u64::MAX / 2)) {             assert_eq!(date(far), "292277026596-12-04");diff --git a/xtask/src/stats.rs b/xtask/src/stats.rs--- a/xtask/src/stats.rs+++ b/xtask/src/stats.rs@@ -134,7 +134,7 @@ pub fn run(_root: &Path, exact: bool) -> Result<(), String> {  #[cfg(test)] mod tests {-    use super::{Counts, count_tests, walk};+    use super::{Counts, count_tests};      #[test]     fn counts_attributes_and_proptest_blocks() {@@ -155,7 +155,7 @@ mod tests {         // A directory symlink pointing back at `real` would be re-scanned if followed.         symlink(&real, base.join("alias")).unwrap();         let mut c = Counts::default();-        walk(&base, &mut c).unwrap();+        super::walk(&base, &mut c).unwrap();         assert_eq!(c.files, 1, "the aliased directory was followed and counted twice");         std::fs::remove_dir_all(&base).ok();     }