From 4eea87db2d84b3b25076c44c3ce0ff29f10064cc Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Thu, 23 Jul 2026 22:10:42 +0800 Subject: [PATCH] feat(editor): hide account entry points for v0.8.2 --- .../agent_settings_account_gate_tests.rs | 42 ++++++++++++++ .../src/widgets/agent_settings_panel.rs | 5 +- .../widgets/agent_settings_panel_geometry.rs | 17 ++++++ crates/op-editor-ui/src/widgets/mod.rs | 9 +++ crates/op-editor-ui/src/widgets/top_bar.rs | 10 ++-- .../op-editor-ui/src/widgets/top_bar_tests.rs | 22 ++++--- .../src/widget_host/account_press_tests.rs | 57 +++++++++++++++++-- .../op-host-native/src/widget_host/input.rs | 8 ++- .../op-host-native/src/widget_host/paint.rs | 4 +- .../op-host-native/src/widget_host/press.rs | 11 +++- .../src/widget_host/shortcuts.rs | 2 +- .../src/widget_host/keyboard_edit_ops.rs | 2 +- 12 files changed, 159 insertions(+), 30 deletions(-) create mode 100644 crates/op-editor-ui/src/widgets/agent_settings_account_gate_tests.rs diff --git a/crates/op-editor-ui/src/widgets/agent_settings_account_gate_tests.rs b/crates/op-editor-ui/src/widgets/agent_settings_account_gate_tests.rs new file mode 100644 index 000000000..285618e9f --- /dev/null +++ b/crates/op-editor-ui/src/widgets/agent_settings_account_gate_tests.rs @@ -0,0 +1,42 @@ +use crate::widgets::agent_settings_panel::AgentSettingsPanel; +use op_editor_core::agent_settings::AgentSettingsTab; +use op_editor_core::EditorState; + +#[test] +fn account_tab_release_gate_hides_nav_and_falls_back_from_stale_state() { + const { assert!(!crate::widgets::ACCOUNT_UI_AVAILABLE) }; + let default_state = EditorState::default(); + let default_panel = AgentSettingsPanel::for_editor(&default_state); + let expected_agents_height = default_panel.content_total_height(); + + let mut state = EditorState::default(); + state.editor_ui.agent_settings.tab = AgentSettingsTab::Account; + let panel = AgentSettingsPanel::for_editor(&state); + let rect = panel.rect(1200.0, 800.0); + + let expected_tabs = [ + AgentSettingsTab::Agents, + AgentSettingsTab::Mcp, + AgentSettingsTab::Images, + AgentSettingsTab::Fonts, + AgentSettingsTab::System, + ]; + for (index, expected) in expected_tabs.into_iter().enumerate() { + let point = crate::Point2D::new( + rect.origin.x + 100.0, + rect.origin.y + 56.0 + index as f32 * 30.0 + 14.0, + ); + assert_eq!(panel.nav_at(rect, point), Some(expected)); + } + + let hidden_account_row = crate::Point2D::new( + rect.origin.x + 100.0, + rect.origin.y + 56.0 + expected_tabs.len() as f32 * 30.0 + 14.0, + ); + assert_eq!(panel.nav_at(rect, hidden_account_row), None); + assert_eq!( + panel.content_total_height(), + expected_agents_height, + "a persisted Account tab must fall back to the first visible tab" + ); +} diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs index fd5cdb11f..f8ec00820 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel.rs @@ -9,7 +9,8 @@ use crate::widgets::agent_settings_mcp::{self, McpHit}; use crate::widgets::agent_settings_panel_card::paint_agent_card; use crate::widgets::agent_settings_panel_geometry::{ acp_section_y, agent_card_rect_at, agent_card_rect_in, close_rect, connect_btn_rect_at, - content_paint_clip_rect, content_rect, disconnect_btn_rect_at, nav_item_rect, tab_i18n_label, + content_paint_clip_rect, content_rect, disconnect_btn_rect_at, full_settings_tabs, + nav_item_rect, tab_i18n_label, }; use crate::widgets::agent_settings_system::{self, SystemHit}; use crate::widgets::editor_state_ext::theme_for; @@ -52,7 +53,7 @@ pub enum AgentSettingsPanelMode { impl AgentSettingsPanelMode { fn visible_tabs(self) -> &'static [AgentSettingsTab] { match self { - AgentSettingsPanelMode::Full => &AgentSettingsTab::ALL, + AgentSettingsPanelMode::Full => full_settings_tabs(), AgentSettingsPanelMode::WebBuiltinOnly => &[ AgentSettingsTab::Agents, AgentSettingsTab::Images, diff --git a/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs b/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs index 02aa6c41a..b6a800a89 100644 --- a/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs +++ b/crates/op-editor-ui/src/widgets/agent_settings_panel_geometry.rs @@ -5,6 +5,7 @@ use crate::widgets::agent_settings_panel::{ CARD_GAP, CARD_HEIGHT, CONNECT_BTN_H, CONNECT_BTN_W, NAV_ITEM_HEIGHT, NAV_ITEM_STEP, NAV_TOP, PAD, SECTION_GAP, SIDEBAR_WIDTH, }; +use crate::widgets::ACCOUNT_UI_AVAILABLE; use crate::widgets::{agent_settings_acp, agent_settings_builtin}; use crate::{Point2D, Rect}; use op_editor_core::agent_settings::{AgentSettings, AgentSettingsTab}; @@ -12,6 +13,22 @@ use op_editor_core::editor_ui_state::EditorUiState; const DISCONNECT_BTN_W: f32 = 96.0; +const FULL_TABS_WITHOUT_ACCOUNT: [AgentSettingsTab; 5] = [ + AgentSettingsTab::Agents, + AgentSettingsTab::Mcp, + AgentSettingsTab::Images, + AgentSettingsTab::Fonts, + AgentSettingsTab::System, +]; + +pub(super) fn full_settings_tabs() -> &'static [AgentSettingsTab] { + if ACCOUNT_UI_AVAILABLE { + &AgentSettingsTab::ALL + } else { + &FULL_TABS_WITHOUT_ACCOUNT + } +} + pub(super) fn tab_i18n_label(ui: &EditorUiState, tab: AgentSettingsTab) -> &'static str { match tab { AgentSettingsTab::Agents => t_settings(ui, "settings.tab.agents"), diff --git a/crates/op-editor-ui/src/widgets/mod.rs b/crates/op-editor-ui/src/widgets/mod.rs index f41a8e464..c570c8bec 100644 --- a/crates/op-editor-ui/src/widgets/mod.rs +++ b/crates/op-editor-ui/src/widgets/mod.rs @@ -26,6 +26,13 @@ use crate::{Point2D, Rect, RenderBackend}; /// `const MIN_RAIL_WIDTH` in each host). pub const MIN_RAIL_WIDTH: f32 = 80.0; +/// Temporary release gate for the unfinished user-account experience. +/// +/// Keep the account state, modal, menu, translations, and host dispatchers +/// compiled and tested so a later release can restore every entry point from +/// this one gate, then update the release-gate assertions. +pub const ACCOUNT_UI_AVAILABLE: bool = false; + // Phase B primitives. pub(crate) mod button; pub mod prop_row; @@ -174,6 +181,8 @@ pub mod brand_icons; // Step 4 — extra editor-chrome widgets (TS app parity). pub mod account_menu; pub mod agent_settings_account; +#[cfg(test)] +mod agent_settings_account_gate_tests; pub mod agent_settings_acp; mod agent_settings_acp_helpers; pub mod agent_settings_builtin; diff --git a/crates/op-editor-ui/src/widgets/top_bar.rs b/crates/op-editor-ui/src/widgets/top_bar.rs index ae1b109fe..4b1314130 100644 --- a/crates/op-editor-ui/src/widgets/top_bar.rs +++ b/crates/op-editor-ui/src/widgets/top_bar.rs @@ -41,12 +41,10 @@ pub(super) const GIT_BUTTON_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); /// has no equivalent runtime/measurement bridge, so hide the button there /// rather than exposing a non-interactive preview flag. pub(super) const PREVIEW_BUTTON_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); -/// The planned sign-in flow (OIDC Auth Code + PKCE via the system -/// browser) is desktop-only. The web build has no equivalent, so the -/// avatar button (and its layout slot) is compiled out there rather -/// than painting a click that goes nowhere (same pattern as -/// `GIT_BUTTON_AVAILABLE` / `PREVIEW_BUTTON_AVAILABLE`). -pub const ACCOUNT_BUTTON_AVAILABLE: bool = !cfg!(target_arch = "wasm32"); +/// The account button follows the shared release gate and remains +/// desktop-only when that experience is enabled again. +pub const ACCOUNT_BUTTON_AVAILABLE: bool = + super::ACCOUNT_UI_AVAILABLE && !cfg!(target_arch = "wasm32"); /// Stacked agent-icon metrics — mirror TS `top-bar.tsx` /// (`w-5 h-5 rounded-md bg-foreground/10 ring-1 ring-card` chips /// overlapped by `-space-x-1.5`). diff --git a/crates/op-editor-ui/src/widgets/top_bar_tests.rs b/crates/op-editor-ui/src/widgets/top_bar_tests.rs index 9a135e7b1..83a5c5316 100644 --- a/crates/op-editor-ui/src/widgets/top_bar_tests.rs +++ b/crates/op-editor-ui/src/widgets/top_bar_tests.rs @@ -335,10 +335,9 @@ fn chip_with_only_builtin_agents_reserves_no_icon_cluster() { ); } -/// The user-avatar button sits directly left of the Globe button — -/// between the agent chip and the locale/theme cluster. #[test] -fn account_button_hit_tests_and_sits_left_of_globe() { +fn account_button_release_gate_removes_hit_target_and_layout_slot() { + const { assert!(!ACCOUNT_BUTTON_AVAILABLE) }; let bar = TopBar::untitled(); let rect = Rect { origin: Point2D::new(0.0, 0.0), @@ -348,13 +347,20 @@ fn account_button_hit_tests_and_sits_left_of_globe() { let globe = bar.globe_rect(rect); assert!( nearly_eq(account.origin.x + account.size.x, globe.origin.x), - "avatar button should abut the globe button's left edge" + "dormant avatar geometry should stay ready for the next release" ); - let center = Point2D::new( - account.origin.x + account.size.x / 2.0, - account.origin.y + account.size.y / 2.0, + assert!( + nearly_eq(bar.chip_right_anchor_x(rect), globe.origin.x), + "hiding the avatar must also collapse its layout slot" ); - assert_eq!(bar.hit_test(rect, center), Some(TopBarHit::Account)); + for x in 0..1000 { + let hit = bar.hit_test(rect, Point2D::new(x as f32, TOP_BAR_HEIGHT / 2.0)); + assert_ne!( + hit, + Some(TopBarHit::Account), + "hidden account control hit at x={x}" + ); + } } #[test] diff --git a/crates/op-host-native/src/widget_host/account_press_tests.rs b/crates/op-host-native/src/widget_host/account_press_tests.rs index 13d9b0533..dfdbebd52 100644 --- a/crates/op-host-native/src/widget_host/account_press_tests.rs +++ b/crates/op-host-native/src/widget_host/account_press_tests.rs @@ -62,7 +62,7 @@ impl Drop for EnvVarGuard { } #[test] -fn clicking_avatar_while_anonymous_opens_login_modal() { +fn account_release_gate_blocks_login_modal_for_anonymous_user() { let mut host = WidgetHostNative::new(); assert_eq!( host.editor_state().editor_ui.account, @@ -75,14 +75,14 @@ fn clicking_avatar_while_anonymous_opens_login_modal() { account_rect.origin.y + account_rect.size.y / 2.0, ); - assert!(host.apply_press(center.x, center.y, VW, VH)); + host.apply_press(center.x, center.y, VW, VH); - assert!(host.editor_state().editor_ui.login_modal_open); + assert!(!host.editor_state().editor_ui.login_modal_open); assert!(!host.editor_state().editor_ui.account_menu_open); } #[test] -fn clicking_avatar_while_signed_in_opens_account_menu() { +fn account_release_gate_blocks_menu_for_signed_in_state() { let mut host = WidgetHostNative::new(); host.editor_state_mut().editor_ui.account = AccountState::SignedIn { display_name: "Fini".into(), @@ -95,12 +95,57 @@ fn clicking_avatar_while_signed_in_opens_account_menu() { account_rect.origin.y + account_rect.size.y / 2.0, ); - assert!(host.apply_press(center.x, center.y, VW, VH)); + host.apply_press(center.x, center.y, VW, VH); - assert!(host.editor_state().editor_ui.account_menu_open); + assert!(!host.editor_state().editor_ui.account_menu_open); assert!(!host.editor_state().editor_ui.login_modal_open); } +#[test] +fn stale_login_modal_state_does_not_dispatch_while_release_gate_is_hidden() { + const { assert!(!op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE) }; + let mut host = WidgetHostNative::new(); + host.editor_state_mut().editor_ui.login_modal_open = true; + + let modal = LoginModal::for_editor(host.editor_state()); + let panel = modal.rect(VW, VH); + let sign_in_point = Point2D::new( + panel.origin.x + panel.size.x / 2.0, + panel.origin.y + panel.size.y - 56.0, + ); + host.apply_press(sign_in_point.x, sign_in_point.y, VW, VH); + + assert_eq!( + host.editor_state().editor_ui.account, + AccountState::Anonymous + ); + assert!(!host.editor_state().editor_ui.login_modal_stub_hint_shown); +} + +#[test] +fn stale_account_menu_state_does_not_dispatch_while_release_gate_is_hidden() { + const { assert!(!op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE) }; + let mut host = WidgetHostNative::new(); + let signed_in = AccountState::SignedIn { + display_name: "Fini".into(), + handle: "fini".into(), + }; + host.editor_state_mut().editor_ui.account = signed_in.clone(); + host.editor_state_mut().editor_ui.account_menu_open = true; + + let top_bar = TopBar::for_editor_ui(&host.editor_state().editor_ui); + let anchor = top_bar.account_button_rect(top_bar_rect()); + let menu = AccountMenu::for_editor_ui(&host.editor_state().editor_ui).expect("signed in"); + let menu_rect = menu.rect_at(anchor); + let sign_out_point = Point2D::new( + menu_rect.origin.x + 20.0, + menu_rect.origin.y + menu_rect.size.y - 8.0, + ); + host.apply_press(sign_out_point.x, sign_out_point.y, VW, VH); + + assert_eq!(host.editor_state().editor_ui.account, signed_in); +} + #[test] fn login_modal_sign_in_without_dev_flag_shows_honest_stub_hint() { let _lock = env_lock(); diff --git a/crates/op-host-native/src/widget_host/input.rs b/crates/op-host-native/src/widget_host/input.rs index d5901cde2..d7254e7d1 100644 --- a/crates/op-host-native/src/widget_host/input.rs +++ b/crates/op-host-native/src/widget_host/input.rs @@ -580,7 +580,9 @@ impl WidgetHostNative { } // Sign-in modal — owns the cursor while open. Hover the close // `✕` + the primary sign-in button. - if self.editor_state.editor_ui.login_modal_open { + if op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE + && self.editor_state.editor_ui.login_modal_open + { use op_editor_ui::widgets::login_modal::LoginModal; let modal = LoginModal::for_editor(&self.editor_state); let panel = modal.rect(self.last_viewport_w, self.last_viewport_h); @@ -595,7 +597,9 @@ impl WidgetHostNative { return changed; } // Signed-in account dropdown — owns the cursor while open. - if self.editor_state.editor_ui.account_menu_open { + if op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE + && self.editor_state.editor_ui.account_menu_open + { use op_editor_ui::widgets::account_menu::AccountMenu; use op_editor_ui::widgets::top_bar::TopBar; use op_editor_ui::widgets::TOP_BAR_HEIGHT; diff --git a/crates/op-host-native/src/widget_host/paint.rs b/crates/op-host-native/src/widget_host/paint.rs index 3817cd1bc..cabae5e1a 100644 --- a/crates/op-host-native/src/widget_host/paint.rs +++ b/crates/op-host-native/src/widget_host/paint.rs @@ -533,7 +533,7 @@ impl WidgetHostNative { } // 10e. Sign-in modal — full-viewport scrim + centred card. - if ui.login_modal_open { + if op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE && ui.login_modal_open { use op_editor_ui::widgets::login_modal::LoginModal; frame.fill_rect( Rect { @@ -558,7 +558,7 @@ impl WidgetHostNative { // 10f. Signed-in account dropdown — anchored under the TopBar // avatar button, no scrim (same tier as the file menu / // locale picker). - if ui.account_menu_open { + if op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE && ui.account_menu_open { use op_editor_ui::widgets::account_menu::AccountMenu; let top_bar_rect = Rect { origin: Point2D::new(0.0, 0.0), diff --git a/crates/op-host-native/src/widget_host/press.rs b/crates/op-host-native/src/widget_host/press.rs index afa49c4ae..c4244b2a0 100644 --- a/crates/op-host-native/src/widget_host/press.rs +++ b/crates/op-host-native/src/widget_host/press.rs @@ -396,7 +396,9 @@ impl WidgetHostNative { self.dispatch_figma_import_press(x, y, viewport_width, viewport_height); return true; } - if self.editor_state.editor_ui.login_modal_open { + if op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE + && self.editor_state.editor_ui.login_modal_open + { self.close_image_popovers_for_higher_overlay(); self.dispatch_login_modal_press(x, y, viewport_width, viewport_height); return true; @@ -405,7 +407,9 @@ impl WidgetHostNative { // 0a'. Account dropdown — anchored under the TopBar avatar // button; must hit-test before the TopBar's own block so a // re-click on the avatar closes rather than re-toggling. - if self.editor_state.editor_ui.account_menu_open { + if op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE + && self.editor_state.editor_ui.account_menu_open + { self.close_image_popovers_for_higher_overlay(); self.dispatch_account_menu_press(x, y, viewport_width, viewport_height); return true; @@ -629,6 +633,9 @@ impl WidgetHostNative { return true; } TopBarHit::Account => { + if !op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE { + return false; + } if self.editor_state.editor_ui.account.is_signed_in() { self.editor_state.editor_ui.account_menu_open = true; self.editor_state.editor_ui.account_menu_hover = None; diff --git a/crates/op-host-native/src/widget_host/shortcuts.rs b/crates/op-host-native/src/widget_host/shortcuts.rs index 02ccc201c..7d2f8a27e 100644 --- a/crates/op-host-native/src/widget_host/shortcuts.rs +++ b/crates/op-host-native/src/widget_host/shortcuts.rs @@ -443,7 +443,7 @@ impl WidgetHostNative { if ui.figma_import_in_progress || !ui.figma_import_pages.is_empty() || ui.export_dialog_open - || ui.login_modal_open + || (op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE && ui.login_modal_open) || ui.agent_settings_open || (ui.missing_fonts_modal_open && ui diff --git a/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs b/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs index 1cdd3248a..4a96f1c87 100644 --- a/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs +++ b/crates/op-host-web/src/widget_host/keyboard_edit_ops.rs @@ -608,7 +608,7 @@ impl WidgetHost { if ui.figma_import_in_progress || !ui.figma_import_pages.is_empty() || ui.export_dialog_open - || ui.login_modal_open + || (op_editor_ui::widgets::ACCOUNT_UI_AVAILABLE && ui.login_modal_open) || ui.agent_settings_open || (ui.missing_fonts_modal_open && ui