Files
aetherbound-guild/runtime/foundation/ui_tokens.gd
T

300 lines
10 KiB
GDScript

class_name ABGUI_Tokens
extends RefCounted
## Shared visual/layout vocabulary. These tokens are deliberately not a page
## layout: a page may choose its own composition while retaining the same
## semantic controls, readable type and focus language.
const DESIGN_ASPECT := 16.0 / 9.0
const DESIGN_CANVAS_SIZE := Vector2(1280.0, 720.0)
const DESIGN_SAFE_MARGIN := Vector2(48.0, 32.0)
const DESIGN_GRID_GUTTER := 16.0
const MIN_TOUCH_TARGET := Vector2(48.0, 48.0)
const MIN_FOCUS_GAP := 4.0
const COLOR_INK := Color("#111921")
const COLOR_NAVY := Color("#182838")
const COLOR_NAVY_LIFT := Color("#263e53")
const COLOR_NAVY_DISABLED := Color("#323b43")
const COLOR_GOLD := Color("#b99a5d")
const COLOR_GOLD_LIGHT := Color("#ead49b")
const COLOR_IVORY := Color("#f5ead0")
const COLOR_SKY := Color("#73b9e7")
const COLOR_DANGER := Color("#a94b53")
const COLOR_MUTED := Color("#73808a")
# Success ink for parchment surfaces (dark enough for ink-on-paper contrast).
const COLOR_SUCCESS := Color("#3d7449")
const COLOR_SURFACE := Color("#182838", 0.90)
const COLOR_SURFACE_BORDER := Color("#b99a5d", 0.78)
const COLOR_SURFACE_INK := Color("#f5ead0")
## Shared spacing and control vocabulary. These values describe semantic
## controls, not a page layout; page masters may still choose their own
## normalized rectangles.
const SPACING := {
"micro": 4,
"tight": 6,
"control": 8,
"section": 12,
"page": 18,
}
const BORDER_WIDTHS := {
"hairline": 1,
"standard": 2,
"emphasis": 3,
}
const CORNER_RADII := {
"control": 6,
"surface": 8,
"notice": 10,
}
const COMMAND_HEIGHTS := {
"primary": 52,
"secondary": 48,
"utility": 48,
"danger": 48,
"icon": 48,
}
## Authored minimum widths for semantic commands. Pages may still use an
## expanding normalized slot, but fixed-width commands must share these
## footprints so labels do not drift between screens.
const COMMAND_WIDTHS := {
"primary": 220,
"secondary": 180,
"utility": 160,
"danger": 190,
"icon": 48,
}
const COMMAND_VARIANTS := ["primary", "secondary", "utility", "danger", "icon"]
const ICON_KINDS := [
"back", "settings", "company", "coin", "equipment", "party", "risk",
"confirm", "inspect", "pause", "speed", "retry", "close", "help", "generic",
]
const FONT_SIZES := {
# caption: the narrow market-card plate opening (name/trait chips) steps
# below micro; recorded in M52_SHARED_UI_RULES.md before page reuse.
"caption": 8,
"micro": 11,
"label": 14,
"body": 16,
"heading": 22,
"title": 30,
}
const SURFACE_VARIANTS := ["shell", "context", "notice", "overlay"]
static func font_size(role: String, text_scale := 1.0) -> int:
var base: int = int(FONT_SIZES.get(role, FONT_SIZES.body))
return maxi(11, int(round(float(base) * maxf(0.8, text_scale))))
static func touch_size(size: Vector2 = Vector2.ZERO) -> Vector2:
return Vector2(maxf(MIN_TOUCH_TARGET.x, size.x), maxf(MIN_TOUCH_TARGET.y, size.y))
static func design_equal_row(count: int, y: float, height: float, left := DESIGN_SAFE_MARGIN.x, right := DESIGN_SAFE_MARGIN.x, gap := DESIGN_GRID_GUTTER) -> Array:
var result: Array = []
if count <= 0:
return result
var available := DESIGN_CANVAS_SIZE.x - left - right - gap * float(count - 1)
var width := available / float(count)
for index in range(count):
var x := left + float(index) * (width + gap)
result.append(Rect2(x / DESIGN_CANVAS_SIZE.x, y / DESIGN_CANVAS_SIZE.y, width / DESIGN_CANVAS_SIZE.x, height / DESIGN_CANVAS_SIZE.y))
return result
static func is_supported_variant(variant: String) -> bool:
return variant in COMMAND_VARIANTS
static func spacing(role: String) -> int:
return int(SPACING.get(role, SPACING.control))
static func border_width(role: String) -> int:
return int(BORDER_WIDTHS.get(role, BORDER_WIDTHS.standard))
static func corner_radius(role: String) -> int:
return int(CORNER_RADII.get(role, CORNER_RADII.control))
static func command_height(variant: String) -> int:
var normalized := normalize_variant(variant)
return int(COMMAND_HEIGHTS.get(normalized, COMMAND_HEIGHTS.secondary))
static func command_width(variant: String) -> int:
var normalized := normalize_variant(variant)
return int(COMMAND_WIDTHS.get(normalized, COMMAND_WIDTHS.secondary))
static func command_size(variant: String, requested := Vector2.ZERO) -> Vector2:
var height := float(command_height(variant))
var width := maxf(MIN_TOUCH_TARGET.x, requested.x)
if requested.x > 0.0:
width = maxf(width, float(command_width(variant)))
return Vector2(width, maxf(height, maxf(MIN_TOUCH_TARGET.y, requested.y)))
static func normalize_variant(variant: String) -> String:
var value := String(variant).to_lower()
if value in COMMAND_VARIANTS:
return value
if value in ["company_primary", "content_primary", "action_primary"]:
return "primary"
if value in ["company_utility", "utility", "content_utility", "action_secondary"]:
return "utility"
if value in ["company_danger", "content_danger", "danger"]:
return "danger"
if value in ["company_tool", "content_tool", "tool", "icon"]:
return "icon"
return "secondary"
static func chrome_variant(variant: String, family := "company") -> String:
var normalized := normalize_variant(variant)
if family == "content":
return "content_primary" if normalized == "primary" else "content_secondary" if normalized == "secondary" else "content_destination" if normalized == "utility" else ""
return "company_primary" if normalized == "primary" else "company_secondary" if normalized == "secondary" else "company_utility" if normalized == "utility" else "company_tool" if normalized == "icon" else ""
static func icon_kind_supported(icon_kind: String) -> bool:
return String(icon_kind).to_lower() in ICON_KINDS
static func icon_color(disabled := false, hot := false, dark_background := true) -> Color:
if disabled:
return COLOR_MUTED
if dark_background:
return COLOR_SKY if hot else COLOR_IVORY
return COLOR_INK if not hot else COLOR_NAVY_LIFT
static func command_colors(variant: String, disabled := false, hot := false, selected := false) -> Dictionary:
var normalized := normalize_variant(variant)
var fill := COLOR_NAVY
var hover_fill := COLOR_NAVY_LIFT
var pressed_fill := COLOR_NAVY_LIFT
var border := COLOR_GOLD
var text := COLOR_IVORY
match normalized:
"primary":
fill = Color("#e8d9b7")
hover_fill = Color("#fff1c9")
pressed_fill = Color("#d7c39a")
border = Color("#806536")
text = COLOR_INK
"danger":
fill = Color("#4d2830")
hover_fill = Color("#68343d")
pressed_fill = Color("#3b2027")
border = COLOR_DANGER
"icon":
fill = Color(COLOR_NAVY, 0.72)
hover_fill = COLOR_NAVY_LIFT
pressed_fill = COLOR_NAVY
border = COLOR_SKY
if selected:
hover_fill = Color("#34506a")
border = COLOR_SKY
if disabled:
fill = COLOR_NAVY_DISABLED
hover_fill = fill
pressed_fill = fill
border = COLOR_MUTED
text = Color("#a1a8aa")
return {
"fill": fill,
"hover": hover_fill if hot else hover_fill,
"pressed": pressed_fill,
"border": border,
"text": text,
"normalized_variant": normalized,
}
static func flat_command_style(variant: String, state := "normal", selected := false, disabled := false) -> StyleBoxFlat:
var colors := command_colors(variant, disabled, state == "hover" or state == "focus", selected)
var fill: Color = colors.fill
if state == "hover" or state == "focus":
fill = colors.hover
elif state == "pressed":
fill = colors.pressed
var style := StyleBoxFlat.new()
style.bg_color = fill
style.border_color = colors.border
style.set_border_width_all(border_width("standard") if colors.normalized_variant == "primary" else border_width("hairline"))
style.set_corner_radius_all(corner_radius("control"))
style.content_margin_left = float(spacing("section"))
style.content_margin_right = float(spacing("section"))
style.content_margin_top = float(spacing("control"))
style.content_margin_bottom = float(spacing("control"))
if state == "focus":
style.shadow_color = Color(COLOR_SKY, 0.32)
style.shadow_size = 3
return style
static func hit_area_style(state := "normal") -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(COLOR_NAVY, 0.10) if state == "normal" else Color(COLOR_GOLD, 0.14) if state == "hover" else Color(COLOR_SKY, 0.16)
style.border_color = Color(COLOR_GOLD_LIGHT, 0.28) if state == "normal" else COLOR_GOLD_LIGHT
style.set_border_width_all(border_width("hairline"))
style.set_corner_radius_all(corner_radius("control"))
return style
static func seal_style(color: Color) -> StyleBoxFlat:
# Small colored identity chip (risk identity markers). The page supplies the
# semantic token color; border, radius and weight stay shared.
var style := StyleBoxFlat.new()
style.bg_color = color
style.border_color = COLOR_IVORY
style.set_border_width_all(border_width("hairline"))
style.set_corner_radius_all(corner_radius("control"))
return style
static func notice_style(severity := "info") -> StyleBoxFlat:
var background := Color(COLOR_NAVY, 0.86)
var border := COLOR_GOLD
match severity:
"success":
background = Color("#214832")
border = Color("#9ed7aa")
"warning":
background = Color("#4d3b23")
border = Color("#f0ca75")
"danger":
background = Color("#4d2830")
border = COLOR_DANGER
var style := StyleBoxFlat.new()
style.bg_color = background
style.border_color = border
style.set_border_width_all(border_width("standard"))
style.set_corner_radius_all(corner_radius("notice"))
style.content_margin_left = float(spacing("section"))
style.content_margin_right = float(spacing("section"))
style.content_margin_top = float(spacing("section"))
style.content_margin_bottom = float(spacing("section"))
return style
static func surface_style(variant := "shell") -> StyleBoxFlat:
var normalized := String(variant).to_lower()
if normalized not in SURFACE_VARIANTS:
normalized = "shell"
var style := StyleBoxFlat.new()
style.bg_color = COLOR_SURFACE if normalized == "shell" else Color(COLOR_SURFACE, 0.34) if normalized == "overlay" else Color(COLOR_SURFACE, 0.82)
style.border_color = COLOR_SURFACE_BORDER if normalized != "notice" else COLOR_GOLD_LIGHT
style.set_border_width_all(border_width("standard"))
style.set_corner_radius_all(corner_radius("surface"))
style.content_margin_left = float(spacing("section"))
style.content_margin_right = float(spacing("section"))
style.content_margin_top = float(spacing("control"))
style.content_margin_bottom = float(spacing("control"))
return style