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
+89 -22
View File
@@ -26,8 +26,11 @@ var exit_button: Button
var paused_banner: Label
var card_panel: PanelContainer
var front_label: Label
var divider: HSeparator
var divider: ColorRect
var answer_caption: Label
var back_label: Label
var extra_caption: Label
var extra_label: Label
var reveal_button: Button
var grade_row: HBoxContainer
var grade_buttons: Dictionary = {}
@@ -57,6 +60,9 @@ func on_show(payload: Variant = null) -> void:
current_note = {}
current_card = {}
revealed = false
last_client_review_id = ""
last_card_id = ""
undo_button.visible = false
_render_loading()
_load_next()
@@ -100,11 +106,11 @@ func _build() -> void:
root.add_child(center)
card_panel = PanelContainer.new()
card_panel.custom_minimum_size = Vector2(640, 360)
card_panel.custom_minimum_size = Vector2(720, 420)
card_panel.add_theme_stylebox_override("panel", Brand.content_card_box(UIThemeService.is_dark_mode(), true, Brand.ROYAL_BLUE))
center.add_child(card_panel)
var card_content := VBoxContainer.new()
card_content.add_theme_constant_override("separation", 18)
card_content.add_theme_constant_override("separation", 20)
card_panel.add_child(card_content)
empty_label = Label.new()
@@ -116,24 +122,42 @@ func _build() -> void:
front_label = Label.new()
front_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
front_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
front_label.add_theme_font_size_override("font_size", 22)
front_label.add_theme_font_size_override("font_size", 25)
card_content.add_child(front_label)
divider = HSeparator.new()
divider = ColorRect.new()
divider.custom_minimum_size = Vector2(0, 2)
divider.visible = false
card_content.add_child(divider)
var answer_block := VBoxContainer.new()
answer_block.add_theme_constant_override("separation", 4)
card_content.add_child(answer_block)
answer_caption = _caption_label("ANSWER")
answer_block.add_child(answer_caption)
back_label = Label.new()
back_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
back_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
back_label.add_theme_font_size_override("font_size", 18)
back_label.add_theme_font_size_override("font_size", 19)
back_label.visible = false
card_content.add_child(back_label)
answer_block.add_child(back_label)
var extra_block := VBoxContainer.new()
extra_block.add_theme_constant_override("separation", 4)
card_content.add_child(extra_block)
extra_caption = _caption_label("KEY TAKEAWAY")
extra_block.add_child(extra_caption)
extra_label = Label.new()
extra_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
extra_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
extra_label.add_theme_font_size_override("font_size", 15)
extra_label.visible = false
extra_block.add_child(extra_label)
reveal_button = Button.new()
reveal_button.text = "Show Answer (Space)"
reveal_button.custom_minimum_size = Vector2(0, 52)
Style.style_primary_button(reveal_button)
_style_gradient_button(reveal_button)
reveal_button.pressed.connect(_reveal)
root.add_child(reveal_button)
@@ -152,10 +176,38 @@ func _build() -> void:
undo_button = Button.new()
undo_button.text = "Undo last review"
undo_button.custom_minimum_size = Vector2(0, 42)
undo_button.disabled = true
undo_button.custom_minimum_size = Vector2(0, 36)
undo_button.visible = false
undo_button.pressed.connect(_undo)
root.add_child(undo_button)
_style_ghost_button(undo_button, UIThemeService.is_dark_mode())
func _caption_label(text: String) -> Label:
var label := Label.new()
label.text = text
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", 12)
label.visible = false
return label
func _style_gradient_button(button: Button) -> void:
button.add_theme_stylebox_override("normal", Brand.gradient_button_box())
button.add_theme_stylebox_override("hover", Brand.gradient_button_box())
button.add_theme_stylebox_override("pressed", 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_ghost_button(button: Button, dark: bool) -> void:
var empty := StyleBoxEmpty.new()
button.add_theme_stylebox_override("normal", empty)
button.add_theme_stylebox_override("hover", empty)
button.add_theme_stylebox_override("pressed", empty)
button.add_theme_stylebox_override("focus", empty)
button.add_theme_color_override("font_color", Brand.muted_color(dark))
button.add_theme_color_override("font_hover_color", Brand.text_color(dark))
func refresh_theme_controls() -> void:
@@ -164,6 +216,11 @@ func refresh_theme_controls() -> void:
var dark := UIThemeService.is_dark_mode()
card_panel.add_theme_stylebox_override("panel", Brand.content_card_box(dark, true, Brand.ROYAL_BLUE))
empty_label.add_theme_color_override("font_color", Brand.muted_color(dark))
divider.color = Brand.ROYAL_BLUE if dark else Brand.PURPLE
answer_caption.add_theme_color_override("font_color", Brand.ROYAL_BLUE if dark else Brand.PURPLE)
extra_caption.add_theme_color_override("font_color", Brand.muted_color(dark))
extra_label.add_theme_color_override("font_color", Brand.muted_color(dark))
_style_ghost_button(undo_button, dark)
func _update_responsive_layout() -> void:
@@ -171,8 +228,8 @@ func _update_responsive_layout() -> void:
return
compact_layout = size.x < 720.0
var available := maxf(size.x - 48.0, 240.0)
card_panel.custom_minimum_size.x = minf(640.0, available)
card_panel.custom_minimum_size.y = 300.0 if compact_layout else 360.0
card_panel.custom_minimum_size.x = minf(720.0, available)
card_panel.custom_minimum_size.y = 340.0 if compact_layout else 420.0
func _load_next() -> void:
@@ -208,8 +265,7 @@ func _render_loading() -> void:
empty_label.visible = false
front_label.visible = true
front_label.text = "Loading..."
back_label.visible = false
divider.visible = false
_hide_answer()
reveal_button.visible = false
grade_row.visible = false
@@ -217,8 +273,7 @@ func _render_loading() -> void:
func _render_empty(card: Dictionary) -> void:
empty_label.visible = true
front_label.visible = false
back_label.visible = false
divider.visible = false
_hide_answer()
reveal_button.visible = false
grade_row.visible = false
progress_label.text = "0 due"
@@ -226,6 +281,14 @@ func _render_empty(card: Dictionary) -> void:
progress_label.text = "%d due, %d new available" % [int(card.get("dueRemaining", 0)), int(card.get("newRemaining", 0))]
func _hide_answer() -> void:
divider.visible = false
answer_caption.visible = false
back_label.visible = false
extra_caption.visible = false
extra_label.visible = false
func _render_card() -> void:
empty_label.visible = false
var note_type := str(current_note.get("type", "basic"))
@@ -235,8 +298,7 @@ func _render_card() -> void:
else:
front_label.text = str(current_note.get("prompt", ""))
front_label.visible = true
back_label.visible = false
divider.visible = false
_hide_answer()
reveal_button.visible = true
grade_row.visible = false
progress_label.text = "%d due, %d new available" % [due_remaining, new_remaining]
@@ -251,11 +313,14 @@ func _reveal() -> void:
if note_type == "cloze":
back_label.text = Cloze.render_back(str(current_note.get("prompt", "")))
else:
var answer := str(current_note.get("answer", ""))
var extra := str(current_note.get("extra", ""))
back_label.text = "%s\n\n%s" % [answer, extra] if not extra.is_empty() else answer
back_label.visible = true
back_label.text = str(current_note.get("answer", ""))
var extra := str(current_note.get("extra", ""))
divider.visible = true
answer_caption.visible = true
back_label.visible = true
extra_caption.visible = not extra.is_empty()
extra_label.visible = not extra.is_empty()
extra_label.text = extra
reveal_button.visible = false
grade_row.visible = true
for rating in grade_buttons:
@@ -278,6 +343,7 @@ func _grade(rating: String) -> void:
var duration_seconds := int(float(Time.get_ticks_msec() - loaded_started_ms) / 1000.0)
last_client_review_id = FlashcardService.submit_review(card_id, rating, expected_revision, duration_seconds, study_day_start_utc)
last_card_id = card_id
undo_button.visible = true
undo_button.disabled = false
if not seen_card_ids.has(card_id):
seen_card_ids.append(card_id)
@@ -295,6 +361,7 @@ func _undo() -> void:
undo_button.disabled = true
FlashcardService.undo_last_review(last_client_review_id, func(_restored: Dictionary):
last_client_review_id = ""
undo_button.visible = false
seen_card_ids.erase(last_card_id)
_load_next())