feat(ui): implant component library + star map refactor (D-169)

New component library at client/ui/implant/:
- ImplantTheme: shared Resource with colors, spacing, font sizes
- ImplantPanel: PanelContainer root with themed background/border
- ImplantHeader: title + subtitle
- ImplantSeparator: themed horizontal rule with above/below spacing
- ImplantDataRow: single-line text with optional color override
- ImplantTextBlock: RichTextLabel for wrapping narrative text
- default_implant.tres: default theme resource

Star map info panel refactored from 140 lines of manual draw_string
calls to 50 lines of component composition via _rebuild_info_panel().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-05 10:47:23 +02:00
co-authored by Claude Opus 4.6
parent 4dd95ba50b
commit 962596fd57
8 changed files with 313 additions and 109 deletions
+24
View File
@@ -0,0 +1,24 @@
[gd_resource type="Resource" script_class="ImplantTheme" load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/implant/implant_theme.gd" id="1"]
[resource]
script = ExtResource("1")
text_primary = Color(0.784, 0.816, 0.878, 1)
text_dim = Color(0.4, 0.467, 0.533, 1)
accent_active = Color(0.941, 0.816, 0.376, 1)
accent_positive = Color(0.267, 0.667, 0.4, 1)
accent_negative = Color(0.667, 0.267, 0.267, 1)
accent_warning = Color(0.667, 0.533, 0.267, 1)
panel_bg = Color(0.05, 0.08, 0.14, 0.92)
separator = Color(0.78, 0.82, 0.88, 0.12)
hover_bg = Color(0.78, 0.82, 0.88, 0.08)
line_height = 18
sep_above = 6
sep_below = 12
padding = 12
border_width = 1.0
font_header = 15
font_body = 11
font_small = 10
font_caption = 9
+28
View File
@@ -0,0 +1,28 @@
@tool
class_name ImplantDataRow
extends Label
## Single-line data display — text with optional color override.
var _color_override: Color = Color.TRANSPARENT # transparent = use theme default
func _init(text: String = "", color: Color = Color.TRANSPARENT) -> void:
self.text = text
_color_override = color
mouse_filter = Control.MOUSE_FILTER_IGNORE
func set_content(text_value: String, color: Color = Color.TRANSPARENT) -> void:
text = text_value
_color_override = color
if _color_override.a > 0.0:
add_theme_color_override("font_color", _color_override)
func apply_implant_theme(t: ImplantTheme) -> void:
add_theme_font_size_override("font_size", t.font_body)
if _color_override.a > 0.0:
add_theme_color_override("font_color", _color_override)
else:
add_theme_color_override("font_color", t.text_dim)
custom_minimum_size.y = t.line_height
+37
View File
@@ -0,0 +1,37 @@
@tool
class_name ImplantHeader
extends VBoxContainer
## Panel header — title + optional subtitle.
var _title_label: Label
var _subtitle_label: Label
func _init(title: String = "", subtitle: String = "") -> void:
_title_label = Label.new()
_title_label.text = title
_title_label.name = "Title"
_subtitle_label = Label.new()
_subtitle_label.text = subtitle
_subtitle_label.name = "Subtitle"
func _ready() -> void:
add_theme_constant_override("separation", 0)
add_child(_title_label)
add_child(_subtitle_label)
func set_content(title: String, subtitle: String = "") -> void:
_title_label.text = title
_subtitle_label.text = subtitle
_subtitle_label.visible = not subtitle.is_empty()
func apply_implant_theme(t: ImplantTheme) -> void:
_title_label.add_theme_font_size_override("font_size", t.font_header)
_title_label.add_theme_color_override("font_color", t.text_primary)
_subtitle_label.add_theme_font_size_override("font_size", t.font_small)
_subtitle_label.add_theme_color_override("font_color", t.text_dim)
+74
View File
@@ -0,0 +1,74 @@
@tool
class_name ImplantPanel
extends PanelContainer
## Root container for any implant UI overlay (D-169).
## Add ImplantHeader, ImplantSeparator, ImplantDataRow, ImplantTextBlock
## as children — they auto-style from the shared theme.
@export var theme_resource: ImplantTheme:
set(value):
theme_resource = value
_apply_theme()
var _vbox: VBoxContainer
func _ready() -> void:
_vbox = VBoxContainer.new()
_vbox.name = "Content"
add_child(_vbox)
_apply_theme()
func _apply_theme() -> void:
if not theme_resource:
return
# Panel background
var style := StyleBoxFlat.new()
style.bg_color = theme_resource.panel_bg
style.border_color = theme_resource.separator
style.border_width_top = int(theme_resource.border_width)
style.border_width_bottom = int(theme_resource.border_width)
style.border_width_left = int(theme_resource.border_width)
style.border_width_right = int(theme_resource.border_width)
style.content_margin_top = theme_resource.padding
style.content_margin_bottom = theme_resource.padding
style.content_margin_left = theme_resource.padding
style.content_margin_right = theme_resource.padding
add_theme_stylebox_override("panel", style)
# VBox spacing
if _vbox:
_vbox.add_theme_constant_override("separation", 0)
# Propagate theme to existing children
for child in get_implant_children():
if child.has_method("apply_implant_theme"):
child.apply_implant_theme(theme_resource)
## Add an implant component to the panel.
func add_component(component: Control) -> void:
if not _vbox:
_vbox = VBoxContainer.new()
_vbox.name = "Content"
add_child(_vbox)
_vbox.add_child(component)
if theme_resource and component.has_method("apply_implant_theme"):
component.apply_implant_theme(theme_resource)
## Get all implant component children.
func get_implant_children() -> Array:
if not _vbox:
return []
return _vbox.get_children()
## Clear all components.
func clear() -> void:
if not _vbox:
return
for child in _vbox.get_children():
child.queue_free()
+28
View File
@@ -0,0 +1,28 @@
@tool
class_name ImplantSeparator
extends Control
## Thin horizontal separator line with themed spacing.
var _theme: ImplantTheme
func _init() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
func apply_implant_theme(t: ImplantTheme) -> void:
_theme = t
custom_minimum_size.y = t.sep_above + t.sep_below
queue_redraw()
func _draw() -> void:
if not _theme:
return
var y: float = _theme.sep_above
draw_line(
Vector2(0, y),
Vector2(size.x, y),
_theme.separator,
1.0
)
+32
View File
@@ -0,0 +1,32 @@
@tool
class_name ImplantTextBlock
extends RichTextLabel
## Multi-line wrapping text block for narrative content (GTTR excerpts, etc.).
var _max_lines: int = -1
var _text_alpha: float = 0.75
func _init(content: String = "", max_lines: int = -1) -> void:
_max_lines = max_lines
text = content
bbcode_enabled = false
fit_content = true
scroll_active = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
if max_lines > 0:
visible_characters_behavior = TextServer.VC_GLYPHS_LTR
# Godot 4 RichTextLabel doesn't have max_lines directly —
# we limit via visible_ratio or custom_minimum_size after layout.
func set_content(content: String, max_lines: int = -1) -> void:
text = content.replace("**", "") # strip markdown bold
_max_lines = max_lines
func apply_implant_theme(t: ImplantTheme) -> void:
add_theme_font_size_override("normal_font_size", t.font_small)
add_theme_color_override("default_color",
Color(t.text_primary.r, t.text_primary.g, t.text_primary.b, _text_alpha))
+46
View File
@@ -0,0 +1,46 @@
class_name ImplantTheme
extends Resource
## Shared styling resource for all implant UI panels (D-169).
## Swap this resource at runtime to change the implant's visual identity
## (hardware variants, upgrades, faction overlays).
# ── Colors ───────────────────────────────────────────────────────────────────
## Primary readable text
@export var text_primary: Color = Color("#c8d0e0")
## Labels, metadata, captions
@export var text_dim: Color = Color("#667788")
## Selected, active, interactive highlight
@export var accent_active: Color = Color("#f0d060")
## Good state, opportunity, profit
@export var accent_positive: Color = Color("#44aa66")
## Danger, loss, hostile
@export var accent_negative: Color = Color("#aa4444")
## Caution, degraded, unknown
@export var accent_warning: Color = Color("#aa8844")
## Panel background
@export var panel_bg: Color = Color(0.05, 0.08, 0.14, 0.92)
## Panel border and separator lines
@export var separator: Color = Color(0.78, 0.82, 0.88, 0.12)
## Hover state background
@export var hover_bg: Color = Color(0.78, 0.82, 0.88, 0.08)
# ── Spacing ──────────────────────────────────────────────────────────────────
## Line height for single-line labels (baseline to baseline)
@export var line_height: int = 18
## Space from last text baseline to separator line
@export var sep_above: int = 6
## Space from separator line to next text baseline
@export var sep_below: int = 12
## Panel internal padding
@export var padding: int = 12
## Border width
@export var border_width: float = 1.0
# ── Font sizes ───────────────────────────────────────────────────────────────
@export var font_header: int = 15
@export var font_body: int = 11
@export var font_small: int = 10
@export var font_caption: int = 9