first commit
This commit is contained in:
202
godot_client/scripts/components/card.gd
Normal file
202
godot_client/scripts/components/card.gd
Normal file
@@ -0,0 +1,202 @@
|
||||
extends PanelContainer
|
||||
class_name Card
|
||||
## Card Component
|
||||
##
|
||||
## Styled container for content with optional header and footer.
|
||||
## Mimics the card design from the web UI.
|
||||
##
|
||||
## Structure:
|
||||
## Card (PanelContainer)
|
||||
## └─ VBoxContainer
|
||||
## ├─ Header (optional)
|
||||
## ├─ Body (content)
|
||||
## └─ Footer (optional)
|
||||
##
|
||||
## Usage:
|
||||
## var card = Card.new()
|
||||
## card.set_header("Character Details")
|
||||
## card.add_content(my_content_node)
|
||||
## card.set_footer_buttons(["Save", "Cancel"])
|
||||
|
||||
signal header_action_pressed(action: String)
|
||||
signal footer_button_pressed(button_text: String)
|
||||
|
||||
# Export variables
|
||||
@export var header_text: String = ""
|
||||
@export var show_header: bool = false
|
||||
@export var show_footer: bool = false
|
||||
@export var card_style: StyleVariant = StyleVariant.DEFAULT
|
||||
|
||||
# Style variants
|
||||
enum StyleVariant {
|
||||
DEFAULT, # Standard card
|
||||
HIGHLIGHTED, # Gold border highlight
|
||||
SUBTLE # Minimal border
|
||||
}
|
||||
|
||||
# Internal nodes
|
||||
var _container: VBoxContainer = null
|
||||
var _header_container: HBoxContainer = null
|
||||
var _header_label: Label = null
|
||||
var _body_container: MarginContainer = null
|
||||
var _body_content: VBoxContainer = null
|
||||
var _footer_container: HBoxContainer = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_structure()
|
||||
_apply_style()
|
||||
|
||||
if not header_text.is_empty():
|
||||
set_header(header_text)
|
||||
|
||||
|
||||
## Setup card structure
|
||||
func _setup_structure() -> void:
|
||||
# Main container
|
||||
_container = VBoxContainer.new()
|
||||
add_child(_container)
|
||||
|
||||
# Header
|
||||
_header_container = HBoxContainer.new()
|
||||
_header_container.visible = show_header
|
||||
|
||||
_header_label = Label.new()
|
||||
_header_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
# TODO: Set heading font when theme is set up
|
||||
_header_container.add_child(_header_label)
|
||||
|
||||
_container.add_child(_header_container)
|
||||
|
||||
# Add separator after header
|
||||
var separator = HSeparator.new()
|
||||
separator.visible = show_header
|
||||
_container.add_child(separator)
|
||||
|
||||
# Body
|
||||
_body_container = MarginContainer.new()
|
||||
_body_container.add_theme_constant_override("margin_left", 16)
|
||||
_body_container.add_theme_constant_override("margin_right", 16)
|
||||
_body_container.add_theme_constant_override("margin_top", 16)
|
||||
_body_container.add_theme_constant_override("margin_bottom", 16)
|
||||
|
||||
_body_content = VBoxContainer.new()
|
||||
_body_content.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_body_content.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
|
||||
_body_container.add_child(_body_content)
|
||||
_container.add_child(_body_container)
|
||||
|
||||
# Footer
|
||||
var footer_separator = HSeparator.new()
|
||||
footer_separator.visible = show_footer
|
||||
_container.add_child(footer_separator)
|
||||
|
||||
_footer_container = HBoxContainer.new()
|
||||
_footer_container.visible = show_footer
|
||||
_footer_container.alignment = BoxContainer.ALIGNMENT_END
|
||||
_container.add_child(_footer_container)
|
||||
|
||||
|
||||
## Apply card styling
|
||||
func _apply_style() -> void:
|
||||
var style = StyleBoxFlat.new()
|
||||
|
||||
match card_style:
|
||||
StyleVariant.DEFAULT:
|
||||
style.bg_color = ThemeColors.BACKGROUND_CARD
|
||||
style.border_width_all = 1
|
||||
style.border_color = ThemeColors.BORDER_DEFAULT
|
||||
style.corner_radius_all = 8
|
||||
|
||||
StyleVariant.HIGHLIGHTED:
|
||||
style.bg_color = ThemeColors.BACKGROUND_CARD
|
||||
style.border_width_all = 2
|
||||
style.border_color = ThemeColors.GOLD_ACCENT
|
||||
style.corner_radius_all = 8
|
||||
style.shadow_color = ThemeColors.SHADOW
|
||||
style.shadow_size = 4
|
||||
|
||||
StyleVariant.SUBTLE:
|
||||
style.bg_color = ThemeColors.BACKGROUND_CARD
|
||||
style.border_width_all = 1
|
||||
style.border_color = ThemeColors.DIVIDER
|
||||
style.corner_radius_all = 4
|
||||
|
||||
add_theme_stylebox_override("panel", style)
|
||||
|
||||
|
||||
## Set header text and show header
|
||||
func set_header(text: String) -> void:
|
||||
header_text = text
|
||||
_header_label.text = text
|
||||
_header_container.visible = true
|
||||
show_header = true
|
||||
|
||||
# Show separator
|
||||
if _container.get_child_count() > 1:
|
||||
_container.get_child(1).visible = true
|
||||
|
||||
|
||||
## Hide header
|
||||
func hide_header() -> void:
|
||||
_header_container.visible = false
|
||||
show_header = false
|
||||
|
||||
# Hide separator
|
||||
if _container.get_child_count() > 1:
|
||||
_container.get_child(1).visible = false
|
||||
|
||||
|
||||
## Add content to card body
|
||||
func add_content(node: Node) -> void:
|
||||
_body_content.add_child(node)
|
||||
|
||||
|
||||
## Clear all content from body
|
||||
func clear_content() -> void:
|
||||
for child in _body_content.get_children():
|
||||
child.queue_free()
|
||||
|
||||
|
||||
## Set footer buttons
|
||||
func set_footer_buttons(button_labels: Array[String]) -> void:
|
||||
# Clear existing buttons
|
||||
for child in _footer_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
# Add new buttons
|
||||
for label in button_labels:
|
||||
var btn = Button.new()
|
||||
btn.text = label
|
||||
btn.pressed.connect(_on_footer_button_pressed.bind(label))
|
||||
_footer_container.add_child(btn)
|
||||
|
||||
# Show footer
|
||||
_footer_container.visible = true
|
||||
show_footer = true
|
||||
|
||||
# Show separator
|
||||
if _container.get_child_count() > 2:
|
||||
_container.get_child(_container.get_child_count() - 2).visible = true
|
||||
|
||||
|
||||
## Hide footer
|
||||
func hide_footer() -> void:
|
||||
_footer_container.visible = false
|
||||
show_footer = false
|
||||
|
||||
# Hide separator
|
||||
if _container.get_child_count() > 2:
|
||||
_container.get_child(_container.get_child_count() - 2).visible = false
|
||||
|
||||
|
||||
## Set card style variant
|
||||
func set_style_variant(variant: StyleVariant) -> void:
|
||||
card_style = variant
|
||||
_apply_style()
|
||||
|
||||
|
||||
## Internal: Handle footer button press
|
||||
func _on_footer_button_pressed(button_text: String) -> void:
|
||||
footer_button_pressed.emit(button_text)
|
||||
1
godot_client/scripts/components/card.gd.uid
Normal file
1
godot_client/scripts/components/card.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dq4fplw7kw5yu
|
||||
280
godot_client/scripts/components/form_field.gd
Normal file
280
godot_client/scripts/components/form_field.gd
Normal file
@@ -0,0 +1,280 @@
|
||||
extends VBoxContainer
|
||||
class_name FormField
|
||||
## Form Field Component
|
||||
##
|
||||
## Combines label, input field, and error message display.
|
||||
## Supports validation and different input types.
|
||||
##
|
||||
## Structure:
|
||||
## VBoxContainer
|
||||
## ├─ Label (field label)
|
||||
## ├─ LineEdit (input field)
|
||||
## └─ Label (error message, hidden by default)
|
||||
##
|
||||
## Usage:
|
||||
## var field = FormField.new()
|
||||
## field.label_text = "Email"
|
||||
## field.placeholder = "Enter your email"
|
||||
## field.input_type = FormField.InputType.EMAIL
|
||||
## field.value_changed.connect(_on_email_changed)
|
||||
|
||||
signal value_changed(new_value: String)
|
||||
signal validation_changed(is_valid: bool)
|
||||
|
||||
# Input types
|
||||
enum InputType {
|
||||
TEXT, # Plain text
|
||||
EMAIL, # Email validation
|
||||
PASSWORD, # Password (hidden)
|
||||
NUMBER, # Numeric only
|
||||
PHONE, # Phone number
|
||||
}
|
||||
|
||||
# Export variables
|
||||
@export var label_text: String = ""
|
||||
@export var placeholder: String = ""
|
||||
@export var input_type: InputType = InputType.TEXT
|
||||
@export var required: bool = false
|
||||
@export var min_length: int = 0
|
||||
@export var max_length: int = 0
|
||||
|
||||
# Internal nodes
|
||||
var _label: Label = null
|
||||
var _input: LineEdit = null
|
||||
var _error_label: Label = null
|
||||
|
||||
# State
|
||||
var _is_valid: bool = true
|
||||
var _error_message: String = ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_setup_structure()
|
||||
_apply_styling()
|
||||
|
||||
|
||||
## Setup form field structure
|
||||
func _setup_structure() -> void:
|
||||
# Label
|
||||
_label = Label.new()
|
||||
_label.text = label_text
|
||||
add_child(_label)
|
||||
|
||||
# Spacing
|
||||
var spacer1 = Control.new()
|
||||
spacer1.custom_minimum_size = Vector2(0, 4)
|
||||
add_child(spacer1)
|
||||
|
||||
# Input field
|
||||
_input = LineEdit.new()
|
||||
_input.placeholder_text = placeholder
|
||||
_input.text_changed.connect(_on_text_changed)
|
||||
_input.text_submitted.connect(_on_text_submitted)
|
||||
|
||||
# Set input type
|
||||
match input_type:
|
||||
InputType.PASSWORD:
|
||||
_input.secret = true
|
||||
InputType.NUMBER:
|
||||
# TODO: Add number-only filter
|
||||
pass
|
||||
_:
|
||||
pass
|
||||
|
||||
# Set max length if specified
|
||||
if max_length > 0:
|
||||
_input.max_length = max_length
|
||||
|
||||
add_child(_input)
|
||||
|
||||
# Spacing
|
||||
var spacer2 = Control.new()
|
||||
spacer2.custom_minimum_size = Vector2(0, 2)
|
||||
add_child(spacer2)
|
||||
|
||||
# Error label
|
||||
_error_label = Label.new()
|
||||
_error_label.visible = false
|
||||
add_child(_error_label)
|
||||
|
||||
|
||||
## Apply styling
|
||||
func _apply_styling() -> void:
|
||||
# Label styling
|
||||
_label.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY)
|
||||
|
||||
# Input styling
|
||||
var normal_style = StyleBoxFlat.new()
|
||||
normal_style.bg_color = ThemeColors.BACKGROUND_SECONDARY
|
||||
normal_style.border_width_all = 2
|
||||
normal_style.border_color = ThemeColors.BORDER_DEFAULT
|
||||
normal_style.corner_radius_all = 4
|
||||
normal_style.content_margin_left = 12
|
||||
normal_style.content_margin_right = 12
|
||||
normal_style.content_margin_top = 8
|
||||
normal_style.content_margin_bottom = 8
|
||||
_input.add_theme_stylebox_override("normal", normal_style)
|
||||
|
||||
var focus_style = normal_style.duplicate()
|
||||
focus_style.border_color = ThemeColors.GOLD_ACCENT
|
||||
_input.add_theme_stylebox_override("focus", focus_style)
|
||||
|
||||
_input.add_theme_color_override("font_color", ThemeColors.TEXT_PRIMARY)
|
||||
_input.add_theme_color_override("font_placeholder_color", ThemeColors.TEXT_SECONDARY)
|
||||
_input.add_theme_color_override("caret_color", ThemeColors.GOLD_ACCENT)
|
||||
|
||||
# Error label styling
|
||||
_error_label.add_theme_color_override("font_color", ThemeColors.ERROR)
|
||||
# TODO: Add smaller font size when theme is set up
|
||||
|
||||
|
||||
## Get current value
|
||||
func get_value() -> String:
|
||||
return _input.text if _input else ""
|
||||
|
||||
|
||||
## Set value
|
||||
func set_value(value: String) -> void:
|
||||
if _input:
|
||||
_input.text = value
|
||||
_validate()
|
||||
|
||||
|
||||
## Set label text
|
||||
func set_label(text: String) -> void:
|
||||
label_text = text
|
||||
if _label:
|
||||
_label.text = text
|
||||
|
||||
|
||||
## Set placeholder text
|
||||
func set_placeholder(text: String) -> void:
|
||||
placeholder = text
|
||||
if _input:
|
||||
_input.placeholder_text = text
|
||||
|
||||
|
||||
## Show error message
|
||||
func show_error(message: String) -> void:
|
||||
_error_message = message
|
||||
_is_valid = false
|
||||
|
||||
if _error_label:
|
||||
_error_label.text = message
|
||||
_error_label.visible = true
|
||||
|
||||
# Change input border to error color
|
||||
if _input:
|
||||
var error_style = StyleBoxFlat.new()
|
||||
error_style.bg_color = ThemeColors.BACKGROUND_SECONDARY
|
||||
error_style.border_width_all = 2
|
||||
error_style.border_color = ThemeColors.ERROR
|
||||
error_style.corner_radius_all = 4
|
||||
error_style.content_margin_left = 12
|
||||
error_style.content_margin_right = 12
|
||||
error_style.content_margin_top = 8
|
||||
error_style.content_margin_bottom = 8
|
||||
_input.add_theme_stylebox_override("normal", error_style)
|
||||
|
||||
validation_changed.emit(false)
|
||||
|
||||
|
||||
## Clear error
|
||||
func clear_error() -> void:
|
||||
_error_message = ""
|
||||
_is_valid = true
|
||||
|
||||
if _error_label:
|
||||
_error_label.visible = false
|
||||
|
||||
# Restore normal input styling
|
||||
_apply_styling()
|
||||
|
||||
validation_changed.emit(true)
|
||||
|
||||
|
||||
## Check if field is valid
|
||||
func is_valid() -> bool:
|
||||
return _is_valid
|
||||
|
||||
|
||||
## Focus the input field
|
||||
func focus_input() -> void:
|
||||
if _input:
|
||||
_input.grab_focus()
|
||||
|
||||
|
||||
## Validate the field
|
||||
func validate() -> bool:
|
||||
return _validate()
|
||||
|
||||
|
||||
## Internal: Validate input
|
||||
func _validate() -> bool:
|
||||
var value = get_value()
|
||||
|
||||
# Required check
|
||||
if required and value.is_empty():
|
||||
show_error("This field is required")
|
||||
return false
|
||||
|
||||
# Min length check
|
||||
if min_length > 0 and value.length() < min_length:
|
||||
show_error("Must be at least %d characters" % min_length)
|
||||
return false
|
||||
|
||||
# Max length check (redundant with LineEdit.max_length, but kept for consistency)
|
||||
if max_length > 0 and value.length() > max_length:
|
||||
show_error("Must be no more than %d characters" % max_length)
|
||||
return false
|
||||
|
||||
# Type-specific validation
|
||||
match input_type:
|
||||
InputType.EMAIL:
|
||||
if not _is_valid_email(value) and not value.is_empty():
|
||||
show_error("Invalid email address")
|
||||
return false
|
||||
|
||||
InputType.NUMBER:
|
||||
if not value.is_valid_int() and not value.is_empty():
|
||||
show_error("Must be a number")
|
||||
return false
|
||||
|
||||
InputType.PHONE:
|
||||
if not _is_valid_phone(value) and not value.is_empty():
|
||||
show_error("Invalid phone number")
|
||||
return false
|
||||
|
||||
# All validations passed
|
||||
clear_error()
|
||||
return true
|
||||
|
||||
|
||||
## Internal: Email validation
|
||||
func _is_valid_email(email: String) -> bool:
|
||||
# Basic email regex validation
|
||||
var regex = RegEx.new()
|
||||
regex.compile("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$")
|
||||
return regex.search(email) != null
|
||||
|
||||
|
||||
## Internal: Phone validation
|
||||
func _is_valid_phone(phone: String) -> bool:
|
||||
# Basic phone validation (digits, spaces, dashes, parentheses)
|
||||
var regex = RegEx.new()
|
||||
regex.compile("^[0-9\\s\\-\\(\\)\\+]+$")
|
||||
return regex.search(phone) != null and phone.length() >= 10
|
||||
|
||||
|
||||
## Internal: Handle text change
|
||||
func _on_text_changed(new_text: String) -> void:
|
||||
# Clear error when user starts typing
|
||||
if not _error_message.is_empty():
|
||||
clear_error()
|
||||
|
||||
value_changed.emit(new_text)
|
||||
|
||||
|
||||
## Internal: Handle text submission (Enter key)
|
||||
func _on_text_submitted(new_text: String) -> void:
|
||||
_validate()
|
||||
1
godot_client/scripts/components/form_field.gd.uid
Normal file
1
godot_client/scripts/components/form_field.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dvx6m2ahutlgm
|
||||
Reference in New Issue
Block a user