Redesign flashcards for visual hierarchy and button consistency

Study card: label the answer and "key takeaway" as distinct sections
with a real visible divider (previously an all-but-invisible
HSeparator) instead of one undifferentiated block of text. Also fixes
a real gap where a cloze card's extra field was silently dropped,
since only basic notes combined answer+extra into one label. Bigger
card, gradient Show-Answer button matching the app's signature CTA
style instead of a duller flat teal, and the Undo button now hides
itself when there's nothing to undo instead of sitting there disabled
looking identical to a real action.

Library: colored type badge per card (Basic/Cloze) instead of plain
gray text, more breathing room inside each row, Study promoted to the
same gradient CTA style, and Edit/Archive given a real secondary
button treatment instead of default theme buttons. Note editor fields
renamed ("Back / Extra takeaway" + a separate "Extra" field was
genuinely confusing) and now show which deck a card is being saved to.
This commit is contained in:
hz
2026-07-21 22:05:36 -05:00
parent b00b321d6f
commit 7e19366dcb
4 changed files with 182 additions and 38 deletions
+77 -12
View File
@@ -23,6 +23,7 @@ var debounce_timer: Timer
var editor_overlay: Control
var editor_panel: PanelContainer
var editor_deck_label: Label
var editor_note_id := ""
var editor_expected_revision: Variant = null
var editor_type_selector: OptionButton
@@ -111,8 +112,12 @@ func _build_toolbar() -> void:
study_button = Button.new()
study_button.text = "Study"
study_button.custom_minimum_size = Vector2(0, 44)
Style.style_primary_button(study_button)
study_button.custom_minimum_size = Vector2(110, 44)
study_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
study_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
study_button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
study_button.add_theme_color_override("font_color", Color.WHITE)
study_button.add_theme_color_override("font_hover_color", Color.WHITE)
study_button.pressed.connect(_start_study)
row.add_child(study_button)
@@ -291,7 +296,12 @@ func _render_notes() -> void:
func _note_row(note: Dictionary) -> Control:
var dark := UIThemeService.is_dark_mode()
var card := PanelContainer.new()
card.add_theme_stylebox_override("panel", Brand.content_card_box(dark, false, Brand.ROYAL_BLUE))
var card_box := Brand.content_card_box(dark, false, Brand.ROYAL_BLUE)
card_box.content_margin_left = 16
card_box.content_margin_right = 16
card_box.content_margin_top = 14
card_box.content_margin_bottom = 14
card.add_theme_stylebox_override("panel", card_box)
var row := BoxContainer.new()
row.vertical = compact_layout
row.add_theme_constant_override("separation", 12)
@@ -299,8 +309,22 @@ func _note_row(note: Dictionary) -> Control:
var content := VBoxContainer.new()
content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
content.add_theme_constant_override("separation", 6)
row.add_child(content)
var type_str := str(note.get("type", "basic"))
var header_row := HBoxContainer.new()
header_row.add_theme_constant_override("separation", 8)
content.add_child(header_row)
header_row.add_child(_type_badge(type_str, dark))
var tags: Array = note.get("tags", [])
if tags.size() > 0:
var tags_label := Label.new()
tags_label.text = ", ".join(PackedStringArray(tags))
tags_label.add_theme_font_size_override("font_size", 12)
tags_label.add_theme_color_override("font_color", Brand.muted_color(dark))
header_row.add_child(tags_label)
var prompt_text := str(note.get("prompt", ""))
var preview := Cloze.render_back(prompt_text) if type_str == "cloze" else prompt_text
var prompt_label := Label.new()
@@ -308,22 +332,21 @@ func _note_row(note: Dictionary) -> Control:
prompt_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
prompt_label.add_theme_font_override("font", Brand.font)
content.add_child(prompt_label)
var meta := Label.new()
var tags: Array = note.get("tags", [])
meta.text = "%s%s" % [type_str.capitalize(), (" | " + ", ".join(PackedStringArray(tags))) if tags.size() > 0 else ""]
meta.add_theme_font_size_override("font_size", 12)
meta.add_theme_color_override("font_color", Brand.muted_color(dark))
content.add_child(meta)
var edit_button := Button.new()
edit_button.text = "Edit"
edit_button.custom_minimum_size = Vector2(70, 40)
edit_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
edit_button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
edit_button.pressed.connect(_open_editor.bind(str(note.get("id", "")), note))
row.add_child(edit_button)
var archive_button := Button.new()
archive_button.text = "Archive"
archive_button.custom_minimum_size = Vector2(85, 40)
archive_button.add_theme_stylebox_override("normal", Brand.secondary_button_box(dark))
archive_button.add_theme_stylebox_override("hover", Brand.secondary_button_box(dark, true))
archive_button.add_theme_color_override("font_color", Color("ff5f76") if dark else Color("be123c"))
archive_button.pressed.connect(func():
FlashcardService.archive_notes([str(note.get("id", ""))], true)
await get_tree().create_timer(0.3).timeout
@@ -333,6 +356,27 @@ func _note_row(note: Dictionary) -> Control:
return card
func _type_badge(type_str: String, dark: bool) -> PanelContainer:
var accent := Brand.TEAL if type_str == "cloze" else Brand.ROYAL_BLUE
var badge := PanelContainer.new()
var box := StyleBoxFlat.new()
box.bg_color = Color(accent, 0.16 if dark else 0.12)
box.border_color = accent
box.set_border_width_all(1)
box.set_corner_radius_all(8)
box.content_margin_left = 8
box.content_margin_right = 8
box.content_margin_top = 2
box.content_margin_bottom = 2
badge.add_theme_stylebox_override("panel", box)
var label := Label.new()
label.text = type_str.capitalize()
label.add_theme_font_size_override("font_size", 11)
label.add_theme_color_override("font_color", accent)
badge.add_child(label)
return badge
# ---------- Note editor ----------
func _build_editor_overlay() -> void:
@@ -373,6 +417,11 @@ func _build_editor_overlay() -> void:
close_button.pressed.connect(_close_editor)
header.add_child(close_button)
editor_deck_label = Label.new()
editor_deck_label.add_theme_font_size_override("font_size", 13)
editor_deck_label.add_theme_color_override("font_color", Brand.muted_color(UIThemeService.is_dark_mode()))
content.add_child(editor_deck_label)
editor_type_selector = OptionButton.new()
editor_type_selector.add_item("Basic")
editor_type_selector.add_item("Cloze")
@@ -391,13 +440,13 @@ func _build_editor_overlay() -> void:
editor_front_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
content.add_child(editor_front_input)
content.add_child(_field_label("Back / Extra takeaway"))
content.add_child(_field_label("Answer"))
editor_back_input = TextEdit.new()
editor_back_input.custom_minimum_size = Vector2(0, 80)
editor_back_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
content.add_child(editor_back_input)
content.add_child(_field_label("Extra"))
content.add_child(_field_label("Extra (optional teaching pearl)"))
editor_extra_input = TextEdit.new()
editor_extra_input.custom_minimum_size = Vector2(0, 60)
editor_extra_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
@@ -415,7 +464,11 @@ func _build_editor_overlay() -> void:
var save_button := Button.new()
save_button.text = "Save"
save_button.custom_minimum_size = Vector2(0, 46)
Style.style_primary_button(save_button)
save_button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
save_button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
save_button.add_theme_stylebox_override("pressed", Brand.gradient_button_box())
save_button.add_theme_color_override("font_color", Color.WHITE)
save_button.add_theme_color_override("font_hover_color", Color.WHITE)
save_button.pressed.connect(_save_editor)
content.add_child(save_button)
@@ -446,10 +499,20 @@ func _open_editor(note_id: String, note: Dictionary) -> void:
editor_extra_input.text = str(note.get("extra", ""))
editor_tags_input.text = ", ".join(PackedStringArray(note.get("tags", [])))
editor_error_label.visible = false
editor_deck_label.text = "Saving to: %s" % _deck_name_for_id(str(note.get("deckId", "")) if not note_id.is_empty() else _selected_deck_id())
_on_editor_type_changed(editor_type_selector.selected)
editor_overlay.visible = true
func _deck_name_for_id(id: String) -> String:
if not id.is_empty():
for summary in decks:
var deck: Dictionary = summary.get("deck", {})
if str(deck.get("id", "")) == id:
return str(deck.get("name", "Default"))
return "Default"
func _close_editor() -> void:
editor_overlay.visible = false
@@ -632,6 +695,8 @@ func _refresh_status() -> void:
var dark := UIThemeService.is_dark_mode()
if editor_panel != null:
editor_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
if editor_deck_label != null:
editor_deck_label.add_theme_color_override("font_color", Brand.muted_color(dark))
if import_panel != null:
import_panel.add_theme_stylebox_override("panel", Brand.panel_box(dark))
if export_menu != null: