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)
|
_apply_screen_margins(current_screen_name)
|
||||||
if header_stats != null:
|
if header_stats != null:
|
||||||
header_stats.visible = size.x >= 720.0
|
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:
|
if drawer_open and not compact_shell:
|
||||||
_close_drawer()
|
_close_drawer()
|
||||||
if app_menu != null:
|
if app_menu != null:
|
||||||
@@ -185,7 +191,9 @@ func _build_ui() -> void:
|
|||||||
theme_button = Button.new()
|
theme_button = Button.new()
|
||||||
theme_button.tooltip_text = "Switch color theme"
|
theme_button.tooltip_text = "Switch color theme"
|
||||||
theme_button.custom_minimum_size = Vector2(44, 44)
|
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.size_flags_vertical = Control.SIZE_SHRINK_CENTER
|
||||||
|
theme_button.focus_mode = Control.FOCUS_NONE
|
||||||
theme_button.pressed.connect(theme_service.toggle_theme)
|
theme_button.pressed.connect(theme_service.toggle_theme)
|
||||||
_style_shell_theme_button(theme_button)
|
_style_shell_theme_button(theme_button)
|
||||||
header.add_child(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.bg_color = Color("111b30") if dark else Brand.LIGHT_SURFACE
|
||||||
normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER
|
normal.border_color = Color("33415d") if dark else Brand.LIGHT_BORDER
|
||||||
normal.set_border_width_all(1)
|
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
|
var hover := normal.duplicate() as StyleBoxFlat
|
||||||
hover.border_color = Brand.AMBER
|
hover.border_color = Brand.AMBER
|
||||||
button.add_theme_stylebox_override("normal", normal)
|
button.add_theme_stylebox_override("normal", normal)
|
||||||
@@ -628,7 +640,7 @@ func _refresh_header_stats() -> void:
|
|||||||
if account_button != null:
|
if account_button != null:
|
||||||
account_button.text = _account_initials(AuthState.username)
|
account_button.text = _account_initials(AuthState.username)
|
||||||
if feedback_button != null:
|
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.icon = preload("res://assets/icons/lucide/message-square.svg")
|
||||||
feedback_button.add_theme_constant_override("icon_max_width", 21)
|
feedback_button.add_theme_constant_override("icon_max_width", 21)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ signal stage_changed(stage_name: String)
|
|||||||
var main: Control
|
var main: Control
|
||||||
var exam_view: VBoxContainer
|
var exam_view: VBoxContainer
|
||||||
var mode_view: VBoxContainer
|
var mode_view: VBoxContainer
|
||||||
|
var stage_scroll: ScrollContainer
|
||||||
|
var stage_content: VBoxContainer
|
||||||
var exam_grid: VBoxContainer
|
var exam_grid: VBoxContainer
|
||||||
var exam_top_row: BoxContainer
|
var exam_top_row: BoxContainer
|
||||||
var mode_grid: GridContainer
|
var mode_grid: GridContainer
|
||||||
@@ -21,6 +23,8 @@ var pending_ccs_launch := false
|
|||||||
var current_stage := "exam"
|
var current_stage := "exam"
|
||||||
var pending_question_bank_launch := false
|
var pending_question_bank_launch := false
|
||||||
var themed_controls: Array[Control] = []
|
var themed_controls: Array[Control] = []
|
||||||
|
var responsive_cards: Array[PanelContainer] = []
|
||||||
|
var responsive_card_rows: Array[BoxContainer] = []
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -36,6 +40,15 @@ func _ready() -> void:
|
|||||||
if not CCSService.request_failed.is_connected(_on_ccs_failed):
|
if not CCSService.request_failed.is_connected(_on_ccs_failed):
|
||||||
CCSService.request_failed.connect(_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_exam_view()
|
||||||
_build_mode_view()
|
_build_mode_view()
|
||||||
_show_stage("exam")
|
_show_stage("exam")
|
||||||
@@ -58,7 +71,7 @@ func _build_exam_view() -> void:
|
|||||||
exam_view = VBoxContainer.new()
|
exam_view = VBoxContainer.new()
|
||||||
exam_view.add_theme_constant_override("separation", 18)
|
exam_view.add_theme_constant_override("separation", 18)
|
||||||
exam_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
exam_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
add_child(exam_view)
|
stage_content.add_child(exam_view)
|
||||||
|
|
||||||
var eyebrow := Label.new()
|
var eyebrow := Label.new()
|
||||||
eyebrow.text = "GET STARTED"
|
eyebrow.text = "GET STARTED"
|
||||||
@@ -69,6 +82,8 @@ func _build_exam_view() -> void:
|
|||||||
|
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = "What are you preparing for?"
|
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_override("font", Brand.font)
|
||||||
title.add_theme_font_size_override("font_size", 34)
|
title.add_theme_font_size_override("font_size", 34)
|
||||||
exam_view.add_child(title)
|
exam_view.add_child(title)
|
||||||
@@ -102,7 +117,7 @@ func _build_mode_view() -> void:
|
|||||||
mode_view = VBoxContainer.new()
|
mode_view = VBoxContainer.new()
|
||||||
mode_view.add_theme_constant_override("separation", 16)
|
mode_view.add_theme_constant_override("separation", 16)
|
||||||
mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
mode_view.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
add_child(mode_view)
|
stage_content.add_child(mode_view)
|
||||||
|
|
||||||
var back_button := Button.new()
|
var back_button := Button.new()
|
||||||
back_button.text = "< All exams"
|
back_button.text = "< All exams"
|
||||||
@@ -125,6 +140,8 @@ func _build_mode_view() -> void:
|
|||||||
|
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = "Choose a study mode"
|
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_override("font", Brand.font)
|
||||||
title.add_theme_font_size_override("font_size", 34)
|
title.add_theme_font_size_override("font_size", 34)
|
||||||
mode_view.add_child(title)
|
mode_view.add_child(title)
|
||||||
@@ -201,6 +218,9 @@ func _build_mode_view() -> void:
|
|||||||
note.bbcode_enabled = true
|
note.bbcode_enabled = true
|
||||||
note.fit_content = true
|
note.fit_content = true
|
||||||
note.scroll_active = false
|
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.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_override("normal_font", Brand.font)
|
||||||
note.add_theme_font_size_override("normal_font_size", 15)
|
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 badge_accent := Brand.SUCCESS if enabled else accent
|
||||||
var card := PanelContainer.new()
|
var card := PanelContainer.new()
|
||||||
card.custom_minimum_size = Vector2(430, 210)
|
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.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
card.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent))
|
card.add_theme_stylebox_override("panel", _mode_card_box(enabled, accent))
|
||||||
card.set_meta("medmind_role", "mode_card")
|
card.set_meta("medmind_role", "mode_card")
|
||||||
card.set_meta("medmind_enabled", enabled)
|
card.set_meta("medmind_enabled", enabled)
|
||||||
card.set_meta("medmind_accent", accent)
|
card.set_meta("medmind_accent", accent)
|
||||||
themed_controls.append(card)
|
themed_controls.append(card)
|
||||||
|
responsive_cards.append(card)
|
||||||
mode_grid.add_child(card)
|
mode_grid.add_child(card)
|
||||||
|
|
||||||
var content := VBoxContainer.new()
|
var content := VBoxContainer.new()
|
||||||
@@ -239,10 +261,12 @@ func _add_mode_card(
|
|||||||
content.add_theme_constant_override("separation", 14)
|
content.add_theme_constant_override("separation", 14)
|
||||||
card.add_child(content)
|
card.add_child(content)
|
||||||
|
|
||||||
var header := HBoxContainer.new()
|
var header := BoxContainer.new()
|
||||||
|
header.vertical = false
|
||||||
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
header.add_theme_constant_override("separation", 20)
|
header.add_theme_constant_override("separation", 20)
|
||||||
content.add_child(header)
|
content.add_child(header)
|
||||||
|
responsive_card_rows.append(header)
|
||||||
var icon_panel := PanelContainer.new()
|
var icon_panel := PanelContainer.new()
|
||||||
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
icon_panel.custom_minimum_size = Vector2(72, 72)
|
icon_panel.custom_minimum_size = Vector2(72, 72)
|
||||||
@@ -273,6 +297,7 @@ func _add_mode_card(
|
|||||||
header.add_child(details)
|
header.add_child(details)
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = title_text
|
title.text = title_text
|
||||||
|
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
title.add_theme_font_override("font", Brand.font)
|
title.add_theme_font_override("font", Brand.font)
|
||||||
title.add_theme_font_size_override("font_size", 22)
|
title.add_theme_font_size_override("font_size", 22)
|
||||||
details.add_child(title)
|
details.add_child(title)
|
||||||
@@ -286,7 +311,7 @@ func _add_mode_card(
|
|||||||
themed_controls.append(subtitle)
|
themed_controls.append(subtitle)
|
||||||
details.add_child(subtitle)
|
details.add_child(subtitle)
|
||||||
var badge := Label.new()
|
var badge := Label.new()
|
||||||
badge.text = "● %s" % badge_text
|
badge.text = badge_text
|
||||||
badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
badge.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||||
badge.add_theme_font_override("font", Brand.font)
|
badge.add_theme_font_override("font", Brand.font)
|
||||||
badge.add_theme_font_size_override("font_size", 13)
|
badge.add_theme_font_size_override("font_size", 13)
|
||||||
@@ -299,10 +324,12 @@ func _add_mode_card(
|
|||||||
details.add_child(badge)
|
details.add_child(badge)
|
||||||
|
|
||||||
content.add_child(HSeparator.new())
|
content.add_child(HSeparator.new())
|
||||||
var bottom := HBoxContainer.new()
|
var bottom := BoxContainer.new()
|
||||||
|
bottom.vertical = false
|
||||||
bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
bottom.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
bottom.add_theme_constant_override("separation", 16)
|
bottom.add_theme_constant_override("separation", 16)
|
||||||
content.add_child(bottom)
|
content.add_child(bottom)
|
||||||
|
responsive_card_rows.append(bottom)
|
||||||
var description_label := Label.new()
|
var description_label := Label.new()
|
||||||
description_label.text = description
|
description_label.text = description
|
||||||
description_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
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:
|
func _add_exam_option(option_name: String, title_text: String, description: String, icon_name: String, available: bool) -> void:
|
||||||
var card := PanelContainer.new()
|
var card := PanelContainer.new()
|
||||||
card.custom_minimum_size = Vector2(470, 178)
|
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.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
card.add_theme_stylebox_override("panel", _exam_card_box(available))
|
card.add_theme_stylebox_override("panel", _exam_card_box(available))
|
||||||
card.set_meta("medmind_role", "exam_card")
|
card.set_meta("medmind_role", "exam_card")
|
||||||
card.set_meta("medmind_enabled", available)
|
card.set_meta("medmind_enabled", available)
|
||||||
themed_controls.append(card)
|
themed_controls.append(card)
|
||||||
|
responsive_cards.append(card)
|
||||||
if available:
|
if available:
|
||||||
exam_grid.add_child(card)
|
exam_grid.add_child(card)
|
||||||
else:
|
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.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
content.add_theme_constant_override("separation", 10)
|
content.add_theme_constant_override("separation", 10)
|
||||||
card.add_child(content)
|
card.add_child(content)
|
||||||
var header := HBoxContainer.new()
|
var header := BoxContainer.new()
|
||||||
|
header.vertical = false
|
||||||
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
header.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
header.add_theme_constant_override("separation", 20)
|
header.add_theme_constant_override("separation", 20)
|
||||||
content.add_child(header)
|
content.add_child(header)
|
||||||
|
responsive_card_rows.append(header)
|
||||||
|
|
||||||
var icon_panel := PanelContainer.new()
|
var icon_panel := PanelContainer.new()
|
||||||
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
icon_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
@@ -645,7 +676,15 @@ func _start_question_bank() -> void:
|
|||||||
|
|
||||||
func _refresh_grid_columns() -> void:
|
func _refresh_grid_columns() -> void:
|
||||||
var column_count := 1 if size.x < 820.0 else 2
|
var column_count := 1 if size.x < 820.0 else 2
|
||||||
|
var compact_cards := size.x < 520.0
|
||||||
if exam_top_row != null:
|
if exam_top_row != null:
|
||||||
exam_top_row.vertical = column_count == 1
|
exam_top_row.vertical = column_count == 1
|
||||||
if mode_grid != null:
|
if mode_grid != null:
|
||||||
mode_grid.columns = column_count
|
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%;
|
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 {
|
.loader-wordmark {
|
||||||
font-size: clamp(52px, 7vw, 78px);
|
font-size: clamp(52px, 7vw, 78px);
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
|
|||||||
Reference in New Issue
Block a user