Files
step3/scripts/ui/ccs_hub.gd
T

699 lines
26 KiB
GDScript

extends VBoxContainer
const Style := preload("res://scripts/ui/ui_style.gd")
const Brand := preload("res://scripts/ui/medmind_style.gd")
const CASE_ICON := preload("res://assets/icons/lucide/stethoscope.svg")
const REVIEW_ICON := preload("res://assets/icons/lucide/chart-no-axes-column-increasing.svg")
var main: Control
var page: VBoxContainer
var active_section: PanelContainer
var active_content: VBoxContainer
var history_section: PanelContainer
var history_header: BoxContainer
var table_header: HBoxContainer
var history_list: VBoxContainer
var status_label: Label
var load_more_button: Button
var filter_option: OptionButton
var sort_option: OptionButton
var abandon_dialog: ConfirmationDialog
var pending_action := ""
var pending_attempt_id := ""
var history_status := "all"
var history_sort := "newest"
var compact_layout := false
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
size_flags_horizontal = Control.SIZE_EXPAND_FILL
size_flags_vertical = Control.SIZE_EXPAND_FILL
add_theme_constant_override("separation", 0)
_build_ui()
_connect_services()
resized.connect(_update_responsive_layout)
UIThemeService.theme_changed.connect(func(_dark_mode: bool): refresh_theme_controls())
call_deferred("_update_responsive_layout")
func _build_ui() -> void:
var scroll := ScrollContainer.new()
scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
add_child(scroll)
page = VBoxContainer.new()
page.size_flags_horizontal = Control.SIZE_EXPAND_FILL
page.add_theme_constant_override("separation", 18)
scroll.add_child(page)
active_section = PanelContainer.new()
active_section.name = "ActiveCcsSection"
page.add_child(active_section)
active_content = VBoxContainer.new()
active_content.add_theme_constant_override("separation", 10)
active_section.add_child(active_content)
history_section = PanelContainer.new()
history_section.name = "CcsHistorySection"
history_section.size_flags_vertical = Control.SIZE_EXPAND_FILL
page.add_child(history_section)
var history_content := VBoxContainer.new()
history_content.add_theme_constant_override("separation", 12)
history_section.add_child(history_content)
history_header = BoxContainer.new()
history_header.add_theme_constant_override("separation", 10)
history_content.add_child(history_header)
var heading := Label.new()
heading.text = "Case History"
heading.size_flags_horizontal = Control.SIZE_EXPAND_FILL
Brand.apply_font(heading)
heading.add_theme_font_size_override("font_size", 21)
history_header.add_child(heading)
filter_option = OptionButton.new()
filter_option.name = "HistoryFilter"
filter_option.custom_minimum_size = Vector2(175, 42)
filter_option.add_item("All Cases")
filter_option.set_item_metadata(0, "all")
filter_option.add_item("Completed")
filter_option.set_item_metadata(1, "completed")
filter_option.add_item("Abandoned")
filter_option.set_item_metadata(2, "abandoned")
filter_option.item_selected.connect(_on_filter_selected)
history_header.add_child(filter_option)
sort_option = OptionButton.new()
sort_option.name = "HistorySort"
sort_option.custom_minimum_size = Vector2(165, 42)
sort_option.add_item("Newest First")
sort_option.set_item_metadata(0, "newest")
sort_option.add_item("Oldest First")
sort_option.set_item_metadata(1, "oldest")
sort_option.item_selected.connect(_on_sort_selected)
history_header.add_child(sort_option)
status_label = Label.new()
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Brand.apply_font(status_label)
status_label.add_theme_font_size_override("font_size", 15)
history_content.add_child(status_label)
table_header = _build_table_header()
history_content.add_child(table_header)
history_list = VBoxContainer.new()
history_list.add_theme_constant_override("separation", 0)
history_content.add_child(history_list)
load_more_button = Button.new()
load_more_button.text = "Show more"
load_more_button.visible = false
load_more_button.custom_minimum_size = Vector2(150, 42)
load_more_button.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
load_more_button.pressed.connect(_load_more)
history_content.add_child(load_more_button)
abandon_dialog = ConfirmationDialog.new()
abandon_dialog.title = "Abandon case?"
abandon_dialog.dialog_text = "This case cannot be resumed and will not receive a score. It will remain visible in your case history."
abandon_dialog.ok_button_text = "Abandon Case"
abandon_dialog.cancel_button_text = "Keep Case"
abandon_dialog.confirmed.connect(_confirm_abandon)
add_child(abandon_dialog)
refresh_theme_controls()
func _connect_services() -> void:
if not CCSService.cases_changed.is_connected(_render_active_section):
CCSService.cases_changed.connect(_render_active_section)
if not CCSService.attempt_changed.is_connected(_on_attempt_changed):
CCSService.attempt_changed.connect(_on_attempt_changed)
if not CCSService.attempt_abandoned.is_connected(_on_attempt_abandoned):
CCSService.attempt_abandoned.connect(_on_attempt_abandoned)
if not CCSService.history_changed.is_connected(_render_history):
CCSService.history_changed.connect(_render_history)
if not CCSService.prediction_changed.is_connected(_render_active_section):
CCSService.prediction_changed.connect(_render_active_section)
if not CCSService.rating_changed.is_connected(_render_active_section):
CCSService.rating_changed.connect(_render_active_section)
if not CCSService.request_failed.is_connected(_on_request_failed):
CCSService.request_failed.connect(_on_request_failed)
func on_show(_payload: Variant = null) -> void:
pending_action = ""
pending_attempt_id = ""
status_label.text = "Loading case history..."
load_more_button.disabled = true
CCSService.load_cases()
CCSService.load_active()
CCSService.load_prediction()
CCSService.load_rating()
_load_history_page()
func refresh_theme_controls() -> void:
if page == null:
return
var dark := UIThemeService.is_dark_mode()
active_section.add_theme_stylebox_override("panel", _section_box(dark))
history_section.add_theme_stylebox_override("panel", _section_box(dark))
status_label.add_theme_color_override("font_color", Brand.muted_color(dark))
_style_option(filter_option, dark)
_style_option(sort_option, dark)
_style_outline_button(load_more_button, Brand.PURPLE, dark)
_render_active_section()
_render_history()
func _render_active_section() -> void:
if active_content == null:
return
_clear(active_content)
var dark := UIThemeService.is_dark_mode()
var has_active := has_active_case(CCSService.attempt)
var section_title := Label.new()
section_title.text = "Active Case" if has_active else "Timed CCS Case"
Brand.apply_font(section_title)
section_title.add_theme_font_size_override("font_size", 18)
section_title.add_theme_color_override("font_color", Color("9b73ff") if dark else Brand.PURPLE)
active_content.add_child(section_title)
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", _active_card_box(dark))
active_content.add_child(card)
var card_layout := BoxContainer.new()
card_layout.name = "ActiveCardLayout"
card_layout.vertical = compact_layout
card_layout.add_theme_constant_override("separation", 18)
card.add_child(card_layout)
var icon_panel := PanelContainer.new()
icon_panel.custom_minimum_size = Vector2(94, 94)
icon_panel.size_flags_vertical = Control.SIZE_SHRINK_CENTER
icon_panel.add_theme_stylebox_override("panel", Brand.icon_circle_box(dark, Brand.TEAL, 8))
card_layout.add_child(icon_panel)
var icon := TextureRect.new()
icon.texture = CASE_ICON
icon.custom_minimum_size = Vector2(44, 44)
icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
icon.modulate = Brand.TEAL
icon_panel.add_child(icon)
var details := VBoxContainer.new()
details.size_flags_horizontal = Control.SIZE_EXPAND_FILL
details.add_theme_constant_override("separation", 5)
card_layout.add_child(details)
var heading := Label.new()
heading.text = "Timed CCS Case"
Brand.apply_font(heading)
heading.add_theme_font_size_override("font_size", 19)
heading.add_theme_color_override("font_color", Brand.text_color(dark))
details.add_child(heading)
if has_active:
_build_active_details(details, dark)
else:
_build_start_details(details, dark)
var actions := VBoxContainer.new()
actions.name = "ActiveActions"
if not compact_layout:
actions.custom_minimum_size = Vector2(265, 0)
actions.size_flags_vertical = Control.SIZE_SHRINK_CENTER
actions.add_theme_constant_override("separation", 10)
card_layout.add_child(actions)
var primary := Button.new()
primary.name = "PrimaryCcsAction"
primary.text = "Resume Case" if has_active else "Start Case"
primary.custom_minimum_size = Vector2(0, 48)
primary.disabled = not pending_action.is_empty() or (not has_active and CCSService.cases.is_empty())
_style_gradient_button(primary)
primary.pressed.connect(_launch_or_resume)
actions.add_child(primary)
if has_active:
var abandon := Button.new()
abandon.name = "AbandonCase"
abandon.text = "Abandon Case"
abandon.custom_minimum_size = Vector2(0, 46)
_style_outline_button(abandon, Color("ff5f76"), dark)
abandon.pressed.connect(func(): abandon_dialog.popup_centered(Vector2i(520, 220)))
actions.add_child(abandon)
_build_performance_summary(dark)
var notice := PanelContainer.new()
notice.add_theme_stylebox_override("panel", _notice_box(dark))
active_content.add_child(notice)
var notice_label := Label.new()
notice_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
notice_label.text = "Abandoning ends this case without a score; the attempt remains in case history." if has_active else _availability_message()
_style_muted_label(notice_label, dark, 14)
notice.add_child(notice_label)
func _build_active_details(details: VBoxContainer, dark: bool) -> void:
var attempt: Dictionary = CCSService.attempt
var started := Label.new()
started.text = "Started: %s" % _format_local_datetime(str(attempt.get("createdUtc", "")))
_style_muted_label(started, dark, 14)
details.add_child(started)
var state := Label.new()
var state_text := "Final orders" if str(attempt.get("state", "")) == "final_orders" else "In progress"
state.text = "%s | %s | %s" % [state_text, humanize(str(attempt.get("location", ""))), remaining_time_text(attempt)]
_style_muted_label(state, dark, 14)
details.add_child(state)
func _build_start_details(details: VBoxContainer, dark: bool) -> void:
var copy := Label.new()
copy.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
copy.text = "Manage a randomly selected patient through focused examination, diagnostic orders, treatment, disposition, and simulated time."
_style_muted_label(copy, dark, 15)
details.add_child(copy)
func _build_performance_summary(dark: bool) -> void:
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _notice_box(dark))
active_content.add_child(panel)
var row := BoxContainer.new()
row.vertical = compact_layout
row.add_theme_constant_override("separation", 18)
panel.add_child(row)
var rating_value := "%d" % int(CCSService.rating.get("elo", 1000))
var rating_detail := "%d rated cases" % int(CCSService.rating.get("ratedCaseCount", 0))
_add_metric(row, "CCS Rating", rating_value, rating_detail, dark)
var ccs_score: Variant = CCSService.prediction.get("ccsScore", null)
var score_value := "--"
if ccs_score != null:
score_value = "%d%%" % int(round(float(ccs_score)))
_add_metric(row, "CCS Performance", score_value, "%d contributing cases" % int(CCSService.prediction.get("contributingCases", 0)), dark)
var combined: Variant = CCSService.prediction.get("combinedPredictedScore", null)
var prediction_value := "--"
var prediction_detail := "Complete a Day 1 simulation"
if combined != null:
prediction_value = "%d" % int(combined)
prediction_detail = "Range %d-%d" % [int(CCSService.prediction.get("rangeLow", 0)), int(CCSService.prediction.get("rangeHigh", 0))]
_add_metric(row, "Provisional Combined Estimate", prediction_value, prediction_detail, dark)
func _add_metric(parent: BoxContainer, caption: String, value: String, detail: String, dark: bool) -> void:
var column := VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
parent.add_child(column)
var caption_label := Label.new()
caption_label.text = caption
_style_muted_label(caption_label, dark, 12)
column.add_child(caption_label)
var value_label := Label.new()
value_label.text = value
Brand.apply_font(value_label)
value_label.add_theme_font_size_override("font_size", 22)
value_label.add_theme_color_override("font_color", Brand.text_color(dark))
column.add_child(value_label)
var detail_label := Label.new()
detail_label.text = detail
_style_muted_label(detail_label, dark, 12)
column.add_child(detail_label)
func _render_history() -> void:
if history_list == null or not visible:
return
_clear(history_list)
var items: Array = CCSService.history
status_label.text = "No cases match this filter." if items.is_empty() else ""
for raw_item in items:
var item: Dictionary = raw_item
history_list.add_child(_build_history_row(item))
load_more_button.visible = not CCSService.history_next_cursor.is_empty()
load_more_button.disabled = false
table_header.visible = not compact_layout and not items.is_empty()
func _build_table_header() -> HBoxContainer:
var dark := UIThemeService.is_dark_mode()
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 10)
for definition in [["Case", 2.0], ["Date", 1.15], ["Status", 1.0], ["Score", 0.8], ["CCS Rating", 1.0], ["Actions", 1.0]]:
var label := Label.new()
label.text = str(definition[0])
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.size_flags_stretch_ratio = float(definition[1])
Brand.apply_font(label)
label.add_theme_font_size_override("font_size", 13)
label.add_theme_color_override("font_color", Brand.muted_color(dark))
row.add_child(label)
return row
func _build_history_row(item: Dictionary) -> Control:
var dark := UIThemeService.is_dark_mode()
var panel := PanelContainer.new()
panel.add_theme_stylebox_override("panel", _row_box(dark))
var state := str(item.get("state", ""))
if compact_layout:
var card := VBoxContainer.new()
card.add_theme_constant_override("separation", 8)
panel.add_child(card)
var top := HBoxContainer.new()
card.add_child(top)
top.add_child(_case_cell(item, dark))
top.add_child(_status_badge(state, dark))
var metrics := Label.new()
metrics.text = "%s | %s | %s" % [_format_local_datetime(str(item.get("createdUtc", ""))).replace("\n", " "), _score_text(item), _rating_text(item)]
_style_muted_label(metrics, dark, 14)
card.add_child(metrics)
if state == "completed":
card.add_child(_review_button(item, dark))
return panel
var row := HBoxContainer.new()
row.custom_minimum_size = Vector2(0, 66)
row.add_theme_constant_override("separation", 10)
panel.add_child(row)
_add_table_cell(row, _case_cell(item, dark), 2.0)
_add_table_cell(row, _plain_cell(_format_local_datetime(str(item.get("createdUtc", ""))), dark), 1.15)
_add_table_cell(row, _status_badge(state, dark), 1.0)
_add_table_cell(row, _plain_cell(_score_text(item), dark), 0.8)
_add_table_cell(row, _plain_cell(_rating_text(item), dark), 1.0)
var action: Control = _plain_cell("-", dark)
if state == "completed":
action = _review_button(item, dark)
_add_table_cell(row, action, 1.0)
return panel
func _case_cell(item: Dictionary, dark: bool) -> Control:
var column := VBoxContainer.new()
column.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var title := Label.new()
title.text = str(item.get("caseTitle", "CCS Case"))
title.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Brand.apply_font(title)
title.add_theme_font_size_override("font_size", 15)
title.add_theme_color_override("font_color", Brand.text_color(dark))
column.add_child(title)
var mode := Label.new()
mode.text = "Legacy practice case" if str(item.get("mode", "timed")) == "practice" else "Timed case"
_style_muted_label(mode, dark, 12)
column.add_child(mode)
return column
func _plain_cell(value: String, dark: bool) -> Label:
var label := Label.new()
label.text = value
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
Brand.apply_font(label)
label.add_theme_font_size_override("font_size", 14)
label.add_theme_color_override("font_color", Brand.text_color(dark))
return label
func _status_badge(state: String, dark: bool) -> Label:
var label := Label.new()
var accent := Brand.SUCCESS if state == "completed" else Brand.SLATE
label.text = "Completed" if state == "completed" else "Abandoned"
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.size_flags_vertical = Control.SIZE_SHRINK_CENTER
Brand.apply_font(label)
label.add_theme_font_size_override("font_size", 12)
label.add_theme_color_override("font_color", accent if dark else accent.darkened(0.2))
label.add_theme_stylebox_override("normal", Brand.badge_box(dark, accent))
return label
func _review_button(item: Dictionary, dark: bool) -> Button:
var button := Button.new()
button.text = "Review"
button.icon = REVIEW_ICON
button.expand_icon = true
button.add_theme_constant_override("icon_max_width", 17)
button.custom_minimum_size = Vector2(105, 38)
button.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
_style_outline_button(button, Brand.PURPLE, dark)
button.pressed.connect(_open_review.bind(str(item.get("id", ""))))
return button
func _add_table_cell(row: HBoxContainer, control: Control, ratio: float) -> void:
control.size_flags_horizontal = Control.SIZE_EXPAND_FILL
control.size_flags_vertical = Control.SIZE_SHRINK_CENTER
control.size_flags_stretch_ratio = ratio
row.add_child(control)
func _launch_or_resume() -> void:
if not pending_action.is_empty():
return
pending_action = "resume" if has_active_case(CCSService.attempt) else "start"
_render_active_section()
if pending_action == "resume":
main.show_screen("ccs_player")
pending_action = ""
else:
CCSService.create_attempt("", "timed")
func _open_review(attempt_id: String) -> void:
if not pending_action.is_empty():
return
pending_action = "review"
pending_attempt_id = attempt_id
status_label.text = "Opening case review..."
CCSService.load_attempt(attempt_id)
func _confirm_abandon() -> void:
if not has_active_case(CCSService.attempt):
return
pending_action = "abandon"
CCSService.abandon()
_render_active_section()
func _on_attempt_changed() -> void:
if not visible:
return
if pending_action in ["start", "resume_after_conflict"] and has_active_case(CCSService.attempt):
pending_action = ""
main.show_screen("ccs_player")
return
if pending_action == "review" and str(CCSService.attempt.get("id", "")) == pending_attempt_id:
pending_action = ""
pending_attempt_id = ""
main.show_screen("ccs_review", CCSService.attempt)
return
pending_action = ""
_render_active_section()
func _on_attempt_abandoned() -> void:
if not visible:
return
pending_action = ""
CCSService.load_active()
CCSService.load_prediction()
CCSService.load_rating()
_load_history_page()
func _on_filter_selected(index: int) -> void:
history_status = str(filter_option.get_item_metadata(index))
_load_history_page()
func _on_sort_selected(index: int) -> void:
history_sort = str(sort_option.get_item_metadata(index))
_load_history_page()
func _load_history_page() -> void:
status_label.text = "Loading case history..."
load_more_button.disabled = true
CCSService.load_history("", history_status, history_sort)
func _load_more() -> void:
if CCSService.history_next_cursor.is_empty():
return
load_more_button.disabled = true
CCSService.load_history(CCSService.history_next_cursor, history_status, history_sort)
func _on_request_failed(message: String) -> void:
if not visible:
return
if pending_action == "start" and message.contains("409"):
pending_action = "resume_after_conflict"
status_label.text = "An active case was found. Resuming it..."
CCSService.load_active()
return
pending_action = ""
pending_attempt_id = ""
status_label.text = message
load_more_button.disabled = false
_render_active_section()
func _update_responsive_layout() -> void:
var use_compact := size.x < 940
if compact_layout == use_compact:
return
compact_layout = use_compact
history_header.vertical = compact_layout
filter_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL if compact_layout else Control.SIZE_SHRINK_END
sort_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL if compact_layout else Control.SIZE_SHRINK_END
_render_active_section()
_render_history()
func _availability_message() -> String:
if CCSService.cases.is_empty():
return "No published CCS cases are currently available."
return "%d published cases are available. A random case will begin immediately." % CCSService.cases.size()
func _score_text(item: Dictionary) -> String:
var score: Variant = item.get("score", null)
return "--" if score == null else "%d%%" % int(round(float(score)))
func _rating_text(item: Dictionary) -> String:
var rating_value: Variant = item.get("rating", null)
if typeof(rating_value) != TYPE_DICTIONARY:
return "--"
var rating: Dictionary = rating_value
if not bool(rating.get("rated", false)):
return "--"
return "%d (%+d)" % [int(rating.get("userElo", 1000)), int(rating.get("userEloDelta", 0))]
func _format_local_datetime(value: String) -> String:
if value.is_empty():
return "--"
var unix := Time.get_unix_time_from_datetime_string(value)
var timezone: Dictionary = Time.get_time_zone_from_system()
var local := Time.get_datetime_dict_from_unix_time(int(unix) + int(timezone.get("bias", 0)) * 60)
return format_datetime_parts(local)
static func format_datetime_parts(parts: Dictionary) -> String:
const MONTHS := ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
var month_index: int = clamp(int(parts.get("month", 1)) - 1, 0, 11)
var hour_24 := int(parts.get("hour", 0))
var suffix := "AM" if hour_24 < 12 else "PM"
var hour_12 := hour_24 % 12
if hour_12 == 0:
hour_12 = 12
return "%s %d, %d\n%d:%02d %s" % [MONTHS[month_index], int(parts.get("day", 1)), int(parts.get("year", 1970)), hour_12, int(parts.get("minute", 0)), suffix]
static func has_active_case(attempt: Dictionary) -> bool:
return not attempt.is_empty() and str(attempt.get("state", "")) in ["active", "final_orders"]
static func remaining_time_text(attempt: Dictionary, now_unix: float = -1.0) -> String:
var deadline := str(attempt.get("finalOrdersDeadlineUtc", "")) if str(attempt.get("state", "")) == "final_orders" else str(attempt.get("deadlineUtc", ""))
if deadline.is_empty() or deadline == "<null>":
return "Timed"
var current_unix := Time.get_unix_time_from_system() if now_unix < 0.0 else now_unix
var seconds: int = max(0, int(Time.get_unix_time_from_datetime_string(deadline) - current_unix))
var minutes := int(ceil(float(seconds) / 60.0))
return "%d min remaining" % minutes
static func humanize(value: String) -> String:
return value.replace("_", " ").capitalize()
func _style_muted_label(label: Label, dark: bool, font_size: int) -> void:
Brand.apply_font(label)
label.add_theme_font_size_override("font_size", font_size)
label.add_theme_color_override("font_color", Brand.muted_color(dark))
func _style_option(option: OptionButton, dark: bool) -> void:
Brand.apply_font(option)
option.add_theme_font_size_override("font_size", 14)
option.add_theme_color_override("font_color", Brand.text_color(dark))
for state in ["normal", "hover", "pressed"]:
option.add_theme_stylebox_override(state, Brand.secondary_button_box(dark, state != "normal"))
func _style_gradient_button(button: Button) -> void:
Brand.apply_font(button)
button.add_theme_font_size_override("font_size", 15)
for state in ["normal", "hover", "pressed"]:
button.add_theme_stylebox_override(state, Brand.gradient_button_box())
button.add_theme_color_override("font_color", Color.WHITE)
button.add_theme_color_override("font_hover_color", Color.WHITE)
func _style_outline_button(button: Button, accent: Color, dark: bool) -> void:
Brand.apply_font(button)
button.add_theme_font_size_override("font_size", 14)
var normal := Style.button_box(Color(0, 0, 0, 0), accent)
var hover := Style.button_box(Color(accent, 0.12), accent)
button.add_theme_stylebox_override("normal", normal)
button.add_theme_stylebox_override("hover", hover)
button.add_theme_stylebox_override("pressed", hover)
button.add_theme_color_override("font_color", accent if dark else accent.darkened(0.08))
button.add_theme_color_override("font_hover_color", accent)
button.add_theme_color_override("icon_normal_color", accent)
func _section_box(dark: bool) -> StyleBoxFlat:
var box := Brand.content_card_box(dark, false)
box.content_margin_left = 16
box.content_margin_right = 16
box.content_margin_top = 14
box.content_margin_bottom = 14
return box
func _active_card_box(dark: bool) -> StyleBoxFlat:
var box := Brand.content_card_box(dark, true, Brand.TEAL)
box.bg_color = Color("0c1c31") if dark else Color("f4fbfb")
box.border_color = Color("174c58") if dark else Color("b9e7e2")
box.content_margin_left = 16
box.content_margin_right = 16
box.content_margin_top = 16
box.content_margin_bottom = 16
return box
func _notice_box(dark: bool) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("101a32") if dark else Color("f5f7ff")
box.border_color = Color("29365d") if dark else Color("dbe3f4")
box.set_border_width_all(1)
box.set_corner_radius_all(7)
box.content_margin_left = 12
box.content_margin_right = 12
box.content_margin_top = 9
box.content_margin_bottom = 9
return box
func _row_box(dark: bool) -> StyleBoxFlat:
var box := StyleBoxFlat.new()
box.bg_color = Color("0c162b") if dark else Color("ffffff")
box.border_color = Color("273450") if dark else Color("e3e8f1")
box.border_width_top = 1
box.content_margin_left = 12
box.content_margin_right = 12
box.content_margin_top = 10
box.content_margin_bottom = 10
return box
func _clear(container: Node) -> void:
for child in container.get_children():
container.remove_child(child)
child.queue_free()