From ae283c224357d218af379f594d33f2762a4e703b Mon Sep 17 00:00:00 2001 From: hz Date: Thu, 16 Jul 2026 23:07:26 -0500 Subject: [PATCH] Fix narrow web layouts and loader branding --- scripts/ui/main.gd | 16 +++++++- scripts/ui/study_selector.gd | 51 ++++++++++++++++++++++--- tests/simulation_history_ui_test.gd.uid | 1 + tests/web_responsive_test.gd | 49 ++++++++++++++++++++++++ tests/web_responsive_test.gd.uid | 1 + web/medmind_shell.html | 26 ------------- 6 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 tests/simulation_history_ui_test.gd.uid create mode 100644 tests/web_responsive_test.gd create mode 100644 tests/web_responsive_test.gd.uid diff --git a/scripts/ui/main.gd b/scripts/ui/main.gd index c2eb237..11b365b 100644 --- a/scripts/ui/main.gd +++ b/scripts/ui/main.gd @@ -51,6 +51,12 @@ func _update_responsive_layout() -> void: _apply_screen_margins(current_screen_name) if header_stats != null: header_stats.visible = size.x >= 720.0 + if user_label != null: + user_label.visible = size.x >= 760.0 + if feedback_button != null: + feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0 + if account_chevron_button != null: + account_chevron_button.visible = size.x >= 480.0 if drawer_open and not compact_shell: _close_drawer() if app_menu != null: @@ -185,7 +191,9 @@ func _build_ui() -> void: theme_button = Button.new() theme_button.tooltip_text = "Switch color theme" theme_button.custom_minimum_size = Vector2(44, 44) + theme_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER theme_button.size_flags_vertical = Control.SIZE_SHRINK_CENTER + theme_button.focus_mode = Control.FOCUS_NONE theme_button.pressed.connect(theme_service.toggle_theme) _style_shell_theme_button(theme_button) header.add_child(theme_button) @@ -592,7 +600,11 @@ func _style_shell_theme_button(button: Button) -> void: normal.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER normal.set_border_width_all(1) - normal.set_corner_radius_all(22) + normal.set_corner_radius_all(24) + normal.content_margin_left = 10 + normal.content_margin_right = 10 + normal.content_margin_top = 10 + normal.content_margin_bottom = 10 var hover := normal.duplicate() as StyleBoxFlat hover.border_color = Brand.AMBER button.add_theme_stylebox_override("normal", normal) @@ -628,7 +640,7 @@ func _refresh_header_stats() -> void: if account_button != null: account_button.text = _account_initials(AuthState.username) if feedback_button != null: - feedback_button.visible = AuthState.is_logged_in + feedback_button.visible = AuthState.is_logged_in and size.x >= 560.0 feedback_button.icon = preload("res://assets/icons/lucide/message-square.svg") feedback_button.add_theme_constant_override("icon_max_width", 21) diff --git a/scripts/ui/study_selector.gd b/scripts/ui/study_selector.gd index 98cea7b..d26a9c9 100644 --- a/scripts/ui/study_selector.gd +++ b/scripts/ui/study_selector.gd @@ -8,6 +8,8 @@ signal stage_changed(stage_name: String) var main: Control var exam_view: VBoxContainer var mode_view: VBoxContainer +var stage_scroll: ScrollContainer +var stage_content: VBoxContainer var exam_grid: VBoxContainer var exam_top_row: BoxContainer var mode_grid: GridContainer @@ -21,6 +23,8 @@ var pending_ccs_launch := false var current_stage := "exam" var pending_question_bank_launch := false var themed_controls: Array[Control] = [] +var responsive_cards: Array[PanelContainer] = [] +var responsive_card_rows: Array[BoxContainer] = [] func _ready() -> void: @@ -36,6 +40,15 @@ func _ready() -> void: if not CCSService.request_failed.is_connected(_on_ccs_failed): CCSService.request_failed.connect(_on_ccs_failed) + stage_scroll = ScrollContainer.new() + stage_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL + stage_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL + stage_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + add_child(stage_scroll) + stage_content = VBoxContainer.new() + stage_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL + stage_scroll.add_child(stage_content) + _build_exam_view() _build_mode_view() _show_stage("exam") @@ -58,7 +71,7 @@ func _build_exam_view() -> void: exam_view = VBoxContainer.new() exam_view.add_theme_constant_override("separation", 18) exam_view.size_flags_vertical = Control.SIZE_EXPAND_FILL - add_child(exam_view) + stage_content.add_child(exam_view) var eyebrow := Label.new() eyebrow.text = "GET STARTED" @@ -69,6 +82,8 @@ func _build_exam_view() -> void: var title := Label.new() title.text = "What are you preparing for?" + title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 34) exam_view.add_child(title) @@ -102,7 +117,7 @@ func _build_mode_view() -> void: mode_view = VBoxContainer.new() mode_view.add_theme_constant_override("separation", 16) mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL - add_child(mode_view) + stage_content.add_child(mode_view) var back_button := Button.new() back_button.text = "< All exams" @@ -125,6 +140,8 @@ func _build_mode_view() -> void: var title := Label.new() title.text = "Choose a study mode" + title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 34) mode_view.add_child(title) @@ -201,6 +218,9 @@ func _build_mode_view() -> void: note.bbcode_enabled = true note.fit_content = true note.scroll_active = false + note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + note.size_flags_horizontal = Control.SIZE_EXPAND_FILL + note.custom_minimum_size.x = 0 note.text = "Full-length, 12-block simulation is available in the [url=exams][color=#7c3aed]Exams[/color][/url] section." note.add_theme_font_override("normal_font", Brand.font) note.add_theme_font_size_override("normal_font_size", 15) @@ -226,12 +246,14 @@ func _add_mode_card( var badge_accent := Brand.SUCCESS if enabled else accent var card := PanelContainer.new() card.custom_minimum_size = Vector2(430, 210) + card.set_meta("medmind_wide_minimum", Vector2(430, 210)) card.size_flags_horizontal = Control.SIZE_EXPAND_FILL card.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent)) card.set_meta("medmind_role", "mode_card") card.set_meta("medmind_enabled", enabled) card.set_meta("medmind_accent", accent) themed_controls.append(card) + responsive_cards.append(card) mode_grid.add_child(card) var content := VBoxContainer.new() @@ -239,10 +261,12 @@ func _add_mode_card( content.add_theme_constant_override("separation", 14) card.add_child(content) - var header := HBoxContainer.new() + var header := BoxContainer.new() + header.vertical = false header.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_theme_constant_override("separation", 20) content.add_child(header) + responsive_card_rows.append(header) var icon_panel := PanelContainer.new() icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE icon_panel.custom_minimum_size = Vector2(72, 72) @@ -273,6 +297,7 @@ func _add_mode_card( header.add_child(details) var title := Label.new() title.text = title_text + title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART title.add_theme_font_override("font", Brand.font) title.add_theme_font_size_override("font_size", 22) details.add_child(title) @@ -286,7 +311,7 @@ func _add_mode_card( themed_controls.append(subtitle) details.add_child(subtitle) var badge := Label.new() - badge.text = "● %s" % badge_text + badge.text = badge_text badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN badge.add_theme_font_override("font", Brand.font) badge.add_theme_font_size_override("font_size", 13) @@ -299,10 +324,12 @@ func _add_mode_card( details.add_child(badge) content.add_child(HSeparator.new()) - var bottom := HBoxContainer.new() + var bottom := BoxContainer.new() + bottom.vertical = false bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE bottom.add_theme_constant_override("separation", 16) content.add_child(bottom) + responsive_card_rows.append(bottom) var description_label := Label.new() description_label.text = description description_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART @@ -404,11 +431,13 @@ func _style_back_bar(button: Button) -> void: func _add_exam_option(option_name: String, title_text: String, description: String, icon_name: String, available: bool) -> void: var card := PanelContainer.new() card.custom_minimum_size = Vector2(470, 178) + card.set_meta("medmind_wide_minimum", Vector2(470, 178)) card.size_flags_horizontal = Control.SIZE_EXPAND_FILL card.add_theme_stylebox_override("panel", _exam_card_box(available)) card.set_meta("medmind_role", "exam_card") card.set_meta("medmind_enabled", available) themed_controls.append(card) + responsive_cards.append(card) if available: exam_grid.add_child(card) else: @@ -418,10 +447,12 @@ func _add_exam_option(option_name: String, title_text: String, description: Stri content.mouse_filter = Control.MOUSE_FILTER_IGNORE content.add_theme_constant_override("separation", 10) card.add_child(content) - var header := HBoxContainer.new() + var header := BoxContainer.new() + header.vertical = false header.mouse_filter = Control.MOUSE_FILTER_IGNORE header.add_theme_constant_override("separation", 20) content.add_child(header) + responsive_card_rows.append(header) var icon_panel := PanelContainer.new() icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -645,7 +676,15 @@ func _start_question_bank() -> void: func _refresh_grid_columns() -> void: var column_count := 1 if size.x < 820.0 else 2 + var compact_cards := size.x < 520.0 if exam_top_row != null: exam_top_row.vertical = column_count == 1 if mode_grid != null: mode_grid.columns = column_count + for card in responsive_cards: + if is_instance_valid(card): + card.custom_minimum_size = Vector2(0, 0) if compact_cards else card.get_meta("medmind_wide_minimum", Vector2(430, 178)) + for row in responsive_card_rows: + if is_instance_valid(row): + row.vertical = compact_cards + row.alignment = BoxContainer.ALIGNMENT_BEGIN if compact_cards else BoxContainer.ALIGNMENT_CENTER diff --git a/tests/simulation_history_ui_test.gd.uid b/tests/simulation_history_ui_test.gd.uid new file mode 100644 index 0000000..c7794f8 --- /dev/null +++ b/tests/simulation_history_ui_test.gd.uid @@ -0,0 +1 @@ +uid://djp7hoxcwgydv diff --git a/tests/web_responsive_test.gd b/tests/web_responsive_test.gd new file mode 100644 index 0000000..86224d1 --- /dev/null +++ b/tests/web_responsive_test.gd @@ -0,0 +1,49 @@ +extends SceneTree + +var failures := 0 + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var host := Control.new() + host.size = Vector2(240, 720) + root.add_child(host) + var selector = load("res://scripts/ui/study_selector.gd").new() + selector.size = host.size + host.add_child(selector) + await process_frame + selector._show_stage("step3") + selector._refresh_grid_columns() + await process_frame + + _assert(selector.stage_scroll != null, "study selection should scroll vertically on short web viewports") + _assert(selector.stage_scroll.horizontal_scroll_mode == ScrollContainer.SCROLL_MODE_DISABLED, "study selection should never introduce horizontal scrolling") + _assert(selector.mode_grid.columns == 1, "phone-width mode cards should use one column") + _assert(selector.stage_content.get_combined_minimum_size().x <= host.size.x, "phone-width study content should fit inside the web canvas (minimum %.1f px)" % selector.stage_content.get_combined_minimum_size().x) + for card in selector.responsive_cards: + _assert(card.custom_minimum_size.x == 0.0, "phone-width cards should release their desktop minimum width") + for row in selector.responsive_card_rows: + _assert(row.vertical, "phone-width card content should stack instead of clipping") + + var main_screen = load("res://scripts/ui/main.gd").new() + main_screen.size = host.size + host.add_child(main_screen) + await process_frame + main_screen._update_responsive_layout() + _assert(main_screen.theme_button.custom_minimum_size == Vector2(44, 44), "the theme toggle should keep a stable square footprint") + _assert(main_screen.theme_button.size.x >= 44.0 and main_screen.theme_button.size.y >= 44.0, "the theme toggle should not collapse into an oval") + _assert(not main_screen.user_label.visible and not main_screen.account_chevron_button.visible, "nonessential account controls should collapse on phone-width headers") + + host.queue_free() + await process_frame + quit(1 if failures > 0 else 0) + + +func _assert(condition: bool, message: String) -> void: + if condition: + return + failures += 1 + push_error(message) diff --git a/tests/web_responsive_test.gd.uid b/tests/web_responsive_test.gd.uid new file mode 100644 index 0000000..82c859a --- /dev/null +++ b/tests/web_responsive_test.gd.uid @@ -0,0 +1 @@ +uid://5j5vyo8jl6st diff --git a/web/medmind_shell.html b/web/medmind_shell.html index d4fff43..4679122 100644 --- a/web/medmind_shell.html +++ b/web/medmind_shell.html @@ -173,32 +173,6 @@ width: 100%; } - .loader-logo-wrap::before, - .loader-logo-wrap::after { - background: #3154c4; - border-radius: 2px; - content: ""; - left: 50%; - position: absolute; - top: 50%; - transform: translate(-50%, -50%); - } - - .loader-logo-wrap::before { - height: 8%; - width: 19%; - } - - .loader-logo-wrap::after { - height: 19%; - width: 8%; - } - - :root[data-theme="dark"] .loader-logo-wrap::before, - :root[data-theme="dark"] .loader-logo-wrap::after { - background: #ffffff; - } - .loader-wordmark { font-size: clamp(52px, 7vw, 78px); font-weight: 650;