Fix narrow web layouts and loader branding
This commit is contained in:
+14
-2
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://djp7hoxcwgydv
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://5j5vyo8jl6st
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user