feat(ui): status_badge, status_dot, stat_tile, empty_state, page_header, icon_button and form classes (#5094)

#5094 "Status and layout macros, and form classes".

- formatting.STATES: one state -> (tone, label) map for up, down, stale,
  firing, pending, acknowledged, no_data, ok, warn, crit, resolved and
  inactive; status_info() reads it (an unmapped state is drawn dim under its
  own name). required() lets a macro refuse a missing argument.
- _macros.html: status_badge and status_dot (a dot always carries its label,
  sr-only unless shown), stat_tile, empty_state, page_header (actions via a
  call block) and icon_button, which raises without a label.
- steward.css: tone classes (badge-/dot-/stat- ok, warn, crit, info, dim),
  .sr-only, .page-header, .stat-tile, .empty-state, .btn-icon and the form
  classes .field, .field-label, .field-hint and .input. Info text is a 40%
  mix (--blue): the house --fs-info-fg falls to 4.18:1 on its tint over a
  hovered row, 40% holds 5.09:1.
- First users: the Alerts list, history and maintenance pages (headers,
  state badges, empty states) and the alert forms' hints; the inline inp/lbl
  style sets on the monitor and host forms become .input / .field-label.
- tests/core/test_macros.py renders each macro through the app environment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:34:21 -04:00
co-authored by Claude Opus 5.5
parent f47fc23265
commit 477b05b9d0
16 changed files with 296 additions and 117 deletions
+67 -3
View File
@@ -17,6 +17,70 @@
</nav>
{%- endmacro %}
{# Metric threshold coloring moved to the `threshold_style(value, kind)` jinja
global (steward/core/settings.py + app.py), which reads operator-configurable
cutoffs from Settings → Thresholds. Call it as threshold_style(v, 'cpu'). #}
{# A state as a labelled badge: up, down, stale, firing, pending, acknowledged,
no_data, ok, warn, crit, resolved, inactive (formatting.STATES). `state` may
be a string or an enum; `label` replaces the standard wording. #}
{% macro status_badge(state, label=none, title=none) -%}
{%- set tone, text = status_info(state) -%}
<span class="badge badge-{{ tone }}"{% if title %} title="{{ title }}"{% endif %}>{{ label or text }}</span>
{%- endmacro %}
{# A state as a coloured dot. The dot is never the only signal: its label is
read out to a screen reader, and shown beside it when `show_label`. #}
{% macro status_dot(state, label=none, show_label=false) -%}
{%- set tone, text = status_info(state) -%}
<span class="dot dot-{{ tone }}" aria-hidden="true" title="{{ label or text }}"></span>
{%- if show_label %}<span class="dot-label">{{ label or text }}</span>{% else %}<span class="sr-only">{{ label or text }}</span>{% endif %}
{%- endmacro %}
{# Threshold colouring: `threshold_style(value, kind)` (an inline style) and
`status_class(value, kind)` ('ok' / 'warn' / 'crit' / 'unknown') are jinja
globals reading the cutoffs from Settings → Thresholds. #}
{# One headline number. `state` (a STATES name, or a status_class result)
colours the value; `href` makes the whole tile a link. #}
{% macro stat_tile(label, value, state=none, hint=none, href=none) -%}
{%- set tone = status_info(state)[0] if state else none -%}
{%- set body -%}
<span class="stat-tile-label">{{ label }}</span>
<span class="stat-tile-value">{{ value }}</span>
{%- if hint %}<span class="stat-tile-hint">{{ hint }}</span>{% endif %}
{%- endset -%}
{%- if href -%}
<a href="{{ href }}" class="stat-tile{% if tone %} stat-{{ tone }}{% endif %}">{{ body }}</a>
{%- else -%}
<div class="stat-tile{% if tone %} stat-{{ tone }}{% endif %}">{{ body }}</div>
{%- endif -%}
{%- endmacro %}
{# What a list shows when it has nothing in it, with an optional next step. #}
{% macro empty_state(message, action_label=none, action_href=none) -%}
<div class="empty-state">
<p>{{ message }}</p>
{%- if action_label and action_href %}<a class="btn btn-ghost btn-sm" href="{{ action_href }}">{{ action_label }}</a>{% endif %}
</div>
{%- endmacro %}
{# The page's <h1>, with its actions to the right:
{% call page_header("Alerts") %}<a class="btn" href="…">New rule</a>{% endcall %} #}
{% macro page_header(title, subtitle=none) -%}
<div class="page-header">
<div>
<h1 class="page-title">{{ title }}</h1>
{%- if subtitle %}<p class="page-subtitle">{{ subtitle }}</p>{% endif %}
</div>
{%- if caller is defined %}<div class="page-actions">{{ caller() }}</div>{% endif %}
</div>
{%- endmacro %}
{# A button that shows only an icon. The label is required: it is the button's
accessible name and its tooltip, and rendering one without it raises. #}
{% macro icon_button(icon, label, href=none, cls="btn-ghost") -%}
{%- set name = required(label, "icon_button label") -%}
{%- if href -%}
<a class="btn btn-sm btn-icon {{ cls }}" href="{{ href }}" aria-label="{{ name }}" title="{{ name }}">{{ icon }}</a>
{%- else -%}
<button type="{{ kwargs.get('type', 'button') }}" class="btn btn-sm btn-icon {{ cls }}" aria-label="{{ name }}" title="{{ name }}"
{%- if kwargs.get('confirm') %} data-confirm="{{ kwargs.confirm }}"{% endif %}>{{ icon }}</button>
{%- endif -%}
{%- endmacro %}