Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- `ui_stat_card` takes `note:` to show a short note under its value, `plain: true` to draw a plain figure with no card border or background, and `fallback:` to show muted text such as "N/A" when its value is missing, and a stat card given none of them looks as it did.

## [0.48.0] - 2026-10-15

### Changed
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,9 @@ Renders a metric card for dashboards.
- `calculation:` (String) — how the metric is computed
- `change:` (Number) — percent change against the previous period, shown as ▲/▼
- `href:` (String) — turns the value into a link to that URL; the card itself stays unlinked
- `note:` (String) — a short note shown under the value
- `plain:` (Boolean, default `false`) — draws a plain figure with no card border or background, for a row of figures
- `fallback:` (String) — shown in a muted colour when the value is missing, such as `"N/A"`

When `definition:` or `calculation:` is given, an info button appears. Hovering or focusing it shows the details in a panel floating below the card, and tapping it toggles the panel on touch screens. The card never changes size.

Expand Down
3 changes: 2 additions & 1 deletion app/components/keystone/ui/stat_card_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
<% end %>
</div>
<p class="<%= value_classes %>">
<% if link? %><a href="<%= href %>" class="<%= value_link_classes %>"><%= value %></a><% else %><%= value %><% end %><% if suffix? %><span class="<%= suffix_classes %>"> <%= suffix %></span><% end %>
<% if missing? %><span class="<%= Keystone::Ui::StatCardComponent::FALLBACK_CLASSES %>"><%= fallback %></span><% elsif link? %><a href="<%= href %>" class="<%= value_link_classes %>"><%= value %></a><% else %><%= value %><% end %><% if suffix? %><span class="<%= suffix_classes %>"> <%= suffix %></span><% end %>
</p>
<% if note %><p class="<%= Keystone::Ui::StatCardComponent::NOTE_CLASSES %>"><%= note %></p><% end %>
<p class="<%= change_row_classes %>"><%= change_label %></p>
</div>
20 changes: 17 additions & 3 deletions app/components/keystone/ui/stat_card_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ class StatCardComponent < ViewComponent::Base
EMPHASIS_CLASSES = "ks-stat-card-emphasis"
HEADER_CLASSES = "ks-stat-card-header flex items-start justify-between"
CARD_CLASSES = "ks-metric-card relative"
PLAIN_CLASSES = "ks-metric-plain relative"
LABEL_CLASSES = "ks-stat-card-label text-sm"
VALUE_BASE_CLASSES = "ks-stat-card-value text-3xl"
SUFFIX_CLASSES = "ks-stat-card-suffix text-lg"
DISCLOSURE_CLASSES = "ks-stat-card-disclosure hidden peer-hover:block peer-focus-visible:block absolute inset-x-0 top-full z-10 text-sm"
CHANGE_ROW_CLASSES = "ks-stat-card-change h-5 text-sm"
VALUE_LINK_CLASSES = "hover:underline focus:outline-none focus-visible:underline"
INFO_BUTTON_CLASSES = "ks-stat-card-info peer shrink-0 transition"
NOTE_CLASSES = "ks-stat-card-note text-sm"
FALLBACK_CLASSES = "ks-stat-card-fallback"

INFO_ICON = <<~SVG.freeze
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
Expand All @@ -26,10 +29,13 @@ class StatCardComponent < ViewComponent::Base
info: "ks-tone-info"
}.freeze

attr_reader :label, :value, :suffix, :definition, :calculation, :change, :href
attr_reader :label, :value, :suffix, :definition, :calculation, :change, :href, :note

def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil)
def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil, note: nil, plain: false, fallback: nil)
@label = label
@fallback = fallback
@note = note
@plain = plain
@value = value
@variant = variant
@suffix = suffix
Expand All @@ -39,8 +45,16 @@ def initialize(label:, value:, variant: :neutral, suffix: nil, definition: nil,
@href = href
end

def missing?
@value.to_s.empty? && !@fallback.nil?
end

def fallback
@fallback
end

def classes
CARD_CLASSES
@plain ? PLAIN_CLASSES : CARD_CLASSES
end

def label_classes
Expand Down
2 changes: 1 addition & 1 deletion keystone_ui.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Gem::Specification.new do |spec|
spec.files = Dir["lib/**/*", "app/**/*", "config/**/*", "the_local/**/*"] + [ "MIT-LICENSE" ]
spec.require_paths = [ "lib" ]

spec.add_dependency "keystone_ui-styles", ">= 0.22.0"
spec.add_dependency "keystone_ui-styles", ">= 0.25.0"
spec.add_dependency "view_component", ">= 2.0", "< 5"

spec.post_install_message = <<~MSG
Expand Down
6 changes: 6 additions & 0 deletions test/keystone_ui/packaging_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ def test_requires_a_keystone_ui_styles_with_the_bar_chart_classes
refute requirement.satisfied_by?(Gem::Version.new("0.21.0"))
end

def test_requires_a_keystone_ui_styles_with_the_stat_note_and_plain_classes
requirement = gemspec.dependencies.find { |d| d.name == "keystone_ui-styles" }.requirement

refute requirement.satisfied_by?(Gem::Version.new("0.24.0"))
end

private

def gemspec
Expand Down
23 changes: 23 additions & 0 deletions test/render/stat_card_report_render_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# frozen_string_literal: true

require "render_helper"

class Keystone::Ui::StatCardReportRenderTest < ViewComponent::TestCase
def test_shows_a_note_under_its_value
page = render_inline(Keystone::Ui::StatCardComponent.new(label: "Commits", value: "553", note: "excluding merge commits"))

assert_equal "excluding merge commits", page.css(".ks-stat-card-value + .ks-stat-card-note").text.strip
end

def test_draws_a_plain_figure_without_the_card_border_when_asked
page = render_inline(Keystone::Ui::StatCardComponent.new(label: "Commits", value: "553", plain: true))

assert_equal [ true, false ], [ page.css(".ks-metric-plain").any?, page.css(".ks-metric-card").any? ]
end

def test_shows_its_fallback_muted_when_its_value_is_missing
page = render_inline(Keystone::Ui::StatCardComponent.new(label: "Reviews", value: "", fallback: "N/A"))

assert_equal "N/A", page.css(".ks-stat-card-value .ks-stat-card-fallback").text.strip
end
end
2 changes: 1 addition & 1 deletion the_local/agents/keystone_ui-develop.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ two exceptions: `ui_page`'s `padding:` treats any value other than `:none` as

10. Explain figures.

- `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil)`
- `ui_stat_card(label:, value:, variant: :neutral, suffix: nil, definition: nil, calculation: nil, change: nil, href: nil, note: nil, plain: false, fallback: nil)`
— `variant:` is `:neutral` `:success` `:danger` `:warning` `:info` and
colours the value. `change:` is a signed number shown as `▲ 4.2%` in
green, `▼` in red, or plain at zero. `href:` links the value only, so
Expand Down
Loading