diff --git a/scripts/ui/login_screen.gd b/scripts/ui/login_screen.gd index 432f99f..8670cb8 100644 --- a/scripts/ui/login_screen.gd +++ b/scripts/ui/login_screen.gd @@ -25,7 +25,9 @@ var reset_view: VBoxContainer var steam_view: VBoxContainer var login_input: LineEdit var password_input: LineEdit -var password_reveal_button: Button +var password_reveal_entries: Array[Dictionary] = [] +var legal_consent_texts: Array[RichTextLabel] = [] +var legal_consent_squares: Array[Panel] = [] var registration_username_input: LineEdit var registration_email_input: LineEdit var registration_password_input: LineEdit @@ -124,8 +126,8 @@ func _build_ui() -> void: page_margin.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) page_margin.add_theme_constant_override("margin_left", 52) page_margin.add_theme_constant_override("margin_right", 52) - page_margin.add_theme_constant_override("margin_top", 56) - page_margin.add_theme_constant_override("margin_bottom", 56) + page_margin.add_theme_constant_override("margin_top", 28) + page_margin.add_theme_constant_override("margin_bottom", 28) add_child(page_margin) var columns := HBoxContainer.new() @@ -189,23 +191,34 @@ func _build_ui() -> void: panel_center.size_flags_vertical = Control.SIZE_EXPAND_FILL columns.add_child(panel_center) auth_panel = PanelContainer.new() - auth_panel.custom_minimum_size = Vector2(710, 820) + auth_panel.custom_minimum_size = Vector2(710, 1000) panel_center.add_child(auth_panel) var scroll := ScrollContainer.new() scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED auth_panel.add_child(scroll) panel_gutter = MarginContainer.new() panel_gutter.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel_gutter.size_flags_vertical = Control.SIZE_EXPAND_FILL panel_gutter.add_theme_constant_override("margin_right", 28) scroll.add_child(panel_gutter) panel_content = VBoxContainer.new() panel_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL + panel_content.size_flags_vertical = Control.SIZE_EXPAND_FILL panel_content.add_theme_constant_override("separation", 24) panel_gutter.add_child(panel_content) + # main_column holds everything except the legal footer and is the part + # that should center vertically when a view (e.g. login) is shorter than + # the panel - the footer stays pinned to the very bottom either way. + var main_column := VBoxContainer.new() + main_column.size_flags_vertical = Control.SIZE_EXPAND_FILL + main_column.alignment = BoxContainer.ALIGNMENT_CENTER + main_column.add_theme_constant_override("separation", 16) + panel_content.add_child(main_column) + var panel_header := HBoxContainer.new() panel_header.add_theme_constant_override("separation", 18) - panel_content.add_child(panel_header) + main_column.add_child(panel_header) var small_icon := TextureRect.new() small_icon.texture = Brand.ranaze_mark_texture() small_icon.custom_minimum_size = Vector2(72, 72) @@ -229,14 +242,14 @@ func _build_ui() -> void: reset_view = _build_reset_view() steam_view = _build_steam_view() for view in [login_view, register_view, reset_view, steam_view]: - panel_content.add_child(view) + main_column.add_child(view) message_label = Label.new() message_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART message_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER message_label.add_theme_font_size_override("font_size", 14) message_label.add_theme_font_override("font", Brand.font) - panel_content.add_child(message_label) + main_column.add_child(message_label) var legal_footer := HBoxContainer.new() legal_footer.alignment = BoxContainer.ALIGNMENT_CENTER legal_footer.add_theme_constant_override("separation", 12) @@ -284,7 +297,8 @@ func _build_register_view() -> VBoxContainer: registration_email_input = _add_field(view, "Email", "name@example.com") registration_password_input = _add_field(view, "Password", "At least 8 characters", true) registration_confirm_input = _add_field(view, "Confirm password", "Re-enter your password", true) - _add_show_password(view, [registration_password_input, registration_confirm_input]) + _add_password_reveal(registration_password_input) + _add_password_reveal(registration_confirm_input) registration_legal_check = _add_legal_consent(view) _add_primary_button(view, "Create Account", _on_register_pressed) _add_back_button(view, "Already have an account? Sign in", "login") @@ -302,7 +316,8 @@ func _build_reset_view() -> VBoxContainer: reset_code_input.max_length = 6 reset_password_input = _add_field(reset_confirm_group, "New password", "At least 8 characters", true) reset_confirm_input = _add_field(reset_confirm_group, "Confirm new password", "Re-enter your new password", true) - _add_show_password(reset_confirm_group, [reset_password_input, reset_confirm_input]) + _add_password_reveal(reset_password_input) + _add_password_reveal(reset_confirm_input) _add_primary_button(reset_confirm_group, "Update Password", _confirm_reset) _add_back_button(view, "Back to sign in", "login") reset_confirm_group.visible = false @@ -323,7 +338,7 @@ func _build_steam_view() -> VBoxContainer: steam_username_input = _add_field(view, "Username or email", "Choose a username") steam_email_input = _add_field(view, "Email", "name@example.com") steam_password_input = _add_field(view, "Password", "At least 8 characters", true) - _add_show_password(view, [steam_password_input]) + _add_password_reveal(steam_password_input) steam_legal_check = _add_legal_consent(view) _add_primary_button(view, "Continue", _complete_steam) _add_back_button(view, "Cancel external sign in", "login") @@ -331,17 +346,84 @@ func _build_steam_view() -> VBoxContainer: func _add_legal_consent(parent: VBoxContainer) -> CheckBox: + # A bare CheckBox's own word-wrap left-aligns every wrapped line flush + # with the checkbox glyph, so a second line reads as un-indented. Split + # the toggle from its text into a row instead - the RichTextLabel's own + # wrapping then stays confined to its column, and "Terms of Service"/ + # "Privacy Policy" render as inline links rather than a duplicate row + # of link buttons underneath. + # + # The actual toggle state lives on a real CheckBox (kept invisible and + # out of the row's layout) because other code reads .button_pressed - + # but Button/CheckBox reserves vertical space for a text baseline even + # with an empty string, which was making the visible square non-square + # no matter what custom_minimum_size said. Drawing the visible square + # as a plain Panel sidesteps that entirely. var check := CheckBox.new() - check.text = "I agree to the Terms of Service and acknowledge the Privacy Policy." - check.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + check.visible = false parent.add_child(check) - var links := HBoxContainer.new() - links.add_theme_constant_override("separation", 12) - parent.add_child(links) - _add_legal_links(links) + + var margin := MarginContainer.new() + margin.add_theme_constant_override("margin_top", 8) + margin.add_theme_constant_override("margin_bottom", 8) + parent.add_child(margin) + var center := CenterContainer.new() + margin.add_child(center) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 12) + center.add_child(row) + + var square := Panel.new() + square.custom_minimum_size = Vector2(20, 20) + square.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN + square.size_flags_vertical = Control.SIZE_SHRINK_CENTER + square.mouse_filter = Control.MOUSE_FILTER_STOP + square.set_meta("legal_consent_checkbox", check) + square.gui_input.connect(func(event: InputEvent): + if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: + check.button_pressed = not check.button_pressed + ) + row.add_child(square) + legal_consent_squares.append(square) + check.toggled.connect(func(_pressed: bool): _refresh_legal_consent_square(square)) + + var consent_text := RichTextLabel.new() + consent_text.bbcode_enabled = true + consent_text.fit_content = true + consent_text.scroll_active = false + consent_text.meta_underlined = false + consent_text.custom_minimum_size = Vector2(480, 0) + consent_text.size_flags_vertical = Control.SIZE_SHRINK_CENTER + consent_text.add_theme_font_override("normal_font", Brand.font) + consent_text.add_theme_font_size_override("normal_font_size", 13) + consent_text.meta_clicked.connect(func(meta: Variant): _open_legal_document(str(meta))) + legal_consent_texts.append(consent_text) + row.add_child(consent_text) return check +func _refresh_legal_consent_text(consent_text: RichTextLabel, dark: bool) -> void: + var text_hex := Brand.text_color(dark).to_html(false) + var link_hex := (Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE).to_html(false) + consent_text.text = ( + "[color=#%s]I agree to the [/color][url=terms][color=#%s]Terms of Service[/color][/url][color=#%s] and acknowledge the [/color][url=privacy][color=#%s]Privacy Policy[/color][/url][color=#%s].[/color]" + % [text_hex, link_hex, text_hex, link_hex, text_hex] + ) + + +func _refresh_legal_consent_square(square: Panel) -> void: + var dark := UIThemeService.is_dark_mode() + var accent := Brand.SKY_BLUE if dark else Brand.ROYAL_BLUE + var checked := square.get_meta("legal_consent_checkbox", null) as CheckBox + var is_checked := checked != null and checked.button_pressed + var box := StyleBoxFlat.new() + box.bg_color = accent if is_checked else Color.TRANSPARENT + box.border_color = accent + box.set_border_width_all(2) + box.set_corner_radius_all(5) + square.add_theme_stylebox_override("panel", box) + + func _add_legal_links(parent: HBoxContainer) -> void: for spec in [["Terms of Service", "terms"], ["Privacy Policy", "privacy"]]: var link := Button.new() @@ -357,7 +439,7 @@ func _open_legal_document(kind: String) -> void: func _new_form() -> VBoxContainer: var form := VBoxContainer.new() - form.add_theme_constant_override("separation", 16) + form.add_theme_constant_override("separation", 10) return form @@ -388,7 +470,7 @@ func _add_field(parent: VBoxContainer, label_text: String, placeholder: String, input.secret = secret input.caret_blink = true input.caret_blink_interval = 0.5 - input.custom_minimum_size = Vector2(0, 68) + input.custom_minimum_size = Vector2(0, 56) input.add_theme_font_size_override("font_size", 17) input.add_theme_font_override("font", Brand.font) parent.add_child(input) @@ -396,19 +478,9 @@ func _add_field(parent: VBoxContainer, label_text: String, placeholder: String, return input -func _add_show_password(parent: VBoxContainer, targets: Array) -> void: - var toggle := CheckButton.new() - toggle.text = "Show password" - toggle.toggled.connect(func(should_reveal: bool): - for target in targets: - target.secret = not should_reveal - ) - parent.add_child(toggle) - - func _add_password_reveal(input: LineEdit) -> void: var reveal := Button.new() - password_reveal_button = reveal + password_reveal_entries.append({"input": input, "button": reveal}) reveal.text = "" reveal.tooltip_text = "Show password" reveal.flat = true @@ -487,6 +559,9 @@ func _update_responsive_layout() -> void: var layout_width := size.x if layout_width <= 0.0: layout_width = get_viewport_rect().size.x + var layout_height := size.y + if layout_height <= 0.0: + layout_height = get_viewport_rect().size.y var compact_desktop := layout_width < 900.0 var mobile_web := false if OS.has_feature("web"): @@ -505,7 +580,16 @@ func _update_responsive_layout() -> void: theme_button.custom_minimum_size = Vector2(44, 44) theme_button.position = Vector2(-58, 14) theme_button.add_theme_font_size_override("font_size", 20) - auth_panel.custom_minimum_size = Vector2(640, 790) if compact_desktop else Vector2(710, 820) + # 1000/1010 comfortably fits every view (login/register/reset/steam) + # without the inner ScrollContainer kicking in, on any window tall + # enough to hold it - but clamp to whatever height is actually + # available so a short window shrinks the panel (and lets the + # ScrollContainer take over) instead of the panel overflowing off + # the top/bottom of the screen. + var available_panel_height := layout_height - 56.0 + var target_height := 1010.0 if compact_desktop else 1000.0 + var panel_height := clampf(available_panel_height, 400.0, target_height) + auth_panel.custom_minimum_size = Vector2(640 if compact_desktop else 710, panel_height) panel_gutter.add_theme_constant_override("margin_right", 28) _apply_compact_scale(panel_content, 1.0) _apply_theme() @@ -544,7 +628,13 @@ func _apply_theme() -> void: label.add_theme_color_override("font_color", Brand.text_color(dark)) for input in fields: _style_auth_field(input, dark) + for consent_text in legal_consent_texts: + _refresh_legal_consent_text(consent_text, dark) + for square in legal_consent_squares: + _refresh_legal_consent_square(square) for button in find_children("*", "Button", true, false): + if button is CheckBox: + continue var typed := button as Button typed.add_theme_font_override("font", Brand.font) typed.add_theme_color_override("font_color", Brand.text_color(dark)) @@ -565,8 +655,8 @@ func _apply_theme() -> void: theme_button.add_theme_stylebox_override("normal", theme_normal) theme_button.add_theme_stylebox_override("hover", theme_normal) theme_button.add_theme_color_override("font_color", Brand.AMBER) - if password_reveal_button != null: - _refresh_password_reveal(password_input, password_reveal_button) + for entry in password_reveal_entries: + _refresh_password_reveal(entry["input"], entry["button"]) subheading.add_theme_color_override("font_color", Brand.muted_color(dark)) message_label.add_theme_color_override("font_color", Brand.muted_color(dark)) if brand_tagline != null: diff --git a/scripts/ui/question_player.gd b/scripts/ui/question_player.gd index 0067286..86dad0d 100644 --- a/scripts/ui/question_player.gd +++ b/scripts/ui/question_player.gd @@ -2824,10 +2824,10 @@ func _build_report_modal() -> void: report_form_content.add_theme_constant_override("separation", 12) modal_body.add_child(report_form_content) - var category_label := Label.new() - category_label.text = "What is the problem?" - _set_scaled_label(category_label, 18, Style.CYAN) - report_form_content.add_child(category_label) + var report_category_label := Label.new() + report_category_label.text = "What is the problem?" + _set_scaled_label(report_category_label, 18, Style.CYAN) + report_form_content.add_child(report_category_label) var category_grid := GridContainer.new() category_grid.columns = 2 diff --git a/web/medmind_shell.html b/web/medmind_shell.html index 257f86c..345dbfe 100644 --- a/web/medmind_shell.html +++ b/web/medmind_shell.html @@ -180,6 +180,8 @@ letter-spacing: 0; line-height: 1; margin: 12px 0 0; + text-align: center; + width: 100%; } .wordmark-rana { @@ -207,7 +209,9 @@ font-size: clamp(20px, 2.3vw, 28px); font-weight: 430; letter-spacing: 0; + line-height: 1.3; margin: 17px 0 clamp(36px, 6vh, 54px); + width: 100%; } .progress-row { @@ -239,6 +243,7 @@ font-size: 22px; font-variant-numeric: tabular-nums; font-weight: 650; + line-height: 1.3; min-width: 60px; } @@ -246,8 +251,10 @@ color: var(--muted); font-size: 19px; letter-spacing: 0; + line-height: 1.3; margin: 24px 0 0; text-align: center; + width: 100%; } #status-notice {