feat(editor): hide account entry points for v0.8.2

This commit is contained in:
Kayshen-X 2026-07-23 22:10:42 +08:00
parent 5c8a4163c2
commit 4eea87db2d
12 changed files with 159 additions and 30 deletions

View file

@ -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"
);
}

View file

@ -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,

View file

@ -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"),

View file

@ -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;

View file

@ -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`).

View file

@ -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]

View file

@ -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();

View file

@ -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;

View file

@ -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),

View file

@ -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;

View file

@ -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

View file

@ -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