Fix narrow web layouts and loader branding

This commit is contained in:
hz
2026-07-16 23:07:26 -05:00
parent a778a680e0
commit ae283c2243
6 changed files with 110 additions and 34 deletions
+14 -2
View File
@@ -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)
+45 -6
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
uid://djp7hoxcwgydv
+49
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
uid://5j5vyo8jl6st
-26
View File
@@ -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;